mirror of
https://github.com/directus/directus.git
synced 2026-02-16 14:05:17 -05:00
* 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 <rijkvanzanten@me.com>
262 lines
6.4 KiB
Vue
262 lines
6.4 KiB
Vue
<template>
|
|
<div class="input-translated-string">
|
|
<v-menu ref="menuEl" :disabled="disabled" :close-on-content-click="false" attached>
|
|
<template #activator="{ toggle, active }">
|
|
<v-input
|
|
class="translation-input"
|
|
:model-value="localValue"
|
|
:placeholder="placeholder"
|
|
:disabled="disabled"
|
|
:active="active"
|
|
@update:model-value="localValue = $event"
|
|
>
|
|
<template v-if="hasValidKey" #input>
|
|
<button :disabled="disabled" @click.stop="setValue(null)">{{ value && getKeyWithoutPrefix(value) }}</button>
|
|
</template>
|
|
<template #append>
|
|
<v-icon
|
|
name="translate"
|
|
class="translate-icon"
|
|
:class="{ active }"
|
|
clickable
|
|
:disabled="disabled"
|
|
@click="toggle"
|
|
/>
|
|
</template>
|
|
</v-input>
|
|
</template>
|
|
|
|
<div v-if="searchValue !== null || translations.length >= 25" class="search">
|
|
<v-input
|
|
class="search-input"
|
|
type="text"
|
|
:model-value="searchValue"
|
|
autofocus
|
|
:placeholder="t('interfaces.input-translated-string.search_placeholder')"
|
|
@update:model-value="searchValue = $event"
|
|
>
|
|
<template #append>
|
|
<v-icon name="search" class="search-icon" />
|
|
</template>
|
|
</v-input>
|
|
</div>
|
|
|
|
<v-list>
|
|
<v-list-item
|
|
v-for="translation in translations"
|
|
:key="translation.key"
|
|
class="translation-key"
|
|
:class="{ selected: localValue && translation.key === localValueWithoutPrefix }"
|
|
clickable
|
|
@click="selectKey(translation.key!)"
|
|
>
|
|
<v-list-item-icon>
|
|
<v-icon name="translate" />
|
|
</v-list-item-icon>
|
|
<v-list-item-content><v-highlight :text="translation.key" :query="searchValue" /></v-list-item-content>
|
|
<v-list-item-icon class="info">
|
|
<TranslationStringsTooltip :translations="translation.translations" hide-display-text />
|
|
</v-list-item-icon>
|
|
</v-list-item>
|
|
<v-list-item class="new-translation-string" clickable @click="openNewTranslationStringDialog">
|
|
<v-list-item-icon>
|
|
<v-icon name="add" />
|
|
</v-list-item-icon>
|
|
<v-list-item-content>
|
|
{{ t('interfaces.input-translated-string.new_translation_string') }}
|
|
</v-list-item-content>
|
|
</v-list-item>
|
|
</v-list>
|
|
</v-menu>
|
|
|
|
<TranslationStringsDrawer
|
|
:model-value="isTranslationStringDialogOpen"
|
|
:translation-string="editingTranslationString"
|
|
@update:model-value="updateTranslationStringsDialog"
|
|
@saved-key="setValue(`${translationPrefix}${$event}`)"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<script lang="ts" setup>
|
|
import { computed, ref, watch } from 'vue';
|
|
import { useI18n } from 'vue-i18n';
|
|
import { useTranslationStrings, TranslationString } from '@/composables/use-translation-strings';
|
|
import TranslationStringsDrawer from '@/modules/settings/routes/translation-strings/translation-strings-drawer.vue';
|
|
import TranslationStringsTooltip from '@/modules/settings/routes/translation-strings/translation-strings-tooltip.vue';
|
|
|
|
const translationPrefix = '$t:';
|
|
|
|
interface Props {
|
|
value?: string | null;
|
|
disabled?: boolean;
|
|
placeholder?: string | null;
|
|
}
|
|
|
|
const props = withDefaults(defineProps<Props>(), { value: () => null, disabled: false, placeholder: () => null });
|
|
|
|
const emit = defineEmits(['input']);
|
|
|
|
const { t } = useI18n();
|
|
|
|
const menuEl = ref();
|
|
const hasValidKey = ref<boolean>(false);
|
|
const searchValue = ref<string | null>(null);
|
|
|
|
const { translationStrings } = useTranslationStrings();
|
|
|
|
const isTranslationStringDialogOpen = ref<boolean>(false);
|
|
|
|
const editingTranslationString = ref<TranslationString | null>(null);
|
|
|
|
const translations = computed(() => {
|
|
const keys = translationStrings.value ?? [];
|
|
|
|
return !searchValue.value ? keys : keys.filter((key) => key.key?.includes(searchValue.value!));
|
|
});
|
|
|
|
const localValue = computed<string | null>({
|
|
get() {
|
|
return props.value;
|
|
},
|
|
set(val) {
|
|
if (props.value === val) return;
|
|
emit('input', val);
|
|
},
|
|
});
|
|
|
|
watch(
|
|
() => props.value,
|
|
(newVal) => setValue(newVal),
|
|
{ immediate: true }
|
|
);
|
|
|
|
const localValueWithoutPrefix = computed(() => (localValue.value ? getKeyWithoutPrefix(localValue.value) : null));
|
|
|
|
function getKeyWithoutPrefix(val: string) {
|
|
return val.substring(translationPrefix.length);
|
|
}
|
|
|
|
function selectKey(key: string) {
|
|
setValue(`${translationPrefix}${key}`);
|
|
menuEl.value.deactivate();
|
|
searchValue.value = null;
|
|
}
|
|
|
|
function setValue(newValue: any) {
|
|
hasValidKey.value = false;
|
|
|
|
if (
|
|
newValue &&
|
|
newValue.startsWith(translationPrefix) &&
|
|
translations.value.find((t) => t.key?.includes(getKeyWithoutPrefix(newValue)))
|
|
) {
|
|
hasValidKey.value = true;
|
|
}
|
|
|
|
localValue.value = newValue;
|
|
}
|
|
|
|
function openNewTranslationStringDialog() {
|
|
menuEl.value.deactivate();
|
|
isTranslationStringDialogOpen.value = true;
|
|
}
|
|
|
|
function updateTranslationStringsDialog(val: boolean) {
|
|
if (val) return;
|
|
|
|
editingTranslationString.value = null;
|
|
isTranslationStringDialogOpen.value = val;
|
|
}
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.translation-input {
|
|
:deep(button) {
|
|
margin-right: auto;
|
|
padding: 2px 8px 0;
|
|
color: var(--primary);
|
|
background-color: var(--primary-alt);
|
|
border-radius: var(--border-radius);
|
|
transition: var(--fast) var(--transition);
|
|
transition-property: background-color, color;
|
|
user-select: none;
|
|
font-family: var(--family-monospace);
|
|
}
|
|
|
|
:deep(button:not(:disabled):hover) {
|
|
color: var(--white);
|
|
background-color: var(--danger);
|
|
}
|
|
|
|
.translate-icon {
|
|
&:hover,
|
|
&.active {
|
|
--v-icon-color-hover: var(--primary);
|
|
--v-icon-color: var(--primary);
|
|
}
|
|
}
|
|
}
|
|
|
|
.search {
|
|
padding: 12px 8px 6px 8px;
|
|
|
|
.search-input {
|
|
--input-height: 48px;
|
|
}
|
|
|
|
.search-icon {
|
|
pointer-events: none;
|
|
}
|
|
}
|
|
|
|
.translation-key {
|
|
transition: color var(--fast) var(--transition);
|
|
|
|
.info :deep(.icon) {
|
|
transition: opacity var(--fast) var(--transition);
|
|
opacity: 0;
|
|
}
|
|
|
|
&:hover .info :deep(.icon) {
|
|
opacity: 1;
|
|
}
|
|
|
|
:deep(mark) {
|
|
flex-basis: auto;
|
|
flex-grow: 0;
|
|
flex-shrink: 1;
|
|
color: var(--primary);
|
|
}
|
|
|
|
&.selected {
|
|
--v-list-item-color-active: var(--foreground-inverted);
|
|
--v-list-item-background-color-active: var(--primary);
|
|
--v-list-item-color-hover: var(--foreground-inverted);
|
|
--v-list-item-background-color-hover: var(--primary);
|
|
|
|
background-color: var(--primary);
|
|
color: var(--foreground-inverted);
|
|
|
|
.v-list-item-icon {
|
|
--v-icon-color: var(--foreground-inverted);
|
|
}
|
|
|
|
.info :deep(.icon) {
|
|
color: var(--foreground-inverted);
|
|
opacity: 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
.new-translation-string {
|
|
--v-list-item-color-hover: var(--primary-125);
|
|
|
|
color: var(--primary);
|
|
|
|
.v-list-item-icon {
|
|
--v-icon-color: var(--primary);
|
|
}
|
|
}
|
|
</style>
|