mirror of
https://github.com/directus/directus.git
synced 2026-01-24 17:17:58 -05:00
RTL support in translation interface (#14665)
* first draft for translations rtl implementation * make direction field dybamic * Fixed default direction field * added directionality to: tags, input-multiline, repeater (list) * added directionality for wysiwyg, input-autocomplete, groups * reverted directionality in wysiwyg-editor * removed hardcoded rtl, ltr buttons from wysiwyg toolbar * working directionality in wysiwyg editor * also add v-if to await language for second language (split-view) in translations.vue * added watcher for changing wysiwyg directionality on language change Co-authored-by: Rijk van Zanten <rijkvanzanten@me.com> Co-authored-by: Martijn de Voogd <devoogd@kissthefrog.nl> Co-authored-by: Martijn <73393707+martijn-dev@users.noreply.github.com> Co-authored-by: Brainslug <br41nslug@users.noreply.github.com>
This commit is contained in:
@@ -26,6 +26,7 @@
|
||||
:field-data="field"
|
||||
:primary-key="primaryKey"
|
||||
:length="field.schema && field.schema.max_length"
|
||||
:direction="direction"
|
||||
@input="$emit('update:modelValue', $event)"
|
||||
@set-field-value="$emit('setFieldValue', $event)"
|
||||
/>
|
||||
@@ -92,6 +93,10 @@ export default defineComponent({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
direction: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
emits: ['update:modelValue', 'setFieldValue'],
|
||||
setup(props) {
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
:primary-key="primaryKey"
|
||||
:raw-editor-enabled="rawEditorEnabled"
|
||||
:raw-editor-active="rawEditorActive"
|
||||
:direction="direction"
|
||||
@update:model-value="emitValue($event)"
|
||||
@set-field-value="$emit('setFieldValue', $event)"
|
||||
/>
|
||||
@@ -98,6 +99,7 @@ interface Props {
|
||||
badge?: string;
|
||||
rawEditorEnabled?: boolean;
|
||||
rawEditorActive?: boolean;
|
||||
direction?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
@@ -113,6 +115,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
badge: undefined,
|
||||
rawEditorEnabled: false,
|
||||
rawEditorActive: false,
|
||||
direction: undefined,
|
||||
});
|
||||
|
||||
const emit = defineEmits(['toggle-batch', 'toggle-raw', 'unset', 'update:modelValue', 'setFieldValue']);
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
:validation-errors="validationErrors"
|
||||
:badge="badge"
|
||||
:raw-editor-enabled="rawEditorEnabled"
|
||||
:direction="direction"
|
||||
v-bind="fieldsMap[fieldName].meta?.options || {}"
|
||||
@apply="apply"
|
||||
/>
|
||||
@@ -65,6 +66,7 @@
|
||||
:badge="badge"
|
||||
:raw-editor-enabled="rawEditorEnabled"
|
||||
:raw-editor-active="rawActiveFields.has(fieldName)"
|
||||
:direction="direction"
|
||||
@update:model-value="setValue(fieldName, $event)"
|
||||
@set-field-value="setValue($event.field, $event.value, { force: true })"
|
||||
@unset="unsetValue(fieldsMap[fieldName])"
|
||||
@@ -153,6 +155,10 @@ export default defineComponent({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
direction: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
setup(props, { emit }) {
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
:loading="loading"
|
||||
:batch-mode="batchMode"
|
||||
:disabled="disabled"
|
||||
:direction="direction"
|
||||
nested
|
||||
@update:model-value="$emit('apply', $event)"
|
||||
/>
|
||||
@@ -99,6 +100,10 @@ export default defineComponent({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
direction: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
emits: ['apply', 'toggleAll'],
|
||||
setup(props, { emit }) {
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
:raw-editor-enabled="rawEditorEnabled"
|
||||
:group="field.meta.field"
|
||||
:multiple="accordionMode === false"
|
||||
:direction="direction"
|
||||
@apply="$emit('apply', $event)"
|
||||
@toggle-all="toggleAll"
|
||||
/>
|
||||
@@ -91,6 +92,10 @@ export default defineComponent({
|
||||
enum: ['opened', 'closed', 'first'],
|
||||
default: 'closed',
|
||||
},
|
||||
direction: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
emits: ['apply'],
|
||||
setup(props) {
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
:batch-mode="batchMode"
|
||||
:disabled="disabled"
|
||||
:badge="badge"
|
||||
:direction="direction"
|
||||
nested
|
||||
@update:model-value="$emit('apply', $event)"
|
||||
/>
|
||||
@@ -112,6 +113,10 @@ export default defineComponent({
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
direction: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
emits: ['apply'],
|
||||
setup(props) {
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
:disabled="disabled"
|
||||
:badge="badge"
|
||||
:raw-editor-enabled="rawEditorEnabled"
|
||||
:direction="direction"
|
||||
nested
|
||||
@update:model-value="$emit('apply', $event)"
|
||||
/>
|
||||
@@ -71,6 +72,10 @@ export default defineComponent({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
direction: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
emits: ['apply'],
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
:disabled="disabled"
|
||||
:class="font"
|
||||
:model-value="value"
|
||||
:dir="direction"
|
||||
@update:model-value="onInput"
|
||||
@focus="activate"
|
||||
>
|
||||
@@ -85,6 +86,10 @@ export default defineComponent({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
direction: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
emits: ['input'],
|
||||
setup(props, { emit }) {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
:nullable="!clear"
|
||||
:disabled="disabled"
|
||||
:class="font"
|
||||
:dir="direction"
|
||||
@update:model-value="$emit('input', $event)"
|
||||
>
|
||||
<template v-if="(percentageRemaining && percentageRemaining <= 20) || softLength" #append>
|
||||
@@ -55,6 +56,10 @@ export default defineComponent({
|
||||
type: Number,
|
||||
default: undefined,
|
||||
},
|
||||
direction: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
emits: ['input'],
|
||||
setup(props) {
|
||||
|
||||
@@ -176,7 +176,7 @@
|
||||
<script lang="ts">
|
||||
import Editor from '@tinymce/tinymce-vue';
|
||||
import { percentage } from '@/utils/percentage';
|
||||
import { ComponentPublicInstance, computed, defineComponent, PropType, ref, toRefs } from 'vue';
|
||||
import { ComponentPublicInstance, computed, defineComponent, PropType, ref, toRefs, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import getEditorStyles from './get-editor-styles';
|
||||
import useImage from './useImage';
|
||||
@@ -273,6 +273,10 @@ export default defineComponent({
|
||||
type: Number,
|
||||
default: undefined,
|
||||
},
|
||||
direction: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
emits: ['input'],
|
||||
setup(props, { emit }) {
|
||||
@@ -323,11 +327,27 @@ export default defineComponent({
|
||||
get() {
|
||||
return props.value || '';
|
||||
},
|
||||
set() {
|
||||
set(value) {
|
||||
if (props.value !== value) {
|
||||
contentUpdated();
|
||||
}
|
||||
return;
|
||||
},
|
||||
});
|
||||
|
||||
watch(
|
||||
() => [props.direction, editorRef],
|
||||
() => {
|
||||
if (editorRef.value) {
|
||||
if (props.direction === 'rtl') {
|
||||
editorRef.value.editorCommands?.commands?.exec?.mcedirectionrtl();
|
||||
} else {
|
||||
editorRef.value.editorCommands?.commands?.exec?.mcedirectionltr();
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const editorOptions = computed(() => {
|
||||
let styleFormats = null;
|
||||
|
||||
@@ -369,6 +389,7 @@ export default defineComponent({
|
||||
file_picker_types: 'customImage customMedia image media',
|
||||
link_default_protocol: 'https',
|
||||
browser_spellcheck: true,
|
||||
directionality: props.direction,
|
||||
setup,
|
||||
...(props.tinymceOverrides || {}),
|
||||
};
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
:min="min"
|
||||
:max="max"
|
||||
:step="step"
|
||||
:dir="direction"
|
||||
:autocomplete="masked ? 'new-password' : 'off'"
|
||||
@update:model-value="$emit('input', $event)"
|
||||
>
|
||||
@@ -111,6 +112,10 @@ export default defineComponent({
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
direction: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
emits: ['input'],
|
||||
setup(props) {
|
||||
|
||||
@@ -19,7 +19,12 @@
|
||||
<template #item="{ element, index }">
|
||||
<v-list-item :dense="internalValue.length > 4" block @click="openItem(index)">
|
||||
<v-icon v-if="!disabled && !sort" name="drag_handle" class="drag-handle" left @click.stop="() => {}" />
|
||||
<render-template :fields="fields" :item="{ ...defaults, ...element }" :template="templateWithDefaults" />
|
||||
<render-template
|
||||
:fields="fields"
|
||||
:item="{ ...defaults, ...element }"
|
||||
:direction="direction"
|
||||
:template="templateWithDefaults"
|
||||
/>
|
||||
<div class="spacer" />
|
||||
<v-icon v-if="!disabled" name="close" @click.stop="removeItem(element)" />
|
||||
</v-list-item>
|
||||
@@ -54,6 +59,7 @@
|
||||
:disabled="disabled"
|
||||
:fields="fieldsWithNames"
|
||||
:model-value="activeItem"
|
||||
:direction="direction"
|
||||
autofocus
|
||||
primary-key="+"
|
||||
@update:model-value="trackEdits($event)"
|
||||
@@ -130,6 +136,10 @@ export default defineComponent({
|
||||
type: String,
|
||||
default: () => i18n.global.t('no_items'),
|
||||
},
|
||||
direction: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
emits: ['input'],
|
||||
setup(props, { emit }) {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
v-if="allowCustom"
|
||||
:placeholder="placeholder || t('interfaces.tags.add_tags')"
|
||||
:disabled="disabled"
|
||||
:dir="direction"
|
||||
@keydown="onInput"
|
||||
>
|
||||
<template v-if="iconLeft" #prepend><v-icon :name="iconLeft" /></template>
|
||||
@@ -16,6 +17,7 @@
|
||||
:key="preset"
|
||||
:class="['tag', { inactive: !selectedVals.includes(preset) }]"
|
||||
:disabled="disabled"
|
||||
:dir="direction"
|
||||
small
|
||||
label
|
||||
clickable
|
||||
@@ -30,6 +32,7 @@
|
||||
v-for="val in customVals"
|
||||
:key="val"
|
||||
:disabled="disabled"
|
||||
:dir="direction"
|
||||
class="tag"
|
||||
small
|
||||
label
|
||||
@@ -90,6 +93,10 @@ export default defineComponent({
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
direction: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
emits: ['input'],
|
||||
setup(props, { emit }) {
|
||||
|
||||
@@ -38,6 +38,21 @@ export default defineInterface({
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'languageDirectionField',
|
||||
type: 'string',
|
||||
name: '$t:interfaces.translations.language_direction_field',
|
||||
schema: {
|
||||
data_type: 'string',
|
||||
default_value: choices.some((choice) => choice.value === 'direction') ? 'direction' : null,
|
||||
},
|
||||
meta: {
|
||||
interface: 'select-dropdown',
|
||||
options: {
|
||||
choices,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'defaultLanguage',
|
||||
name: '$t:interfaces.translations.default_language',
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
</template>
|
||||
</language-select>
|
||||
<v-form
|
||||
v-if="languageOptions.find((lang) => lang.value === firstLang)"
|
||||
:primary-key="
|
||||
relationInfo?.junctionPrimaryKeyField.field
|
||||
? firstItemInitial?.[relationInfo?.junctionPrimaryKeyField.field]
|
||||
@@ -24,6 +25,7 @@
|
||||
:model-value="firstItem"
|
||||
:initial-values="firstItemInitial"
|
||||
:badge="languageOptions.find((lang) => lang.value === firstLang)?.text"
|
||||
:direction="languageOptions.find((lang) => lang.value === firstLang)?.direction"
|
||||
:autofocus="autofocus"
|
||||
@update:model-value="updateValue($event, firstLang)"
|
||||
/>
|
||||
@@ -41,6 +43,7 @@
|
||||
</template>
|
||||
</language-select>
|
||||
<v-form
|
||||
v-if="languageOptions.find((lang) => lang.value === secondLang)"
|
||||
:primary-key="
|
||||
relationInfo?.junctionPrimaryKeyField.field
|
||||
? secondItemInitial?.[relationInfo?.junctionPrimaryKeyField.field]
|
||||
@@ -51,6 +54,7 @@
|
||||
:initial-values="secondItemInitial"
|
||||
:fields="fields"
|
||||
:badge="languageOptions.find((lang) => lang.value === secondLang)?.text"
|
||||
:direction="languageOptions.find((lang) => lang.value === secondLang)?.direction"
|
||||
:model-value="secondItem"
|
||||
@update:model-value="updateValue($event, secondLang)"
|
||||
/>
|
||||
@@ -78,6 +82,7 @@ const props = withDefaults(
|
||||
field: string;
|
||||
primaryKey: string | number;
|
||||
languageField?: string | null;
|
||||
languageDirectionField?: string | null;
|
||||
defaultLanguage?: string | null;
|
||||
userLanguage?: boolean;
|
||||
value: (number | string | Record<string, any>)[] | Record<string, any>;
|
||||
@@ -86,6 +91,7 @@ const props = withDefaults(
|
||||
}>(),
|
||||
{
|
||||
languageField: () => null,
|
||||
languageDirectionField: () => 'direction',
|
||||
value: () => [],
|
||||
autofocus: false,
|
||||
disabled: false,
|
||||
@@ -220,6 +226,7 @@ function useLanguages() {
|
||||
|
||||
return {
|
||||
text: language[props.languageField ?? relationInfo.value.relatedPrimaryKeyField.field],
|
||||
direction: props.languageDirectionField ? language[props.languageDirectionField] : undefined,
|
||||
value: langCode,
|
||||
edited: edits?.$type !== undefined,
|
||||
progress: Math.round((filledFields / totalFields) * 100),
|
||||
@@ -240,6 +247,10 @@ function useLanguages() {
|
||||
fields.add(props.languageField);
|
||||
}
|
||||
|
||||
if (props.languageDirectionField !== null) {
|
||||
fields.add(props.languageDirectionField);
|
||||
}
|
||||
|
||||
const pkField = relationInfo.value.relatedPrimaryKeyField.field;
|
||||
|
||||
fields.add(pkField);
|
||||
|
||||
@@ -301,6 +301,8 @@ next: Next
|
||||
field_name: Field Name
|
||||
translations: Translations
|
||||
no_translations: No Translations
|
||||
left_to_right: Left to Right
|
||||
right_to_left: Right to Left
|
||||
note: Note
|
||||
enter_a_value: Enter a value...
|
||||
enter_a_placeholder: Enter a placeholder...
|
||||
@@ -1682,6 +1684,7 @@ interfaces:
|
||||
user_language: Use Current User Language
|
||||
default_language: Default Language
|
||||
language_field: Language Indicator Field
|
||||
language_direction_field: Language Direction Field
|
||||
list-o2m-tree-view:
|
||||
description: Tree view for nested recursive one-to-many items
|
||||
recursive_only: The tree view interface only works for recursive relationships.
|
||||
|
||||
@@ -181,6 +181,43 @@ export function generateCollections(updates: StateUpdates, state: State, { getCu
|
||||
schema: {},
|
||||
meta: {},
|
||||
},
|
||||
// Open for discussion: we might want to limit choices to 'ltr' and 'rtl'
|
||||
{
|
||||
field: 'direction',
|
||||
type: 'string',
|
||||
schema: {
|
||||
default_value: 'ltr',
|
||||
},
|
||||
meta: {
|
||||
interface: 'select-dropdown',
|
||||
options: {
|
||||
choices: [
|
||||
{
|
||||
text: '$t:left_to_right',
|
||||
value: 'ltr',
|
||||
},
|
||||
{
|
||||
text: '$t:right_to_left',
|
||||
value: 'rtl',
|
||||
},
|
||||
],
|
||||
},
|
||||
display: 'labels',
|
||||
display_options: {
|
||||
choices: [
|
||||
{
|
||||
text: '$t:left_to_right',
|
||||
value: 'ltr',
|
||||
},
|
||||
{
|
||||
text: '$t:right_to_left',
|
||||
value: 'rtl',
|
||||
},
|
||||
],
|
||||
format: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -194,30 +231,42 @@ export function generateCollections(updates: StateUpdates, state: State, { getCu
|
||||
{
|
||||
code: 'en-US',
|
||||
name: 'English',
|
||||
direction: 'ltr',
|
||||
},
|
||||
{
|
||||
code: 'ar-SA',
|
||||
name: 'Arabic',
|
||||
direction: 'rtl',
|
||||
},
|
||||
{
|
||||
code: 'de-DE',
|
||||
name: 'German',
|
||||
direction: 'ltr',
|
||||
},
|
||||
{
|
||||
code: 'fr-FR',
|
||||
name: 'French',
|
||||
direction: 'ltr',
|
||||
},
|
||||
{
|
||||
code: 'ru-RU',
|
||||
name: 'Russian',
|
||||
direction: 'ltr',
|
||||
},
|
||||
{
|
||||
code: 'es-ES',
|
||||
name: 'Spanish',
|
||||
direction: 'ltr',
|
||||
},
|
||||
{
|
||||
code: 'it-IT',
|
||||
name: 'Italian',
|
||||
direction: 'ltr',
|
||||
},
|
||||
{
|
||||
code: 'pt-BR',
|
||||
name: 'Portuguese',
|
||||
direction: 'ltr',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
:collection="part.collection"
|
||||
:field="part.field"
|
||||
/>
|
||||
<span v-else-if="typeof part === 'string'">{{ translate(part) }}</span>
|
||||
<span v-else-if="typeof part === 'string'" :dir="direction">{{ translate(part) }}</span>
|
||||
<span v-else>{{ part }}</span>
|
||||
</template>
|
||||
</div>
|
||||
@@ -47,6 +47,10 @@ export default defineComponent({
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
direction: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const fieldsStore = useFieldsStore();
|
||||
|
||||
Reference in New Issue
Block a user