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:
Ramon van Bezouw
2022-08-09 15:23:05 +02:00
committed by GitHub
parent 3e4ca34f0e
commit df054f294d
18 changed files with 173 additions and 4 deletions

View File

@@ -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) {

View File

@@ -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']);

View File

@@ -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 }) {

View File

@@ -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 }) {

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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'],
});

View File

@@ -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 }) {

View File

@@ -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) {

View File

@@ -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 || {}),
};

View File

@@ -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) {

View File

@@ -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 }) {

View File

@@ -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 }) {

View File

@@ -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',

View File

@@ -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);

View File

@@ -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.

View File

@@ -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',
},
],
};

View File

@@ -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();