mirror of
https://github.com/directus/directus.git
synced 2026-02-15 04:25:03 -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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user