i18n: default-language picker offers only shipped locales; widen stored bare codes on read
This commit is contained in:
@@ -3,6 +3,7 @@ import { X } from 'lucide-react';
|
|||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { useSettings } from 'state/useSettings';
|
import { useSettings } from 'state/useSettings';
|
||||||
import { useTranslation } from '@/lib/i18n';
|
import { useTranslation } from '@/lib/i18n';
|
||||||
|
import { SHIPPED_LOCALES } from '@/lib/locales';
|
||||||
|
|
||||||
const LANGUAGES = [
|
const LANGUAGES = [
|
||||||
{ code: 'en', label: 'English' },
|
{ code: 'en', label: 'English' },
|
||||||
@@ -56,7 +57,8 @@ export const Languages = () => {
|
|||||||
const removeSpoken = (code: string) => {
|
const removeSpoken = (code: string) => {
|
||||||
const next = spoken.filter((s) => s !== code);
|
const next = spoken.filter((s) => s !== code);
|
||||||
const updates = { ...settings.languages, spoken: next };
|
const updates = { ...settings.languages, spoken: next };
|
||||||
if (defaultLang === code) updates.default = next[0] ?? 'en';
|
// `default` (UI localization) draws from SHIPPED_LOCALES, not `spoken` — removing a spoken
|
||||||
|
// language never leaves it without a valid value.
|
||||||
if (translateTo === code) updates.translateTo = next[0] ?? 'en';
|
if (translateTo === code) updates.translateTo = next[0] ?? 'en';
|
||||||
save(updates);
|
save(updates);
|
||||||
};
|
};
|
||||||
@@ -104,7 +106,7 @@ export const Languages = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Default language */}
|
{/* Default language — UI localization, so it only offers locales that actually ship (D2) */}
|
||||||
<Label className="grid gap-2">
|
<Label className="grid gap-2">
|
||||||
<span className="text-duck-dark/70">{t('settings.profile.languages.defaultLabel')}</span>
|
<span className="text-duck-dark/70">{t('settings.profile.languages.defaultLabel')}</span>
|
||||||
<select
|
<select
|
||||||
@@ -112,9 +114,9 @@ export const Languages = () => {
|
|||||||
onChange={(ev) => save({ ...settings.languages, default: ev.target.value })}
|
onChange={(ev) => save({ ...settings.languages, default: ev.target.value })}
|
||||||
className="text-sm border border-duck-dark/20 rounded-md px-3 py-2 bg-background/60 text-duck-dark cursor-pointer"
|
className="text-sm border border-duck-dark/20 rounded-md px-3 py-2 bg-background/60 text-duck-dark cursor-pointer"
|
||||||
>
|
>
|
||||||
{spoken.map((code) => (
|
{SHIPPED_LOCALES.map(({ code, label }) => (
|
||||||
<option key={code} value={code}>
|
<option key={code} value={code}>
|
||||||
{getLabel(code)}
|
{label}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
|||||||
@@ -1,68 +0,0 @@
|
|||||||
{
|
|
||||||
"common": {
|
|
||||||
"save": "Save",
|
|
||||||
"saving": "Saving...",
|
|
||||||
"cancel": "Cancel"
|
|
||||||
},
|
|
||||||
"settings": {
|
|
||||||
"profile": {
|
|
||||||
"searchPlaceholder": "Search settings...",
|
|
||||||
"sections": {
|
|
||||||
"profileTitle": "Profile",
|
|
||||||
"profileDescription": "Update your name and avatar.",
|
|
||||||
"changePasswordTitle": "Change Password",
|
|
||||||
"changePasswordDescription": "Update your account password.",
|
|
||||||
"tasksTitle": "Tasks",
|
|
||||||
"tasksDescription": "Default model for file browser tasks.",
|
|
||||||
"languagesTitle": "Languages",
|
|
||||||
"languagesDescription": "Set your spoken languages and translation preferences."
|
|
||||||
},
|
|
||||||
"userData": {
|
|
||||||
"emailLabel": "Email",
|
|
||||||
"nameLabel": "Name",
|
|
||||||
"namePlaceholder": "Your name",
|
|
||||||
"profileUpdated": "Profile updated",
|
|
||||||
"profileUpdateFailed": "Failed to update profile",
|
|
||||||
"selectImageFile": "Please select an image file",
|
|
||||||
"imageTooLarge": "Image must be smaller than 384KB"
|
|
||||||
},
|
|
||||||
"changePassword": {
|
|
||||||
"currentPasswordLabel": "Current Password",
|
|
||||||
"currentPasswordPlaceholder": "Current password",
|
|
||||||
"newPasswordLabel": "New Password",
|
|
||||||
"newPasswordPlaceholder": "New password",
|
|
||||||
"confirmPasswordLabel": "Confirm New Password",
|
|
||||||
"confirmPasswordPlaceholder": "Confirm new password",
|
|
||||||
"changeButton": "Change Password",
|
|
||||||
"changingButton": "Changing...",
|
|
||||||
"passwordChanged": "Password changed",
|
|
||||||
"passwordChangeFailed": "Failed to change password"
|
|
||||||
},
|
|
||||||
"languages": {
|
|
||||||
"spokenLabel": "Languages you speak",
|
|
||||||
"addLanguage": "Add a language...",
|
|
||||||
"defaultLabel": "Default language",
|
|
||||||
"defaultHint": "Used for UI localization.",
|
|
||||||
"translateToLabel": "Translate to",
|
|
||||||
"translateToHint": "Target language when translating content you don't speak."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"header": {
|
|
||||||
"userMenu": {
|
|
||||||
"profile": "Profile",
|
|
||||||
"systemSettings": "System Settings",
|
|
||||||
"signOut": "Sign Out"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"dock": {
|
|
||||||
"chat": "Chat",
|
|
||||||
"files": "Files",
|
|
||||||
"terminal": "Terminal",
|
|
||||||
"plans": "Plans",
|
|
||||||
"skills": "Skills",
|
|
||||||
"tasks": "Tasks",
|
|
||||||
"processes": "Processes",
|
|
||||||
"logs": "Logs"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
{
|
|
||||||
"common": {
|
|
||||||
"save": "Guardar",
|
|
||||||
"saving": "A guardar...",
|
|
||||||
"cancel": "Cancelar"
|
|
||||||
},
|
|
||||||
"settings": {
|
|
||||||
"profile": {
|
|
||||||
"searchPlaceholder": "Pesquisar definições...",
|
|
||||||
"sections": {
|
|
||||||
"profileTitle": "Perfil",
|
|
||||||
"profileDescription": "Atualiza o teu nome e avatar.",
|
|
||||||
"changePasswordTitle": "Alterar Palavra-passe",
|
|
||||||
"changePasswordDescription": "Atualiza a palavra-passe da tua conta.",
|
|
||||||
"tasksTitle": "Tarefas",
|
|
||||||
"tasksDescription": "Modelo predefinido para tarefas do explorador de ficheiros.",
|
|
||||||
"languagesTitle": "Idiomas",
|
|
||||||
"languagesDescription": "Define os teus idiomas e preferências de tradução."
|
|
||||||
},
|
|
||||||
"userData": {
|
|
||||||
"emailLabel": "Email",
|
|
||||||
"nameLabel": "Nome",
|
|
||||||
"namePlaceholder": "O teu nome",
|
|
||||||
"profileUpdated": "Perfil atualizado",
|
|
||||||
"profileUpdateFailed": "Falha ao atualizar perfil",
|
|
||||||
"selectImageFile": "Por favor seleciona um ficheiro de imagem",
|
|
||||||
"imageTooLarge": "A imagem deve ter menos de 384KB"
|
|
||||||
},
|
|
||||||
"changePassword": {
|
|
||||||
"currentPasswordLabel": "Palavra-passe Atual",
|
|
||||||
"currentPasswordPlaceholder": "Palavra-passe atual",
|
|
||||||
"newPasswordLabel": "Nova Palavra-passe",
|
|
||||||
"newPasswordPlaceholder": "Nova palavra-passe",
|
|
||||||
"confirmPasswordLabel": "Confirmar Nova Palavra-passe",
|
|
||||||
"confirmPasswordPlaceholder": "Confirmar nova palavra-passe",
|
|
||||||
"changeButton": "Alterar Palavra-passe",
|
|
||||||
"changingButton": "A alterar...",
|
|
||||||
"passwordChanged": "Palavra-passe alterada",
|
|
||||||
"passwordChangeFailed": "Falha ao alterar palavra-passe"
|
|
||||||
},
|
|
||||||
"languages": {
|
|
||||||
"spokenLabel": "Idiomas que falas",
|
|
||||||
"addLanguage": "Adicionar idioma...",
|
|
||||||
"defaultLabel": "Idioma predefinido",
|
|
||||||
"defaultHint": "Usado para localização da interface.",
|
|
||||||
"translateToLabel": "Traduzir para",
|
|
||||||
"translateToHint": "Idioma alvo ao traduzir conteúdo que não falas."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"header": {
|
|
||||||
"userMenu": {
|
|
||||||
"profile": "Perfil",
|
|
||||||
"systemSettings": "Definições do Sistema",
|
|
||||||
"signOut": "Terminar Sessão"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"dock": {
|
|
||||||
"chat": "Chat",
|
|
||||||
"files": "Ficheiros",
|
|
||||||
"terminal": "Terminal",
|
|
||||||
"plans": "Planos",
|
|
||||||
"skills": "Skills",
|
|
||||||
"tasks": "Tarefas",
|
|
||||||
"processes": "Processos",
|
|
||||||
"logs": "Registos"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,6 +5,13 @@ import { useAuth } from 'hooks/useAuth';
|
|||||||
|
|
||||||
const QUERY_KEY = ['USER_SETTINGS'];
|
const QUERY_KEY = ['USER_SETTINGS'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UI locales went from bare codes to regional BCP 47 tags (docs/localization.md D2). A stale bare
|
||||||
|
* code must resolve rather than fail, and this is a one-time widening on read, not a DB migration.
|
||||||
|
*/
|
||||||
|
const WIDEN_BARE_LOCALE: Record<string, string> = { en: 'en-GB', pt: 'pt-PT' };
|
||||||
|
const widenLocale = (code: string): string => WIDEN_BARE_LOCALE[code] ?? code;
|
||||||
|
|
||||||
const mergeWithDefaults = (saved: Partial<UserSettings>): UserSettings => ({
|
const mergeWithDefaults = (saved: Partial<UserSettings>): UserSettings => ({
|
||||||
chat: { ...DEFAULT_SETTINGS.chat, ...saved.chat },
|
chat: { ...DEFAULT_SETTINGS.chat, ...saved.chat },
|
||||||
ai: {
|
ai: {
|
||||||
@@ -14,7 +21,11 @@ const mergeWithDefaults = (saved: Partial<UserSettings>): UserSettings => ({
|
|||||||
},
|
},
|
||||||
tasks: { ...DEFAULT_SETTINGS.tasks, ...saved.tasks },
|
tasks: { ...DEFAULT_SETTINGS.tasks, ...saved.tasks },
|
||||||
appearance: { ...DEFAULT_SETTINGS.appearance, ...saved.appearance },
|
appearance: { ...DEFAULT_SETTINGS.appearance, ...saved.appearance },
|
||||||
languages: { ...DEFAULT_SETTINGS.languages, ...saved.languages },
|
languages: {
|
||||||
|
...DEFAULT_SETTINGS.languages,
|
||||||
|
...saved.languages,
|
||||||
|
default: widenLocale(saved.languages?.default ?? DEFAULT_SETTINGS.languages.default),
|
||||||
|
},
|
||||||
tts: { ...DEFAULT_SETTINGS.tts, ...saved.tts },
|
tts: { ...DEFAULT_SETTINGS.tts, ...saved.tts },
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -121,7 +132,7 @@ export const DEFAULT_SETTINGS: UserSettings = {
|
|||||||
},
|
},
|
||||||
languages: {
|
languages: {
|
||||||
spoken: ['en'],
|
spoken: ['en'],
|
||||||
default: 'en',
|
default: 'en-GB',
|
||||||
translateTo: 'en',
|
translateTo: 'en',
|
||||||
},
|
},
|
||||||
tts: {
|
tts: {
|
||||||
|
|||||||
Reference in New Issue
Block a user