From 37cbaa0be587744168239a7bb08d26f1fb579658 Mon Sep 17 00:00:00 2001 From: Azri Kahar <42867097+azrikahar@users.noreply.github.com> Date: Tue, 22 Mar 2022 21:39:04 +0800 Subject: [PATCH] Add App Translation Strings in Settings (#12170) * add migration for translation strings * add to navigation * WIP * fix dialog overflow * update translation keys * Update logic * add placeholder to system-language * fix translation * remove unused import * reset dialog on create new * ensure search input is visible when searching * merge translation strings on set language * merge translation strings on update * hydrate * make sure null translation do not get merged * change dialog to drawer * update placeholder text * fix form value * revert dialog style change * rename drawer component * Force safe key name * Move interface to system interfaces The saved values are Directus app proprietary, so to prevent confusion in what it's supposed to do, we'll move it to system. * Move composable to root composables * Use new languages input in interface/display options * hide translation strings field in project settings * set system true to system-input-translated-string * use this in field detail notes * use in list options Co-authored-by: rijkvanzanten --- .../20220314A-add-translation-strings.ts | 13 + .../database/system-data/fields/settings.yaml | 3 + .../composables/use-translation-strings.ts | 151 ++++++++++ app/src/displays/boolean/index.ts | 4 +- app/src/displays/formatted-value/index.ts | 6 +- app/src/displays/labels/index.ts | 2 +- app/src/hydrate.ts | 3 + .../system-input-translated-string/index.ts | 29 ++ .../input-translated-string.vue | 261 ++++++++++++++++++ .../preview.svg | 11 + .../system-language/system-language.vue | 13 +- app/src/interfaces/boolean/index.ts | 2 +- .../input-autocomplete-api/index.ts | 2 +- app/src/interfaces/input-hash/index.ts | 2 +- app/src/interfaces/input-multiline/index.ts | 2 +- .../interfaces/input-rich-text-md/index.ts | 2 +- app/src/interfaces/input/index.ts | 4 +- app/src/interfaces/list/options.vue | 9 +- .../interfaces/presentation-divider/index.ts | 2 +- .../interfaces/presentation-links/index.ts | 2 +- .../interfaces/presentation-notice/index.ts | 2 +- app/src/interfaces/select-color/index.ts | 2 +- app/src/interfaces/select-dropdown/index.ts | 2 +- .../select-multiple-checkbox-tree/index.ts | 2 +- .../select-multiple-checkbox/index.ts | 2 +- .../select-multiple-dropdown/index.ts | 4 +- app/src/interfaces/select-radio/index.ts | 2 +- app/src/interfaces/tags/index.ts | 2 +- app/src/lang/set-language.ts | 3 + app/src/lang/translations/en-US.yaml | 14 + .../settings/components/navigation.vue | 5 + app/src/modules/settings/index.ts | 12 + .../field-detail-advanced-field.vue | 7 +- .../routes/translation-strings/collection.vue | 136 +++++++++ .../translation-strings-drawer.vue | 214 ++++++++++++++ .../translation-strings-tooltip.vue | 136 +++++++++ packages/shared/src/types/settings.ts | 1 + 37 files changed, 1035 insertions(+), 34 deletions(-) create mode 100644 api/src/database/migrations/20220314A-add-translation-strings.ts create mode 100644 app/src/composables/use-translation-strings.ts create mode 100644 app/src/interfaces/_system/system-input-translated-string/index.ts create mode 100644 app/src/interfaces/_system/system-input-translated-string/input-translated-string.vue create mode 100644 app/src/interfaces/_system/system-input-translated-string/preview.svg create mode 100644 app/src/modules/settings/routes/translation-strings/collection.vue create mode 100644 app/src/modules/settings/routes/translation-strings/translation-strings-drawer.vue create mode 100644 app/src/modules/settings/routes/translation-strings/translation-strings-tooltip.vue diff --git a/api/src/database/migrations/20220314A-add-translation-strings.ts b/api/src/database/migrations/20220314A-add-translation-strings.ts new file mode 100644 index 0000000000..b674694a10 --- /dev/null +++ b/api/src/database/migrations/20220314A-add-translation-strings.ts @@ -0,0 +1,13 @@ +import { Knex } from 'knex'; + +export async function up(knex: Knex): Promise { + await knex.schema.alterTable('directus_settings', (table) => { + table.json('translation_strings'); + }); +} + +export async function down(knex: Knex): Promise { + await knex.schema.alterTable('directus_settings', (table) => { + table.dropColumn('translation_strings'); + }); +} diff --git a/api/src/database/system-data/fields/settings.yaml b/api/src/database/system-data/fields/settings.yaml index 2206347a3d..87d942a6b3 100644 --- a/api/src/database/system-data/fields/settings.yaml +++ b/api/src/database/system-data/fields/settings.yaml @@ -378,3 +378,6 @@ fields: interface: input options: placeholder: $t:fields.directus_settings.attribution_placeholder + + - field: translation_strings + hidden: true diff --git a/app/src/composables/use-translation-strings.ts b/app/src/composables/use-translation-strings.ts new file mode 100644 index 0000000000..3704c6f8bd --- /dev/null +++ b/app/src/composables/use-translation-strings.ts @@ -0,0 +1,151 @@ +import { ref, Ref } from 'vue'; +import api from '@/api'; +import { unexpectedError } from '@/utils/unexpected-error'; +import { Language, i18n } from '@/lang'; +import { useUserStore } from '@/stores'; + +export type Translation = { + language: string; + translation: string; +}; + +export type TranslationStringRaw = { + key?: string | null; + translations?: Record | null; +}; + +export type TranslationString = { + key?: string | null; + translations?: Translation[] | null; +}; + +type UsableTranslationStrings = { + loading: Ref; + error: Ref; + translationStrings: Ref; + refresh: () => Promise; + updating: Ref; + update: (newTranslationStrings: TranslationString[]) => Promise; + mergeTranslationStringsForLanguage: (lang: Language) => void; +}; + +let loading: Ref | null = null; +let translationStrings: Ref | null = null; +let error: Ref | null = null; + +export function useTranslationStrings(): UsableTranslationStrings { + if (loading === null) loading = ref(false); + if (error === null) error = ref(null); + if (translationStrings === null) translationStrings = ref(null); + const updating = ref(false); + + return { loading, error, translationStrings, refresh, updating, update, mergeTranslationStringsForLanguage }; + + async function fetchTranslationStrings() { + if (loading === null) return; + if (translationStrings === null) return; + if (error === null) return; + + loading.value = true; + error.value = null; + + try { + const response = await api.get('/settings', { + params: { + fields: ['translation_strings'], + }, + }); + + const { translation_strings } = response.data.data; + + if (translation_strings) { + translationStrings.value = translation_strings.map((p: TranslationStringRaw) => ({ + key: p.key, + translations: getTranslationsFromKeyValues(p.translations ?? null), + })); + } + } catch (err: any) { + error.value = err; + } finally { + loading.value = false; + } + } + + async function refresh() { + if (loading === null) return; + if (translationStrings === null) return; + if (error === null) return; + + loading.value = false; + error.value = null; + + await fetchTranslationStrings(); + } + + async function update(newTranslationStrings: TranslationString[]) { + if (loading === null) return; + if (translationStrings === null) return; + if (error === null) return; + + updating.value = true; + + const resultingTranslationStrings = getUniqueTranslationStrings([...newTranslationStrings]); + + const payload = resultingTranslationStrings.map((p: TranslationString) => ({ + key: p.key, + translations: getKeyValuesFromTranslations(p.translations), + })); + + try { + const settingsResponse = await api.patch('/settings', { + translation_strings: payload, + }); + if (settingsResponse.data.data.translation_strings) { + translationStrings.value = settingsResponse.data.data.translation_strings.map((p: TranslationStringRaw) => ({ + key: p.key, + translations: getTranslationsFromKeyValues(p.translations ?? null), + })); + + const { currentUser } = useUserStore(); + if (currentUser && 'language' in currentUser && currentUser.language) { + mergeTranslationStringsForLanguage(currentUser.language); + } else { + mergeTranslationStringsForLanguage('en-US'); + } + } + } catch (err: any) { + unexpectedError(err); + } finally { + updating.value = false; + } + } + + function mergeTranslationStringsForLanguage(lang: Language) { + if (!translationStrings?.value) return; + const localeMessages: Record = translationStrings.value.reduce((acc, cur) => { + if (!cur.key || !cur.translations) return acc; + const translationForCurrentLang = cur.translations.find((t) => t.language === lang); + if (!translationForCurrentLang || !translationForCurrentLang.translation) return acc; + return { ...acc, [cur.key]: translationForCurrentLang.translation }; + }, {}); + i18n.global.mergeLocaleMessage(lang, localeMessages); + } + + function getUniqueTranslationStrings(arr: TranslationString[]): TranslationString[] { + return [...new Map(arr.map((item: TranslationString) => [item.key, item])).values()]; + } + + function getKeyValuesFromTranslations(val: TranslationString['translations'] | null): Record { + if (!val || (val && val.length === 0)) return {}; + + return val.reduce((acc, cur) => { + return { ...acc, [cur.language]: cur.translation }; + }, {}); + } + + function getTranslationsFromKeyValues(val: Record | null): TranslationString['translations'] { + if (!val || Object.keys(val).length === 0) return []; + + return Object.entries(val).map(([k, v]) => ({ language: k, translation: v })); + } +} diff --git a/app/src/displays/boolean/index.ts b/app/src/displays/boolean/index.ts index 1cbca41cf2..acbcbbf723 100644 --- a/app/src/displays/boolean/index.ts +++ b/app/src/displays/boolean/index.ts @@ -14,7 +14,7 @@ export default defineDisplay({ name: '$t:displays.boolean.label_on', type: 'string', meta: { - interface: 'input', + interface: 'system-input-translated-string', width: 'half', options: { placeholder: '$t:displays.boolean.label_on_placeholder', @@ -26,7 +26,7 @@ export default defineDisplay({ name: '$t:displays.boolean.label_off', type: 'string', meta: { - interface: 'input', + interface: 'system-input-translated-string', width: 'half', options: { placeholder: '$t:displays.boolean.label_off_placeholder', diff --git a/app/src/displays/formatted-value/index.ts b/app/src/displays/formatted-value/index.ts index fc5ac5efa7..df8216df11 100644 --- a/app/src/displays/formatted-value/index.ts +++ b/app/src/displays/formatted-value/index.ts @@ -84,7 +84,7 @@ export default defineDisplay({ type: 'string', meta: { width: 'half', - interface: 'input', + interface: 'system-input-translated-string', options: { label: '$t:displays.formatted-value.prefix_label', trim: false, @@ -97,7 +97,7 @@ export default defineDisplay({ type: 'string', meta: { width: 'half', - interface: 'input', + interface: 'system-input-translated-string', options: { label: '$t:displays.formatted-value.suffix_label', trim: false, @@ -209,7 +209,7 @@ export default defineDisplay({ name: '$t:displays.formatted-value.text', type: 'string', meta: { - interface: 'input', + interface: 'system-input-translated-string', width: 'half', options: { placeholder: '$t:displays.formatted-value.text_placeholder', diff --git a/app/src/displays/labels/index.ts b/app/src/displays/labels/index.ts index 812a0d3568..a20dd4a365 100644 --- a/app/src/displays/labels/index.ts +++ b/app/src/displays/labels/index.ts @@ -50,7 +50,7 @@ export default defineDisplay({ name: '$t:text', type: 'string', meta: { - interface: 'input', + interface: 'system-input-translated-string', width: 'half', options: { placeholder: '$t:displays.labels.choices_text_placeholder', diff --git a/app/src/hydrate.ts b/app/src/hydrate.ts index 73ff0cb0f5..ca698c96d1 100644 --- a/app/src/hydrate.ts +++ b/app/src/hydrate.ts @@ -16,6 +16,7 @@ import { useUserStore, useNotificationsStore, } from '@/stores'; +import { useTranslationStrings } from '@/composables/use-translation-strings'; type GenericStore = { $id: string; @@ -50,6 +51,7 @@ export async function hydrate(): Promise { const appStore = useAppStore(); const userStore = useUserStore(); const permissionsStore = usePermissionsStore(); + const { refresh: hydrateTranslationStrings } = useTranslationStrings(); if (appStore.hydrated) return; if (appStore.hydrating) return; @@ -71,6 +73,7 @@ export async function hydrate(): Promise { await Promise.all(stores.filter(({ $id }) => !hydratedStores.includes($id)).map((store) => store.hydrate?.())); await registerModules(); + await hydrateTranslationStrings(); await setLanguage(userStore.currentUser?.language ?? 'en-US'); } diff --git a/app/src/interfaces/_system/system-input-translated-string/index.ts b/app/src/interfaces/_system/system-input-translated-string/index.ts new file mode 100644 index 0000000000..01f1a812e9 --- /dev/null +++ b/app/src/interfaces/_system/system-input-translated-string/index.ts @@ -0,0 +1,29 @@ +import { defineInterface } from '@directus/shared/utils'; +import InterfaceInputTranslatedString from './input-translated-string.vue'; +import PreviewSVG from './preview.svg?raw'; + +export default defineInterface({ + id: 'system-input-translated-string', + name: '$t:interfaces.input-translated-string.input-translated-string', + description: '$t:interfaces.input-translated-string.description', + icon: 'translate', + component: InterfaceInputTranslatedString, + system: true, + types: ['string', 'text'], + group: 'standard', + preview: PreviewSVG, + options: [ + { + field: 'placeholder', + name: '$t:placeholder', + type: 'string', + meta: { + width: 'half', + interface: 'input', + options: { + placeholder: '$t:enter_a_placeholder', + }, + }, + }, + ], +}); diff --git a/app/src/interfaces/_system/system-input-translated-string/input-translated-string.vue b/app/src/interfaces/_system/system-input-translated-string/input-translated-string.vue new file mode 100644 index 0000000000..b562fe2482 --- /dev/null +++ b/app/src/interfaces/_system/system-input-translated-string/input-translated-string.vue @@ -0,0 +1,261 @@ + + + + + diff --git a/app/src/interfaces/_system/system-input-translated-string/preview.svg b/app/src/interfaces/_system/system-input-translated-string/preview.svg new file mode 100644 index 0000000000..9b544235dc --- /dev/null +++ b/app/src/interfaces/_system/system-input-translated-string/preview.svg @@ -0,0 +1,11 @@ + + + + + + + + + + diff --git a/app/src/interfaces/_system/system-language/system-language.vue b/app/src/interfaces/_system/system-language/system-language.vue index 36583e0fb0..f324991101 100644 --- a/app/src/interfaces/_system/system-language/system-language.vue +++ b/app/src/interfaces/_system/system-language/system-language.vue @@ -1,9 +1,16 @@ diff --git a/app/src/interfaces/boolean/index.ts b/app/src/interfaces/boolean/index.ts index 8105a569c1..b02438ad54 100644 --- a/app/src/interfaces/boolean/index.ts +++ b/app/src/interfaces/boolean/index.ts @@ -60,7 +60,7 @@ export default defineInterface({ type: 'string', meta: { width: 'half', - interface: 'input', + interface: 'system-input-translated-string', options: { placeholder: '$t:interfaces.boolean.label_placeholder', }, diff --git a/app/src/interfaces/input-autocomplete-api/index.ts b/app/src/interfaces/input-autocomplete-api/index.ts index 51c1e82be9..39abef43dd 100644 --- a/app/src/interfaces/input-autocomplete-api/index.ts +++ b/app/src/interfaces/input-autocomplete-api/index.ts @@ -106,7 +106,7 @@ export default defineInterface({ name: '$t:placeholder', meta: { width: 'half', - interface: 'input', + interface: 'system-input-translated-string', options: { placeholder: '$t:enter_a_placeholder', }, diff --git a/app/src/interfaces/input-hash/index.ts b/app/src/interfaces/input-hash/index.ts index ef4cab11bc..d45c9497d4 100644 --- a/app/src/interfaces/input-hash/index.ts +++ b/app/src/interfaces/input-hash/index.ts @@ -18,7 +18,7 @@ export default defineInterface({ type: 'string', meta: { width: 'half', - interface: 'input', + interface: 'system-input-translated-string', options: { placeholder: '$t:enter_a_placeholder', }, diff --git a/app/src/interfaces/input-multiline/index.ts b/app/src/interfaces/input-multiline/index.ts index 2fe06275e5..a0bf2f11ab 100644 --- a/app/src/interfaces/input-multiline/index.ts +++ b/app/src/interfaces/input-multiline/index.ts @@ -17,7 +17,7 @@ export default defineInterface({ type: 'string', meta: { width: 'full', - interface: 'input-multiline', + interface: 'system-input-translated-string', options: { placeholder: '$t:enter_a_placeholder', }, diff --git a/app/src/interfaces/input-rich-text-md/index.ts b/app/src/interfaces/input-rich-text-md/index.ts index ae55abda38..e58cc6080d 100644 --- a/app/src/interfaces/input-rich-text-md/index.ts +++ b/app/src/interfaces/input-rich-text-md/index.ts @@ -92,7 +92,7 @@ export default defineInterface({ type: 'string', meta: { width: 'half', - interface: 'input', + interface: 'system-input-translated-string', options: { placeholder: '$t:enter_a_placeholder', }, diff --git a/app/src/interfaces/input/index.ts b/app/src/interfaces/input/index.ts index 2787a447ea..81641077ee 100644 --- a/app/src/interfaces/input/index.ts +++ b/app/src/interfaces/input/index.ts @@ -19,7 +19,7 @@ export default defineInterface({ name: '$t:placeholder', meta: { width: 'full', - interface: 'input', + interface: 'system-input-translated-string', options: { placeholder: '$t:enter_a_placeholder', }, @@ -178,7 +178,7 @@ export default defineInterface({ type: 'string', meta: { width: 'half', - interface: 'input', + interface: 'system-input-translated-string', options: { placeholder: '$t:enter_a_placeholder', }, diff --git a/app/src/interfaces/list/options.vue b/app/src/interfaces/list/options.vue index a23e251572..fb1cf55195 100644 --- a/app/src/interfaces/list/options.vue +++ b/app/src/interfaces/list/options.vue @@ -7,7 +7,12 @@

{{ t('interfaces.list.add_label') }}

- +
@@ -135,7 +140,7 @@ export default defineComponent({ field: 'note', type: 'string', meta: { - interface: 'input', + interface: 'system-input-translated-string', width: 'full', sort: 6, options: { diff --git a/app/src/interfaces/presentation-divider/index.ts b/app/src/interfaces/presentation-divider/index.ts index f2524fdf1a..1fa76cbff0 100644 --- a/app/src/interfaces/presentation-divider/index.ts +++ b/app/src/interfaces/presentation-divider/index.ts @@ -20,7 +20,7 @@ export default defineInterface({ type: 'string', meta: { width: 'full', - interface: 'input', + interface: 'system-input-translated-string', options: { placeholder: '$t:interfaces.presentation-divider.title_placeholder', }, diff --git a/app/src/interfaces/presentation-links/index.ts b/app/src/interfaces/presentation-links/index.ts index ed3e3f8cb5..dda16db6bb 100644 --- a/app/src/interfaces/presentation-links/index.ts +++ b/app/src/interfaces/presentation-links/index.ts @@ -28,7 +28,7 @@ export default defineInterface({ name: '$t:label', meta: { width: 'full', - interface: 'input', + interface: 'system-input-translated-string', options: { placeholder: '$t:label', }, diff --git a/app/src/interfaces/presentation-notice/index.ts b/app/src/interfaces/presentation-notice/index.ts index d9fde47244..54db614b72 100644 --- a/app/src/interfaces/presentation-notice/index.ts +++ b/app/src/interfaces/presentation-notice/index.ts @@ -50,7 +50,7 @@ export default defineInterface({ type: 'string', meta: { width: 'full', - interface: 'input-multiline', + interface: 'system-input-translated-string', options: { placeholder: '$t:interfaces.presentation-notice.text', }, diff --git a/app/src/interfaces/select-color/index.ts b/app/src/interfaces/select-color/index.ts index b079167ef1..647a65c56d 100644 --- a/app/src/interfaces/select-color/index.ts +++ b/app/src/interfaces/select-color/index.ts @@ -40,7 +40,7 @@ export default defineInterface({ type: 'string', name: '$t:name', meta: { - interface: 'input', + interface: 'system-input-translated-string', width: 'half', options: { placeholder: '$t:interfaces.select-color.name_placeholder', diff --git a/app/src/interfaces/select-dropdown/index.ts b/app/src/interfaces/select-dropdown/index.ts index f244992c01..4f5e07455e 100644 --- a/app/src/interfaces/select-dropdown/index.ts +++ b/app/src/interfaces/select-dropdown/index.ts @@ -97,7 +97,7 @@ export default defineInterface({ type: 'string', meta: { width: 'half', - interface: 'input', + interface: 'system-input-translated-string', options: { placeholder: '$t:enter_a_placeholder', }, diff --git a/app/src/interfaces/select-multiple-checkbox-tree/index.ts b/app/src/interfaces/select-multiple-checkbox-tree/index.ts index df0861ab2e..6f1946bbbf 100644 --- a/app/src/interfaces/select-multiple-checkbox-tree/index.ts +++ b/app/src/interfaces/select-multiple-checkbox-tree/index.ts @@ -10,7 +10,7 @@ const repeaterFields: DeepPartial[] = [ name: '$t:text', meta: { width: 'half', - interface: 'input', + interface: 'system-input-translated-string', options: { placeholder: '$t:interfaces.select-dropdown.choices_name_placeholder', }, diff --git a/app/src/interfaces/select-multiple-checkbox/index.ts b/app/src/interfaces/select-multiple-checkbox/index.ts index 3115ffb2fe..ab807e1568 100644 --- a/app/src/interfaces/select-multiple-checkbox/index.ts +++ b/app/src/interfaces/select-multiple-checkbox/index.ts @@ -28,7 +28,7 @@ export default defineInterface({ name: '$t:text', meta: { width: 'half', - interface: 'input', + interface: 'system-input-translated-string', options: { placeholder: '$t:interfaces.select-dropdown.choices_name_placeholder', }, diff --git a/app/src/interfaces/select-multiple-dropdown/index.ts b/app/src/interfaces/select-multiple-dropdown/index.ts index 4faa019b7c..e9baff9883 100644 --- a/app/src/interfaces/select-multiple-dropdown/index.ts +++ b/app/src/interfaces/select-multiple-dropdown/index.ts @@ -29,7 +29,7 @@ export default defineInterface({ name: '$t:text', meta: { width: 'half', - interface: 'input', + interface: 'system-input-translated-string', options: { placeholder: '$t:interfaces.select-dropdown.choices_name_placeholder', }, @@ -88,7 +88,7 @@ export default defineInterface({ type: 'string', meta: { width: 'half', - interface: 'input', + interface: 'system-input-translated-string', options: { placeholder: '$t:enter_a_placeholder', }, diff --git a/app/src/interfaces/select-radio/index.ts b/app/src/interfaces/select-radio/index.ts index 78913808d2..8de1ff35b0 100644 --- a/app/src/interfaces/select-radio/index.ts +++ b/app/src/interfaces/select-radio/index.ts @@ -29,7 +29,7 @@ export default defineInterface({ name: '$t:text', meta: { width: 'half', - interface: 'input', + interface: 'system-input-translated-string', }, }, { diff --git a/app/src/interfaces/tags/index.ts b/app/src/interfaces/tags/index.ts index c524826da7..234b4ccc91 100644 --- a/app/src/interfaces/tags/index.ts +++ b/app/src/interfaces/tags/index.ts @@ -26,7 +26,7 @@ export default defineInterface({ type: 'string', meta: { width: 'full', - interface: 'input', + interface: 'system-input-translated-string', options: { placeholder: '$t:enter_a_placeholder', }, diff --git a/app/src/lang/set-language.ts b/app/src/lang/set-language.ts index 69891f344b..ebcaa4c4f9 100644 --- a/app/src/lang/set-language.ts +++ b/app/src/lang/set-language.ts @@ -7,6 +7,7 @@ import { useCollectionsStore, useFieldsStore } from '@/stores'; import { translate } from '@/utils/translate-object-values'; import availableLanguages from './available-languages.yaml'; import { i18n, Language, loadedLanguages } from './index'; +import { useTranslationStrings } from '@/composables/use-translation-strings'; const { modules, modulesRaw } = getModules(); const { layouts, layoutsRaw } = getLayouts(); @@ -17,6 +18,7 @@ const { displays, displaysRaw } = getDisplays(); export async function setLanguage(lang: Language): Promise { const collectionsStore = useCollectionsStore(); const fieldsStore = useFieldsStore(); + const { mergeTranslationStringsForLanguage } = useTranslationStrings(); if (Object.keys(availableLanguages).includes(lang) === false) { // eslint-disable-next-line no-console @@ -46,6 +48,7 @@ export async function setLanguage(lang: Language): Promise { collectionsStore.translateCollections(); fieldsStore.translateFields(); + mergeTranslationStringsForLanguage(lang); return true; } diff --git a/app/src/lang/translations/en-US.yaml b/app/src/lang/translations/en-US.yaml index 95fbe935c8..8197395ac7 100644 --- a/app/src/lang/translations/en-US.yaml +++ b/app/src/lang/translations/en-US.yaml @@ -70,6 +70,12 @@ create_role: Create Role create_user: Create User delete_panel: Delete Panel create_webhook: Create Webhook +create_translation_string: Create Translation String +translation_string_key_placeholder: Translation string key... +translation_string_translations_placeholder: Add a new translation +edit_translation_string: Edit Translation String +delete_translation_string_copy: >- + Are you sure you want to delete the translation string "{key}"? This action can not be undone. invite_users: Invite Users invite: Invite email_already_invited: Email "{email}" has already been invited @@ -180,6 +186,7 @@ field_create_success: 'Created Field: "{field}"' field_update_success: 'Updated Field: "{field}"' duplicate_where_to: Where would you like to duplicate this field to? language: Language +language_placeholder: Select a language... aggregate_function: Aggregate Function aggregate_precision: Aggregate Precision group_aggregation: Group Aggregation @@ -828,6 +835,7 @@ settings_permissions: Roles & Permissions settings_project: Project Settings settings_webhooks: Webhooks settings_presets: Presets & Bookmarks +settings_translation_strings: Translation Strings one_or_more_options_are_missing: One or more options are missing scope: Scope select: Select... @@ -878,6 +886,7 @@ page_help_settings_presets_item: >- **Preset Detail** — A form for managing bookmarks and default collection presets. page_help_settings_webhooks_collection: '**Browse Webhooks** — Lists all webhooks within the project.' page_help_settings_webhooks_item: '**Webhook Detail** — A form for creating and managing project webhooks.' +page_help_settings_translation_strings_collection: '**Browse Translation Strings** — Lists all translation strings within the project.' page_help_users_collection: '**User Directory** — Lists all system users within this project.' page_help_users_item: >- **User Detail** — Manage your account information, or view the details of other users. @@ -1534,6 +1543,11 @@ interfaces: input-multiline: textarea: Textarea description: Enter multiline plain-text + input-translated-string: + input-translated-string: Translated Strings + description: Select or add translation string + search_placeholder: Search... + new_translation_string: New Translation String boolean: toggle: Toggle description: Switch between on and off diff --git a/app/src/modules/settings/components/navigation.vue b/app/src/modules/settings/components/navigation.vue index 142c1953c7..2b1ca6fb4f 100644 --- a/app/src/modules/settings/components/navigation.vue +++ b/app/src/modules/settings/components/navigation.vue @@ -58,6 +58,11 @@ export default defineComponent({ name: t('settings_presets'), to: `/settings/presets`, }, + { + icon: 'translate', + name: t('settings_translation_strings'), + to: `/settings/translation-strings`, + }, { icon: 'anchor', name: t('settings_webhooks'), diff --git a/app/src/modules/settings/index.ts b/app/src/modules/settings/index.ts index 77f775b363..6902515ecf 100644 --- a/app/src/modules/settings/index.ts +++ b/app/src/modules/settings/index.ts @@ -17,6 +17,7 @@ import RolesPermissionsDetail from './routes/roles/permissions-detail/permission import RolesPublicItem from './routes/roles/public-item.vue'; import WebhooksCollection from './routes/webhooks/collection.vue'; import WebhooksItem from './routes/webhooks/item.vue'; +import TranslationStringsCollection from './routes/translation-strings/collection.vue'; export default defineModule({ id: 'settings', @@ -175,6 +176,17 @@ export default defineModule({ }, ], }, + { + path: 'translation-strings', + component: RouterPass, + children: [ + { + name: 'settings-translation-strings-collection', + path: '', + component: TranslationStringsCollection, + }, + ], + }, { name: 'settings-not-found', path: ':_(.+)+', diff --git a/app/src/modules/settings/routes/data-model/field-detail/field-detail-advanced/field-detail-advanced-field.vue b/app/src/modules/settings/routes/data-model/field-detail/field-detail-advanced/field-detail-advanced-field.vue index 06bfcaaaf9..e0442f901a 100644 --- a/app/src/modules/settings/routes/data-model/field-detail/field-detail-advanced/field-detail-advanced-field.vue +++ b/app/src/modules/settings/routes/data-model/field-detail/field-detail-advanced/field-detail-advanced-field.vue @@ -17,7 +17,7 @@
{{ t('note') }}
- +
@@ -72,20 +72,15 @@ import { storeToRefs } from 'pinia'; export default defineComponent({ setup() { const { t } = useI18n(); - const fieldDetailStore = useFieldDetailStore(); - const readonly = syncFieldDetailStoreProperty('field.meta.readonly', false); const hidden = syncFieldDetailStoreProperty('field.meta.hidden', false); const required = syncFieldDetailStoreProperty('field.meta.required', false); const note = syncFieldDetailStoreProperty('field.meta.note'); const translations = syncFieldDetailStoreProperty('field.meta.translations'); - const { field } = storeToRefs(fieldDetailStore); - const type = computed(() => field.value.type); const isGenerated = computed(() => field.value.schema?.is_generated); - return { t, readonly, hidden, required, note, translations, type, isGenerated }; }, }); diff --git a/app/src/modules/settings/routes/translation-strings/collection.vue b/app/src/modules/settings/routes/translation-strings/collection.vue new file mode 100644 index 0000000000..449f6a8638 --- /dev/null +++ b/app/src/modules/settings/routes/translation-strings/collection.vue @@ -0,0 +1,136 @@ + + + + + diff --git a/app/src/modules/settings/routes/translation-strings/translation-strings-drawer.vue b/app/src/modules/settings/routes/translation-strings/translation-strings-drawer.vue new file mode 100644 index 0000000000..cb4cca90ef --- /dev/null +++ b/app/src/modules/settings/routes/translation-strings/translation-strings-drawer.vue @@ -0,0 +1,214 @@ + + + + + diff --git a/app/src/modules/settings/routes/translation-strings/translation-strings-tooltip.vue b/app/src/modules/settings/routes/translation-strings/translation-strings-tooltip.vue new file mode 100644 index 0000000000..e02589e1bb --- /dev/null +++ b/app/src/modules/settings/routes/translation-strings/translation-strings-tooltip.vue @@ -0,0 +1,136 @@ + + + + + diff --git a/packages/shared/src/types/settings.ts b/packages/shared/src/types/settings.ts index 5c175ac137..34bb7358f2 100644 --- a/packages/shared/src/types/settings.ts +++ b/packages/shared/src/types/settings.ts @@ -44,4 +44,5 @@ export type Settings = { basemaps: any[] | null; mapbox_key: string | null; module_bar: (SettingsModuleBarLink | SettingsModuleBarModule)[]; + translation_strings: Record[]; };