mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
fix(app): clean up i18n settings labels
This commit is contained in:
@@ -12,12 +12,12 @@ test("Settings language selector switches General labels", async ({ page }) => {
|
||||
await expect(page.getByText("Default send", { exact: true }).first()).toBeVisible();
|
||||
|
||||
await page.getByRole("button", { name: "System", exact: true }).click();
|
||||
await page.getByRole("button", { name: "Simplified Chinese", exact: true }).click();
|
||||
await page.getByRole("button", { name: "简体中文 - Simplified Chinese", exact: true }).click();
|
||||
|
||||
await expect(page.getByText("默认发送", { exact: true }).first()).toBeVisible();
|
||||
|
||||
await page.getByRole("button", { name: "简体中文", exact: true }).click();
|
||||
await page.getByRole("button", { name: "English", exact: true }).click();
|
||||
await page.getByRole("button", { name: "English - 英语", exact: true }).click();
|
||||
|
||||
await expect(page.getByText("Default send", { exact: true }).first()).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { LANGUAGE_OPTIONS, parseAppLanguage, resolveSupportedLocale } from "./locales";
|
||||
import {
|
||||
LANGUAGE_OPTIONS,
|
||||
formatLanguageOptionLabel,
|
||||
parseAppLanguage,
|
||||
resolveSupportedLocale,
|
||||
} from "./locales";
|
||||
|
||||
describe("parseAppLanguage", () => {
|
||||
it("accepts system and all UN official language locales", () => {
|
||||
@@ -32,6 +37,44 @@ describe("parseAppLanguage", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatLanguageOptionLabel", () => {
|
||||
it("shows the native language name and English name in English UI", () => {
|
||||
const arabic = LANGUAGE_OPTIONS.find((option) => option.value === "ar");
|
||||
const spanish = LANGUAGE_OPTIONS.find((option) => option.value === "es");
|
||||
const chinese = LANGUAGE_OPTIONS.find((option) => option.value === "zh-CN");
|
||||
|
||||
expect([
|
||||
formatLanguageOptionLabel(arabic!, "en", "System"),
|
||||
formatLanguageOptionLabel(spanish!, "en", "System"),
|
||||
formatLanguageOptionLabel(chinese!, "en", "System"),
|
||||
]).toEqual(["العربية - Arabic", "Español - Spanish", "简体中文 - Simplified Chinese"]);
|
||||
});
|
||||
|
||||
it("shows the native language name and Chinese name in Chinese UI", () => {
|
||||
const arabic = LANGUAGE_OPTIONS.find((option) => option.value === "ar");
|
||||
const english = LANGUAGE_OPTIONS.find((option) => option.value === "en");
|
||||
const spanish = LANGUAGE_OPTIONS.find((option) => option.value === "es");
|
||||
|
||||
expect([
|
||||
formatLanguageOptionLabel(arabic!, "zh-CN", "系统"),
|
||||
formatLanguageOptionLabel(english!, "zh-CN", "系统"),
|
||||
formatLanguageOptionLabel(spanish!, "zh-CN", "系统"),
|
||||
]).toEqual(["العربية - 阿拉伯语", "English - 英语", "Español - 西班牙语"]);
|
||||
});
|
||||
|
||||
it("uses a single label when both language names match", () => {
|
||||
const english = LANGUAGE_OPTIONS.find((option) => option.value === "en");
|
||||
|
||||
expect(formatLanguageOptionLabel(english!, "en", "System")).toBe("English");
|
||||
});
|
||||
|
||||
it("uses the active-language name for System", () => {
|
||||
const system = LANGUAGE_OPTIONS.find((option) => option.value === "system");
|
||||
|
||||
expect(formatLanguageOptionLabel(system!, "zh-CN", "系统")).toBe("系统");
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveSupportedLocale", () => {
|
||||
it("respects explicit language choices", () => {
|
||||
expect(resolveSupportedLocale("ar", ["en-US"])).toBe("ar");
|
||||
@@ -44,11 +87,16 @@ describe("resolveSupportedLocale", () => {
|
||||
|
||||
it("maps UN official system locales", () => {
|
||||
expect(resolveSupportedLocale("system", ["ar-EG"])).toBe("ar");
|
||||
expect(resolveSupportedLocale("system", ["en-US"])).toBe("en");
|
||||
expect(resolveSupportedLocale("system", ["es-MX"])).toBe("es");
|
||||
expect(resolveSupportedLocale("system", ["fr-CA"])).toBe("fr");
|
||||
expect(resolveSupportedLocale("system", ["ru-RU"])).toBe("ru");
|
||||
});
|
||||
|
||||
it("keeps English when Spanish is a secondary system language", () => {
|
||||
expect(resolveSupportedLocale("system", ["en-US", "es-GB"])).toBe("en");
|
||||
});
|
||||
|
||||
it("maps Chinese system locales to Simplified Chinese", () => {
|
||||
expect(resolveSupportedLocale("system", ["zh"])).toBe("zh-CN");
|
||||
expect(resolveSupportedLocale("system", ["zh-CN"])).toBe("zh-CN");
|
||||
|
||||
@@ -19,6 +19,64 @@ export const LANGUAGE_OPTIONS: LanguageOption[] = [
|
||||
];
|
||||
|
||||
const SUPPORTED_LANGUAGES = new Set<AppLanguage>(["system", "ar", "en", "es", "fr", "ru", "zh-CN"]);
|
||||
const LANGUAGE_NATIVE_NAMES: Record<SupportedLocale, string> = {
|
||||
ar: "العربية",
|
||||
en: "English",
|
||||
es: "Español",
|
||||
fr: "Français",
|
||||
ru: "Русский",
|
||||
"zh-CN": "简体中文",
|
||||
};
|
||||
const LANGUAGE_NAMES_BY_LOCALE: Record<SupportedLocale, Record<SupportedLocale, string>> = {
|
||||
ar: {
|
||||
ar: "العربية",
|
||||
en: "الإنجليزية",
|
||||
es: "الإسبانية",
|
||||
fr: "الفرنسية",
|
||||
ru: "الروسية",
|
||||
"zh-CN": "الصينية المبسطة",
|
||||
},
|
||||
en: {
|
||||
ar: "Arabic",
|
||||
en: "English",
|
||||
es: "Spanish",
|
||||
fr: "French",
|
||||
ru: "Russian",
|
||||
"zh-CN": "Simplified Chinese",
|
||||
},
|
||||
es: {
|
||||
ar: "árabe",
|
||||
en: "inglés",
|
||||
es: "español",
|
||||
fr: "francés",
|
||||
ru: "ruso",
|
||||
"zh-CN": "chino simplificado",
|
||||
},
|
||||
fr: {
|
||||
ar: "arabe",
|
||||
en: "anglais",
|
||||
es: "espagnol",
|
||||
fr: "français",
|
||||
ru: "russe",
|
||||
"zh-CN": "chinois simplifié",
|
||||
},
|
||||
ru: {
|
||||
ar: "арабский",
|
||||
en: "английский",
|
||||
es: "испанский",
|
||||
fr: "французский",
|
||||
ru: "русский",
|
||||
"zh-CN": "упрощенный китайский",
|
||||
},
|
||||
"zh-CN": {
|
||||
ar: "阿拉伯语",
|
||||
en: "英语",
|
||||
es: "西班牙语",
|
||||
fr: "法语",
|
||||
ru: "俄语",
|
||||
"zh-CN": "简体中文",
|
||||
},
|
||||
};
|
||||
|
||||
export function parseAppLanguage(value: unknown): AppLanguage | null {
|
||||
return typeof value === "string" && SUPPORTED_LANGUAGES.has(value as AppLanguage)
|
||||
@@ -26,6 +84,24 @@ export function parseAppLanguage(value: unknown): AppLanguage | null {
|
||||
: null;
|
||||
}
|
||||
|
||||
export function formatLanguageOptionLabel(
|
||||
option: LanguageOption,
|
||||
activeLocale: SupportedLocale,
|
||||
systemLabel: string,
|
||||
): string {
|
||||
if (option.value === "system") {
|
||||
return systemLabel;
|
||||
}
|
||||
|
||||
const nativeName = LANGUAGE_NATIVE_NAMES[option.value];
|
||||
const activeLanguageName = LANGUAGE_NAMES_BY_LOCALE[activeLocale][option.value];
|
||||
if (nativeName === activeLanguageName) {
|
||||
return nativeName;
|
||||
}
|
||||
|
||||
return `${nativeName} - ${activeLanguageName}`;
|
||||
}
|
||||
|
||||
export function resolveSupportedLocale(
|
||||
language: AppLanguage,
|
||||
systemLocales: readonly string[],
|
||||
@@ -39,6 +115,9 @@ export function resolveSupportedLocale(
|
||||
if (normalized === "ar" || normalized.startsWith("ar-")) {
|
||||
return "ar";
|
||||
}
|
||||
if (normalized === "en" || normalized.startsWith("en-")) {
|
||||
return "en";
|
||||
}
|
||||
if (normalized === "es" || normalized.startsWith("es-")) {
|
||||
return "es";
|
||||
}
|
||||
|
||||
@@ -127,6 +127,25 @@ describe("translation resources", () => {
|
||||
expect(findInterpolationMismatches(zhCN)).toEqual([]);
|
||||
});
|
||||
|
||||
it("keeps reported Spanish settings and scripts labels clean", () => {
|
||||
expect(es.workspace.scripts.title).toBe("Scripts");
|
||||
expect(es.settings.general.terminalScrollback.label).toBe("Historial de terminal");
|
||||
expect(es.settings.project.scripts.title).toBe("Scripts");
|
||||
});
|
||||
|
||||
it("keeps model count labels spaced around the count", () => {
|
||||
expect(ar.modelSelector.modelCountPlural).toBe("{{count}} نماذج");
|
||||
expect(es.modelSelector.modelCountPlural).toBe("{{count}} modelos");
|
||||
expect(fr.modelSelector.modelCountPlural).toBe("{{count}} modèles");
|
||||
expect(ru.modelSelector.modelCountPlural).toBe("{{count}} моделей");
|
||||
expect(zhCN.modelSelector.modelCountPlural).toBe("{{count}} 个模型");
|
||||
expect(ar.settings.providers.models.many).toBe("{{count}} نماذج");
|
||||
expect(es.settings.providers.models.many).toBe("{{count}} modelos");
|
||||
expect(fr.settings.providers.models.many).toBe("{{count}} modèles");
|
||||
expect(ru.settings.providers.models.many).toBe("{{count}} моделей");
|
||||
expect(zhCN.settings.providers.models.many).toBe("{{count}} 个 Model");
|
||||
});
|
||||
|
||||
it("keeps local connection fallback errors translated", () => {
|
||||
expect(findUntranslatedConnectionErrors()).toEqual([]);
|
||||
});
|
||||
|
||||
@@ -1082,8 +1082,8 @@ export const ar: TranslationResources = {
|
||||
favorites: "المفضلة",
|
||||
favoriteModel: "النموذج المفضل",
|
||||
unfavoriteModel: "نموذج غير مفضل",
|
||||
modelCount: "موديل{{count}}",
|
||||
modelCountPlural: "نماذج{{count}}",
|
||||
modelCount: "{{count}} نموذج",
|
||||
modelCountPlural: "{{count}} نماذج",
|
||||
retry: "أعد المحاولة",
|
||||
retrying: "جارٍ إعادة المحاولة...",
|
||||
noMatches: "لا توجد نماذج تطابق بحثك",
|
||||
@@ -1677,7 +1677,7 @@ export const ar: TranslationResources = {
|
||||
},
|
||||
models: {
|
||||
one: "1 نموذج",
|
||||
many: "نماذج{{count}}",
|
||||
many: "{{count}} نماذج",
|
||||
addModel: "إضافة نموذج",
|
||||
addCustomTitle: "إضافة نموذج مخصص",
|
||||
modelId: "الموديل ID",
|
||||
|
||||
@@ -523,7 +523,7 @@ export const es: TranslationResources = {
|
||||
},
|
||||
},
|
||||
scripts: {
|
||||
title: "Guiones",
|
||||
title: "Scripts",
|
||||
actions: {
|
||||
run: "Correr",
|
||||
view: "Vista",
|
||||
@@ -1115,8 +1115,8 @@ export const es: TranslationResources = {
|
||||
favorites: "Favoritos",
|
||||
favoriteModel: "modelo favorito",
|
||||
unfavoriteModel: "Modelo no favorito",
|
||||
modelCount: "modelo{{count}}",
|
||||
modelCountPlural: "Modelos{{count}}",
|
||||
modelCount: "{{count}} modelo",
|
||||
modelCountPlural: "{{count}} modelos",
|
||||
retry: "Rever",
|
||||
retrying: "Reintentando...",
|
||||
noMatches: "Ningún modelo coincide con tu búsqueda",
|
||||
@@ -1408,9 +1408,9 @@ export const es: TranslationResources = {
|
||||
},
|
||||
},
|
||||
terminalScrollback: {
|
||||
label: "desplazamiento hacia atrásTerminal",
|
||||
label: "Historial de terminal",
|
||||
description: "Líneas mantenidas en el búfer de terminal incorporado",
|
||||
accessibilityLabel: "Líneas de desplazamiento hacia atrásTerminal",
|
||||
accessibilityLabel: "Líneas del historial de terminal",
|
||||
},
|
||||
language: {
|
||||
label: "Idioma",
|
||||
@@ -1716,7 +1716,7 @@ export const es: TranslationResources = {
|
||||
},
|
||||
models: {
|
||||
one: "1 modelo",
|
||||
many: "Modelos{{count}}",
|
||||
many: "{{count}} modelos",
|
||||
addModel: "Agregar modelo",
|
||||
addCustomTitle: "Agregar modelo personalizado",
|
||||
modelId: "ModeloID",
|
||||
@@ -1785,7 +1785,7 @@ export const es: TranslationResources = {
|
||||
teardownAccessibility: "Comandos de desmontaje del árbol de trabajo",
|
||||
},
|
||||
scripts: {
|
||||
title: "Guiones",
|
||||
title: "Scripts",
|
||||
info: "Servicios de larga duración y comandos únicos que puede iniciar desde cualquier agente en este proyecto",
|
||||
empty: "Aún no hay guiones.",
|
||||
untitled: "Guión sin título",
|
||||
|
||||
@@ -1118,8 +1118,8 @@ export const fr: TranslationResources = {
|
||||
favorites: "Favoris",
|
||||
favoriteModel: "Modèle préféré",
|
||||
unfavoriteModel: "Modèle défavorisé",
|
||||
modelCount: "Modèle{{count}}",
|
||||
modelCountPlural: "Modèles{{count}}",
|
||||
modelCount: "{{count}} modèle",
|
||||
modelCountPlural: "{{count}} modèles",
|
||||
retry: "Réessayer",
|
||||
retrying: "Nouvelle tentative...",
|
||||
noMatches: "Aucun modèle ne correspond à votre recherche",
|
||||
@@ -1722,7 +1722,7 @@ export const fr: TranslationResources = {
|
||||
},
|
||||
models: {
|
||||
one: "1 modèle",
|
||||
many: "Modèles{{count}}",
|
||||
many: "{{count}} modèles",
|
||||
addModel: "Ajouter un modèle",
|
||||
addCustomTitle: "Ajouter un modèle personnalisé",
|
||||
modelId: "ModèleID",
|
||||
|
||||
@@ -1107,8 +1107,8 @@ export const ru: TranslationResources = {
|
||||
favorites: "Избранное",
|
||||
favoriteModel: "Любимая модель",
|
||||
unfavoriteModel: "Нелюбимая модель",
|
||||
modelCount: "Модель{{count}}",
|
||||
modelCountPlural: "Модели{{count}}",
|
||||
modelCount: "{{count}} модель",
|
||||
modelCountPlural: "{{count}} моделей",
|
||||
retry: "Повторить попытку",
|
||||
retrying: "Повторная попытка...",
|
||||
noMatches: "Ни одна модель не соответствует вашему запросу",
|
||||
@@ -1709,7 +1709,7 @@ export const ru: TranslationResources = {
|
||||
},
|
||||
models: {
|
||||
one: "1 модель",
|
||||
many: "Модели{{count}}",
|
||||
many: "{{count}} моделей",
|
||||
addModel: "Добавить модель",
|
||||
addCustomTitle: "Добавить пользовательскую модель",
|
||||
modelId: "Модель ID",
|
||||
|
||||
@@ -94,7 +94,13 @@ import { resolveAppVersion } from "@/utils/app-version";
|
||||
import { settingsStyles } from "@/styles/settings";
|
||||
import { THINKING_TONE_NATIVE_PCM_BASE64 } from "@/utils/thinking-tone.native-pcm";
|
||||
import { useVoiceAudioEngineOptional } from "@/contexts/voice-context";
|
||||
import { LANGUAGE_OPTIONS, type AppLanguage } from "@/i18n/locales";
|
||||
import {
|
||||
LANGUAGE_OPTIONS,
|
||||
formatLanguageOptionLabel,
|
||||
parseAppLanguage,
|
||||
type AppLanguage,
|
||||
type SupportedLocale,
|
||||
} from "@/i18n/locales";
|
||||
import {
|
||||
HostConnectionsPage,
|
||||
HostAgentsPage,
|
||||
@@ -226,6 +232,11 @@ function getServiceUrlBehaviorLabel(t: TFunction, value: ServiceUrlBehavior): st
|
||||
return labels[value];
|
||||
}
|
||||
|
||||
function getActiveLocale(language: string | undefined): SupportedLocale {
|
||||
const parsed = parseAppLanguage(language);
|
||||
return parsed && parsed !== "system" ? parsed : "en";
|
||||
}
|
||||
|
||||
const SERVICE_URL_BEHAVIOR_VALUES: ServiceUrlBehavior[] = ["ask", "in-app", "external"];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -266,17 +277,20 @@ function ServiceUrlBehaviorMenuItem({
|
||||
|
||||
interface LanguageMenuItemProps {
|
||||
value: AppLanguage;
|
||||
activeLocale: SupportedLocale;
|
||||
selected: boolean;
|
||||
onChange: (value: AppLanguage) => void;
|
||||
}
|
||||
|
||||
function LanguageMenuItem({ value, selected, onChange }: LanguageMenuItemProps) {
|
||||
function LanguageMenuItem({ value, activeLocale, selected, onChange }: LanguageMenuItemProps) {
|
||||
const { t } = useTranslation();
|
||||
const handleSelect = useCallback(() => {
|
||||
onChange(value);
|
||||
}, [onChange, value]);
|
||||
const option = LANGUAGE_OPTIONS.find((entry) => entry.value === value);
|
||||
const label = option ? t(option.labelKey) : value;
|
||||
const label = option
|
||||
? formatLanguageOptionLabel(option, activeLocale, t(option.labelKey))
|
||||
: value;
|
||||
|
||||
return (
|
||||
<DropdownMenuItem selected={selected} onSelect={handleSelect}>
|
||||
@@ -294,14 +308,19 @@ function GeneralSection({
|
||||
handleTerminalScrollbackLinesChange,
|
||||
}: GeneralSectionProps) {
|
||||
const { theme } = useUnistyles();
|
||||
const { t } = useTranslation();
|
||||
const { t, i18n } = useTranslation();
|
||||
const activeLocale = getActiveLocale(i18n.language);
|
||||
const iconColor = theme.colors.foregroundMuted;
|
||||
const sendBehaviorOptions = useMemo(() => getSendBehaviorOptions(t), [t]);
|
||||
const selectedLanguageOption = LANGUAGE_OPTIONS.find(
|
||||
(option) => option.value === settings.language,
|
||||
);
|
||||
const selectedLanguageLabel = selectedLanguageOption
|
||||
? t(selectedLanguageOption.labelKey)
|
||||
? formatLanguageOptionLabel(
|
||||
selectedLanguageOption,
|
||||
activeLocale,
|
||||
t(selectedLanguageOption.labelKey),
|
||||
)
|
||||
: settings.language;
|
||||
const [terminalScrollbackValue, setTerminalScrollbackValue] = useState(
|
||||
String(settings.terminalScrollbackLines),
|
||||
@@ -364,6 +383,7 @@ function GeneralSection({
|
||||
<LanguageMenuItem
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
activeLocale={activeLocale}
|
||||
selected={settings.language === option.value}
|
||||
onChange={handleLanguageChange}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user