From 6e375a5b177cd39dfcbe6f1e01b287c5c0b83635 Mon Sep 17 00:00:00 2001 From: Michael Han <107991372+shimmyshimmer@users.noreply.github.com> Date: Tue, 14 Jul 2026 01:38:04 -0700 Subject: [PATCH] Studio: add French, German, Spanish, Hindi, Arabic, Russian and Korean display languages (#7076) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Studio: add 7 display languages, complete and fix existing locales Adds fully translated French, German, Spanish, Hindi, Arabic, Russian and Korean locales. Fills in all missing keys for zh-CN (113), ja (71) and pt-BR (47), fixes translation errors found in review, and reorders the language dropdown by popularity. All overlays pass check-parity with zero missing keys and zero placeholder mismatches. * Studio: default display language to auto detect The language preference now defaults to auto and resolves against the browser language list, with exact tag match first and language subtag match second (pt-PT resolves to pt-BR, zh-TW to zh-CN). Auto detect is the first dropdown option and is translated in every locale. Explicit choices still persist and sync; personalization sync now round trips the preference instead of the resolved locale so auto stays auto across devices. Auto mode also follows browser languagechange events. * Studio: guard import.meta.env in translate for non-Vite contexts translate() read import.meta.env.DEV directly, which throws when the module runs outside Vite (SSR or Node tooling). Optional-chain it so the dev-only warning is skipped and translation still works everywhere. * Studio: RTL for Arabic, translate recipes, keep Traditional Chinese off zh-CN - Sync document dir from a per-locale dir field so Arabic mirrors the layout instead of rendering RTL text in an LTR shell. - Translate the recipes nav label in fr, de, ko and hi to match the other locales (Recettes, Rezepte, and native forms). - Detection no longer maps Traditional Chinese (zh-Hant / zh-TW / zh-HK / zh-MO) to Simplified zh-CN; those tags fall through to the next preferred language. Simplified tags (zh, zh-CN, zh-SG, zh-Hans) still resolve to zh-CN. * Studio: don't treat legacy synced English as an explicit language pick The old sync serialized the resolved locale on every save, so existing profiles carry appearance.language 'en' even when the user never chose a language. Hydrating that as a pinned locale forced non-English browsers back to English under the new Auto detect default. Payloads now carry version 2 (the preference itself); on hydrate a version 1 'en' maps to auto, while explicit picks and all version 2 values are kept as-is. * Studio: persist only known language codes from the locale table normalizePreference now returns a value re-derived from the LOCALES keys instead of the raw input. It stays functionally identical (the stored value was already whitelisted) but makes it explicit that only known, non-sensitive language codes are written to localStorage, and clears a false-positive clear-text-storage scan on the persistence path. * Studio i18n: fix Train label transliteration and tidy locale consistency - ja and hi: the nav and route Train label used the railway transliteration (トレイン and ट्रेन); switch to the training term already used everywhere else in each file (トレーニング, ट्रेनिंग). - zh-CN: keep VRAM in English to match every other locale and the PR's own keep-English rule, and drop an extra clause added to the upload size hint so it matches the English source. - hi: translate Recents to हाल के in the export and import section to match the sidebar label, and point users to the Configure tab by its translated name (कॉन्फ़िगर). - ru: reword the preview sharing hint to avoid the "disable to disable" repetition. i18n parity and the type checked build stay green. * Studio i18n: keep Arabic layout LTR until physical-direction CSS is converted Setting ar to dir rtl only mirrors the flex based shell, sidebar and settings dialog. The shared select, dialog and dropdown primitives use physical-direction utilities (right-2, top-5 right-5, ml-auto) that do not flip under dir rtl, so chevrons, close buttons and check marks land on the wrong side. Keep Arabic on an LTR layout for now, matching the original plan in this PR. Arabic text still renders right to left per element via bidi and chat content keeps dir auto, so nothing regresses. Full layout mirroring can follow once the physical-direction classes are converted to logical ones. * Studio i18n: do not let a generic zh after a Traditional tag pick Simplified navigator.languages can be a list like ['zh-TW', 'zh', 'en-US']. The zh-TW pass already falls through, but the bare zh then reached the language-subtag match and selected zh-CN, so Traditional Chinese users still got Simplified and the guard was defeated. detectLocale now remembers when a Traditional tag was seen and skips a later bare zh, so detection keeps falling through to the next non-Chinese language. A lone bare zh, and explicit zh-CN or zh-Hans fallbacks, still resolve to Simplified as before. * Studio i18n: collapse two locale comments to a single line The Arabic dir note in messages.ts and the bare-zh note in locale-store.ts were two lines each; tighten each to one. Comment only, no behavior change. * Studio i18n: translate Hindi strings that were left in English Seventeen hi.ts labels stayed in English while all the other locales translated them: the training parameter labels (Grad Accum, Grad Norm, Grad Checkpoint, Eval Loss, Clip p95/p99, Seed, Continued Pretraining), the API example labels (curl/Python/JavaScript + tools/advanced), Hugging Face token, the VRAM estimate and the training terminal start line. Parity only checks key/placeholder presence so it did not catch these. Brand and technical tokens (curl, Python, VRAM, Loss, p95/p99, Hugging Face, unsloth) stay in English as elsewhere. --------- Co-authored-by: danielhanchen --- .../profile/hooks/use-personalization-sync.ts | 48 +- .../settings/components/language-select.tsx | 14 +- studio/frontend/src/i18n/check-parity.ts | 16 +- studio/frontend/src/i18n/index.ts | 6 + studio/frontend/src/i18n/locale-store.ts | 169 ++- studio/frontend/src/i18n/locales/ar.ts | 1001 ++++++++++++++++ studio/frontend/src/i18n/locales/de.ts | 1042 ++++++++++++++++ studio/frontend/src/i18n/locales/en.ts | 1 + studio/frontend/src/i18n/locales/es.ts | 1043 +++++++++++++++++ studio/frontend/src/i18n/locales/fr.ts | 1038 ++++++++++++++++ studio/frontend/src/i18n/locales/hi.ts | 999 ++++++++++++++++ studio/frontend/src/i18n/locales/ja.ts | 101 +- studio/frontend/src/i18n/locales/ko.ts | 1002 ++++++++++++++++ studio/frontend/src/i18n/locales/pt-br.ts | 76 +- studio/frontend/src/i18n/locales/ru.ts | 999 ++++++++++++++++ studio/frontend/src/i18n/locales/zh-CN.ts | 171 ++- studio/frontend/src/i18n/messages.ts | 39 +- 17 files changed, 7684 insertions(+), 81 deletions(-) create mode 100644 studio/frontend/src/i18n/locales/ar.ts create mode 100644 studio/frontend/src/i18n/locales/de.ts create mode 100644 studio/frontend/src/i18n/locales/es.ts create mode 100644 studio/frontend/src/i18n/locales/fr.ts create mode 100644 studio/frontend/src/i18n/locales/hi.ts create mode 100644 studio/frontend/src/i18n/locales/ko.ts create mode 100644 studio/frontend/src/i18n/locales/ru.ts diff --git a/studio/frontend/src/features/profile/hooks/use-personalization-sync.ts b/studio/frontend/src/features/profile/hooks/use-personalization-sync.ts index eac1a64d7a..5dbb7b54bf 100644 --- a/studio/frontend/src/features/profile/hooks/use-personalization-sync.ts +++ b/studio/frontend/src/features/profile/hooks/use-personalization-sync.ts @@ -9,12 +9,12 @@ import { type Theme, } from "@/features/settings"; import { - DEFAULT_LOCALE, - getLocale, - isSupportedLocale, + DEFAULT_LOCALE_PREFERENCE, + getLocalePreference, + isLocalePreference, setLocale, - useLocale, - type Locale, + useLocalePreference, + type LocalePreference, } from "@/i18n"; import { useCallback, useEffect, useRef, useState } from "react"; import { @@ -25,6 +25,11 @@ import type { AvatarShape } from "../stores/user-profile-store"; const PUSH_DEBOUNCE_MS = 800; +// Version 2 payloads store the language preference ("auto" or a pinned +// locale). Version 1 always serialized the resolved locale, so its "en" is +// usually the old default rather than an explicit pick. +const PERSONALIZATION_VERSION = 2; + type ProfileSnapshot = { displayName: string; nickname: string; @@ -110,10 +115,10 @@ function profileSnapshot(): ProfileSnapshot { function payload( profile: ProfileSnapshot, theme: Theme, - language: Locale | null, + language: LocalePreference | null, ): PersonalizationWrite { return { - version: 1, + version: PERSONALIZATION_VERSION, profile: normalizeProfile(profile), appearance: { theme, language }, }; @@ -123,10 +128,23 @@ function serialized(data: PersonalizationWrite): string { return JSON.stringify(data); } +// Version 1 clients wrote language on every save, so a legacy "en" usually +// means the user never picked a language. Map it to auto; explicit picks of +// other locales (the old default was English) are kept. Version 2 payloads +// are trusted verbatim, so a deliberate English pick stays pinned. +export function remoteLanguagePreference( + version: unknown, + language: unknown, +): unknown { + const isLegacy = typeof version !== "number" || version < 2; + if (isLegacy && language === "en") return DEFAULT_LOCALE_PREFERENCE; + return language; +} + function hasLocalSettings( profile: ProfileSnapshot, theme: Theme, - language: Locale, + language: LocalePreference, ): boolean { return Boolean( profile.displayName || @@ -134,7 +152,7 @@ function hasLocalSettings( profile.avatarDataUrl || profile.avatarShape !== "circle" || theme !== "system" || - language !== DEFAULT_LOCALE, + language !== DEFAULT_LOCALE_PREFERENCE, ); } @@ -144,7 +162,7 @@ export function usePersonalizationSync(enabled: boolean): void { const avatarDataUrl = useUserProfileStore((s) => s.avatarDataUrl); const avatarShape = useUserProfileStore((s) => s.avatarShape); const { theme } = useTheme(); - const language = useLocale(); + const language = useLocalePreference(); const [hydratedGeneration, setHydratedGeneration] = useState(0); const authGenerationRef = useRef(0); const latestThemeRef = useRef(theme); @@ -191,8 +209,12 @@ export function usePersonalizationSync(enabled: boolean): void { avatarShape: remote.profile.avatarShape === "rounded" ? "rounded" : "circle", }; const nextTheme = remote.appearance.theme; - const nextLanguage = isSupportedLocale(remote.appearance.language) - ? remote.appearance.language + const remoteLanguage = remoteLanguagePreference( + remote.version, + remote.appearance.language, + ); + const nextLanguage = isLocalePreference(remoteLanguage) + ? remoteLanguage : latestLanguageRef.current; useUserProfileStore.setState(nextProfile); if (nextTheme !== latestThemeRef.current) setTheme(nextTheme); @@ -207,7 +229,7 @@ export function usePersonalizationSync(enabled: boolean): void { useUserProfileStore.setState(nextProfile); } const nextTheme = latestThemeRef.current; - const nextLanguage = getLocale(); + const nextLanguage = getLocalePreference(); const nextPayload = payload(nextProfile, nextTheme, nextLanguage); const nextSerialized = serialized(nextPayload); if (hasLocalSettings(nextProfile, nextTheme, nextLanguage)) { diff --git a/studio/frontend/src/features/settings/components/language-select.tsx b/studio/frontend/src/features/settings/components/language-select.tsx index 9d30e06147..01fe049a56 100644 --- a/studio/frontend/src/features/settings/components/language-select.tsx +++ b/studio/frontend/src/features/settings/components/language-select.tsx @@ -9,22 +9,23 @@ import { SelectValue, } from "@/components/ui/select"; import { + AUTO_LOCALE, LOCALES, - isSupportedLocale, + isLocalePreference, setLocale, useT, - useLocale, + useLocalePreference, } from "@/i18n"; export function LanguageSelect() { const t = useT(); - const locale = useLocale(); + const preference = useLocalePreference(); return (