feat(i18n): display only active language in translation fields

Update TranslationFields to render a single input for the current locale
Ensure entity base names fallback to the active locale translation on save
This commit is contained in:
2026-05-01 14:11:31 +08:00
parent 62406bdc84
commit ca3ca35dfc
5 changed files with 63 additions and 20 deletions

View File

@@ -18,9 +18,21 @@ const emit = defineEmits<{
'update:translations': [value: TranslationMap]; 'update:translations': [value: TranslationMap];
}>(); }>();
const { t } = useI18n(); const { locale, t } = useI18n();
const fallbackLanguage: Language = { code: 'en', name: 'English', enabled: true, isDefault: true, sortOrder: 0 };
const visibleLanguages = computed(() => props.languages.filter((language) => language.enabled)); const visibleLanguages = computed(() => props.languages.filter((language) => language.enabled));
const defaultLanguage = computed(() => visibleLanguages.value.find((language) => language.isDefault) ?? visibleLanguages.value[0]); const defaultLanguage = computed(() => visibleLanguages.value.find((language) => language.isDefault) ?? visibleLanguages.value[0] ?? fallbackLanguage);
const currentLanguage = computed(() => {
const currentLocale = String(locale.value || defaultLanguage.value.code);
return visibleLanguages.value.find((language) => language.code === currentLocale) ?? defaultLanguage.value;
});
const isDefaultLanguage = computed(() => currentLanguage.value.code === defaultLanguage.value.code);
const currentValue = computed({
get: () => fieldValue(currentLanguage.value),
set: (value: string) => updateField(currentLanguage.value, value)
});
const currentPlaceholder = computed(() => fieldPlaceholder(currentLanguage.value));
const currentRequired = computed(() => Boolean(props.required && (isDefaultLanguage.value || props.baseValue.trim() === '')));
function fieldValue(language: Language): string { function fieldValue(language: Language): string {
if (language.code === defaultLanguage.value?.code) { if (language.code === defaultLanguage.value?.code) {
@@ -58,23 +70,19 @@ function updateField(language: Language, value: string) {
emit('update:translations', nextTranslations); emit('update:translations', nextTranslations);
} }
function inputValue(event: Event): string {
return event.target instanceof HTMLInputElement ? event.target.value : '';
}
</script> </script>
<template> <template>
<div class="translation-fields"> <div class="translation-fields">
<div v-for="language in visibleLanguages" :key="language.code" class="field"> <div class="field">
<label :for="`${idPrefix}-${language.code}`"> <label :for="`${idPrefix}-${currentLanguage.code}`">
{{ t('common.fieldForLanguage', { field: label, language: language.name }) }} {{ t('common.fieldForLanguage', { field: label, language: currentLanguage.name }) }}
</label> </label>
<input <input
:id="`${idPrefix}-${language.code}`" :id="`${idPrefix}-${currentLanguage.code}`"
:value="fieldValue(language)" v-model="currentValue"
:placeholder="fieldPlaceholder(language)" :placeholder="currentPlaceholder"
:required="required && language.code === defaultLanguage?.code" :required="currentRequired"
@input="updateField(language, inputValue($event))"
/> />
</div> </div>
</div> </div>

View File

@@ -248,6 +248,14 @@ function configBaseNameForSave() {
return configForm.value.translations[currentConfigLocale.value]?.name ?? ''; return configForm.value.translations[currentConfigLocale.value]?.name ?? '';
} }
function checklistTitleForSave() {
if (checklistForm.value.title.trim() !== '') {
return checklistForm.value.title;
}
return checklistForm.value.translations[currentConfigLocale.value]?.title ?? '';
}
function previewChecklistOrder(rows: DailyChecklistItem[]) { function previewChecklistOrder(rows: DailyChecklistItem[]) {
checklistRows.value = rows; checklistRows.value = rows;
} }
@@ -391,7 +399,7 @@ async function loadChecklist() {
async function saveChecklistItem() { async function saveChecklistItem() {
await run(async () => { await run(async () => {
const payload = { const payload = {
title: checklistForm.value.title, title: checklistTitleForSave(),
translations: checklistForm.value.translations translations: checklistForm.value.translations
}; };

View File

@@ -30,7 +30,7 @@ type HabitatAppearanceForm = {
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const { t } = useI18n(); const { locale, t } = useI18n();
const options = ref<Options | null>(null); const options = ref<Options | null>(null);
const itemRows = ref<Item[]>([]); const itemRows = ref<Item[]>([]);
const pokemonRows = ref<Pokemon[]>([]); const pokemonRows = ref<Pokemon[]>([]);
@@ -127,6 +127,15 @@ function closeEditor() {
void router.push(cancelTo.value); void router.push(cancelTo.value);
} }
function habitatNameForSave() {
const baseName = habitatForm.value.name.trim();
if (baseName !== '') {
return habitatForm.value.name;
}
return habitatForm.value.translations[String(locale.value || '')]?.name ?? '';
}
async function loadEditor() { async function loadEditor() {
loading.value = true; loading.value = true;
message.value = ''; message.value = '';
@@ -189,7 +198,7 @@ async function saveHabitat() {
try { try {
const payload: HabitatPayload = { const payload: HabitatPayload = {
name: habitatForm.value.name, name: habitatNameForSave(),
translations: habitatForm.value.translations, translations: habitatForm.value.translations,
recipeItems: toQuantityRows(habitatForm.value.recipeItems), recipeItems: toQuantityRows(habitatForm.value.recipeItems),
pokemonAppearances: habitatForm.value.pokemonAppearances pokemonAppearances: habitatForm.value.pokemonAppearances

View File

@@ -11,7 +11,7 @@ import { api, type ConfigType, type ItemPayload, type Language, type Options, ty
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const { t } = useI18n(); const { locale, t } = useI18n();
const options = ref<Options | null>(null); const options = ref<Options | null>(null);
const languages = ref<Language[]>([]); const languages = ref<Language[]>([]);
const loading = ref(true); const loading = ref(true);
@@ -53,6 +53,15 @@ function closeEditor() {
void router.push(cancelTo.value); void router.push(cancelTo.value);
} }
function itemNameForSave() {
const baseName = itemForm.value.name.trim();
if (baseName !== '') {
return itemForm.value.name;
}
return itemForm.value.translations[String(locale.value || '')]?.name ?? '';
}
async function loadOptions() { async function loadOptions() {
const [loadedOptions, loadedLanguages] = await Promise.all([api.options(), api.languages()]); const [loadedOptions, loadedLanguages] = await Promise.all([api.options(), api.languages()]);
options.value = loadedOptions; options.value = loadedOptions;
@@ -131,7 +140,7 @@ async function saveItem() {
try { try {
const payload: ItemPayload = { const payload: ItemPayload = {
name: itemForm.value.name, name: itemNameForSave(),
translations: itemForm.value.translations, translations: itemForm.value.translations,
categoryId: Number(itemForm.value.categoryId), categoryId: Number(itemForm.value.categoryId),
usageId: itemForm.value.usageId ? Number(itemForm.value.usageId) : null, usageId: itemForm.value.usageId ? Number(itemForm.value.usageId) : null,

View File

@@ -16,7 +16,7 @@ type SkillItemDropForm = {
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const { t } = useI18n(); const { locale, t } = useI18n();
const options = ref<Options | null>(null); const options = ref<Options | null>(null);
const itemOptions = ref<NamedEntity[]>([]); const itemOptions = ref<NamedEntity[]>([]);
const languages = ref<Language[]>([]); const languages = ref<Language[]>([]);
@@ -87,6 +87,15 @@ function skillDropLabel(skillId: string) {
return name ? t('pages.pokemon.skillDrop', { name }) : t('pages.pokemon.dropItem'); return name ? t('pages.pokemon.skillDrop', { name }) : t('pages.pokemon.dropItem');
} }
function pokemonNameForSave() {
const baseName = pokemonForm.value.name.trim();
if (baseName !== '') {
return pokemonForm.value.name;
}
return pokemonForm.value.translations[String(locale.value || '')]?.name ?? '';
}
function closeEditor() { function closeEditor() {
void router.push(cancelTo.value); void router.push(cancelTo.value);
} }
@@ -164,7 +173,7 @@ async function savePokemon() {
try { try {
const payload: PokemonPayload = { const payload: PokemonPayload = {
id: Number(isEditing.value ? routeId.value : pokemonForm.value.id), id: Number(isEditing.value ? routeId.value : pokemonForm.value.id),
name: pokemonForm.value.name, name: pokemonNameForSave(),
translations: pokemonForm.value.translations, translations: pokemonForm.value.translations,
environmentId: Number(pokemonForm.value.environmentId), environmentId: Number(pokemonForm.value.environmentId),
skillIds: toIds(pokemonForm.value.skillIds.slice(0, 2)), skillIds: toIds(pokemonForm.value.skillIds.slice(0, 2)),