From 62406bdc844a4c984e2e143855ecf154c19ecd0a Mon Sep 17 00:00:00 2001 From: xiaomai Date: Fri, 1 May 2026 14:07:07 +0800 Subject: [PATCH] fix(i18n): prevent base name overwrites when editing in localized UI Include base names in API responses to correctly populate edit forms. Show base values as placeholders in translation fields for better UX. Use default locale when fetching previous state for history diffs. --- backend/src/queries.ts | 16 ++++++++++------ frontend/src/components/TranslationFields.vue | 5 +++++ frontend/src/services/api.ts | 4 ++++ frontend/src/views/AdminView.vue | 9 +++++---- frontend/src/views/HabitatEdit.vue | 2 +- frontend/src/views/ItemEdit.vue | 2 +- frontend/src/views/PokemonEdit.vue | 2 +- 7 files changed, 27 insertions(+), 13 deletions(-) diff --git a/backend/src/queries.ts b/backend/src/queries.ts index 675196f..6dd6542 100644 --- a/backend/src/queries.ts +++ b/backend/src/queries.ts @@ -861,6 +861,7 @@ function pokemonProjection(locale: string): string { SELECT p.id, ${pokemonName} AS name, + p.name AS "baseName", ${translationsSelect('pokemon', 'p.id')} AS translations, ${auditSelect('p', 'pokemon_created_user', 'pokemon_updated_user')}, json_build_object('id', e.id, 'name', ${environmentName}) AS environment, @@ -924,7 +925,7 @@ export async function listDailyChecklistItems(locale = defaultLocale) { const title = localizedField('daily-checklist-items', 'c.id', 'c.title', 'title', locale); return query( ` - SELECT c.id, ${title} AS title, ${translationsSelect('daily-checklist-items', 'c.id')} AS translations + SELECT c.id, ${title} AS title, c.title AS "baseTitle", ${translationsSelect('daily-checklist-items', 'c.id')} AS translations FROM daily_checklist_items c ORDER BY c.sort_order, c.id ` @@ -935,7 +936,7 @@ async function getDailyChecklistItemById(id: number, locale = defaultLocale) { const title = localizedField('daily-checklist-items', 'c.id', 'c.title', 'title', locale); return queryOne( ` - SELECT c.id, ${title} AS title, ${translationsSelect('daily-checklist-items', 'c.id')} AS translations + SELECT c.id, ${title} AS title, c.title AS "baseTitle", ${translationsSelect('daily-checklist-items', 'c.id')} AS translations FROM daily_checklist_items c WHERE c.id = $1 `, @@ -1442,7 +1443,7 @@ export async function createPokemon(payload: Record, userId: nu export async function updatePokemon(id: number, payload: Record, userId: number, locale = defaultLocale) { const cleanPayload = cleanPokemonPayload({ ...payload, id }); - const before = await getPokemon(id, locale); + const before = await getPokemon(id, defaultLocale); const updated = await withTransaction(async (client) => { const result = await client.query( @@ -1487,6 +1488,7 @@ export async function listHabitats(locale = defaultLocale) { SELECT h.id, ${habitatName} AS name, + h.name AS "baseName", ${translationsSelect('habitats', 'h.id')} AS translations, ${auditSelect('h', 'habitat_created_user', 'habitat_updated_user')}, COALESCE(( @@ -1521,6 +1523,7 @@ export async function getHabitat(id: number, locale = defaultLocale) { SELECT h.id, ${habitatName} AS name, + h.name AS "baseName", ${translationsSelect('habitats', 'h.id')} AS translations, ${auditSelect('h', 'habitat_created_user', 'habitat_updated_user')}, COALESCE(( @@ -1650,7 +1653,7 @@ export async function createHabitat(payload: Record, userId: nu export async function updateHabitat(id: number, payload: Record, userId: number, locale = defaultLocale) { const cleanPayload = cleanHabitatPayload(payload); - const before = await getHabitat(id, locale); + const before = await getHabitat(id, defaultLocale); const updated = await withTransaction(async (client) => { const result = await client.query( @@ -1692,6 +1695,7 @@ function itemProjection(locale: string): string { SELECT i.id, ${itemName} AS name, + i.name AS "baseName", ${translationsSelect('items', 'i.id')} AS translations, ${auditSelect('i', 'item_created_user', 'item_updated_user')}, json_build_object('id', c.id, 'name', ${categoryName}) AS category, @@ -1980,7 +1984,7 @@ export async function createItem(payload: Record, userId: numbe export async function updateItem(id: number, payload: Record, userId: number, locale = defaultLocale) { const cleanPayload = cleanItemPayload(payload); - const before = await getItem(id, locale); + const before = await getItem(id, defaultLocale); const updated = await withTransaction(async (client) => { await ensureItemCanDisableRecipe(client, id, cleanPayload.noRecipe); @@ -2170,7 +2174,7 @@ export async function createRecipe(payload: Record, userId: num export async function updateRecipe(id: number, payload: Record, userId: number, locale = defaultLocale) { const cleanPayload = cleanRecipePayload(payload); - const before = await getRecipe(id, locale); + const before = await getRecipe(id, defaultLocale); const updated = await withTransaction(async (client) => { await ensureItemCanHaveRecipe(client, cleanPayload.itemId); diff --git a/frontend/src/components/TranslationFields.vue b/frontend/src/components/TranslationFields.vue index 9f7edc1..449df90 100644 --- a/frontend/src/components/TranslationFields.vue +++ b/frontend/src/components/TranslationFields.vue @@ -30,6 +30,10 @@ function fieldValue(language: Language): string { return props.translations[language.code]?.[props.field] ?? ''; } +function fieldPlaceholder(language: Language): string { + return language.code === defaultLanguage.value?.code ? '' : props.baseValue; +} + function updateField(language: Language, value: string) { if (language.code === defaultLanguage.value?.code) { emit('update:baseValue', value); @@ -68,6 +72,7 @@ function inputValue(event: Event): string { diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index 9d99d27..1501068 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -56,6 +56,7 @@ export interface EditHistoryEntry { export interface Pokemon extends EditInfo { id: number; name: string; + baseName?: string; translations?: TranslationMap; environment: NamedEntity; skills: Skill[]; @@ -79,6 +80,7 @@ export interface PokemonDetail extends Pokemon { export interface Habitat extends EditInfo { id: number; name: string; + baseName?: string; translations?: TranslationMap; recipe: Array; pokemon?: NamedEntity[]; @@ -113,6 +115,7 @@ export interface HabitatUsage { export interface Item extends EditInfo { id: number; name: string; + baseName?: string; translations?: TranslationMap; category: NamedEntity; usage: NamedEntity | null; @@ -147,6 +150,7 @@ export interface Recipe extends EditInfo { export interface DailyChecklistItem { id: number; title: string; + baseTitle?: string; translations?: TranslationMap; } diff --git a/frontend/src/views/AdminView.vue b/frontend/src/views/AdminView.vue index e31f003..e20b522 100644 --- a/frontend/src/views/AdminView.vue +++ b/frontend/src/views/AdminView.vue @@ -81,7 +81,7 @@ const configNameInput = computed({ return configForm.value.name; } - return configForm.value.translations[currentConfigLocale.value]?.name ?? configForm.value.name; + return configForm.value.translations[currentConfigLocale.value]?.name ?? ''; }, set: (value: string) => { if (isConfigDefaultLocale.value) { @@ -92,6 +92,7 @@ const configNameInput = computed({ updateConfigTranslation(currentConfigLocale.value, value); } }); +const configNamePlaceholder = computed(() => (isConfigDefaultLocale.value ? '' : configForm.value.name)); const activeConfigTab = computed({ get: () => activeConfigType.value, set: (value: string) => { @@ -194,7 +195,7 @@ function closeChecklistModal() { } function editChecklistItem(item: DailyChecklistItem) { - checklistForm.value = { id: item.id, title: item.title, translations: item.translations ?? {} }; + checklistForm.value = { id: item.id, title: item.baseTitle ?? item.title, translations: item.translations ?? {} }; checklistModalOpen.value = true; } @@ -240,7 +241,7 @@ function updateConfigTranslation(localeCode: string, value: string) { } function configBaseNameForSave() { - if (configForm.value.name.trim() !== '' || isConfigDefaultLocale.value) { + if (configForm.value.name.trim() !== '') { return configForm.value.name; } @@ -805,7 +806,7 @@ onMounted(() => {