mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
feat(i18n): add German UI language (#34917)
* feat(i18n): add German UI language Register German (de) as a built-in UI language alongside en, zh, zh-Hant, ko, ja, es, fr, pt, ru and tr: language setting, system-locale detection (de, de-DE, de-AT, de-CH, de_DE.UTF-8), renderer and main-process lazy catalogs, Settings picker label in every catalog, the "Sprache" settings search keyword, onboarding picker, docs and a German glossary and style guide (config/i18n-german.md). de.json covers every en.json key except the sparse-policy entries that must fall back to English (status-bar credit balance copy and the incomplete goal change notice). Tests that used de-DE as the unsupported system language now use it-IT. The catalog is machine-translated, reviewed by two independent passes with arbitrated fixes, and swept for terminology consistency. * feat(i18n): translate German strings added on main Translate the 158 keys main added since the branch point (WSL project move, diff size limits and others), reviewed by two passes like the rest of the catalog, and drop the 5 keys main retired. --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
# German (de) UI Glossary and Style
|
||||
|
||||
Catalog: `src/renderer/src/i18n/locales/de.json`. Missing keys fall back to English at runtime. System detection maps `de`, `de-DE`, `de-AT`, `de-CH` and POSIX tags such as `de_DE.UTF-8` to `de`.
|
||||
|
||||
## Voice
|
||||
|
||||
- Avoid addressing the user. Prefer neutral phrasing: `Branch auswählen`, `Keine Worktrees gefunden`, `Datei konnte nicht gespeichert werden.`
|
||||
- When a sentence must address the user, use informal `du` consistently (`dein`, `dir`), lowercase. Never use `Sie`.
|
||||
- Buttons, menu items and commands use the infinitive: `Öffnen`, `Speichern`, `Löschen`, `Entfernen`, `Erneut versuchen`, `Abbrechen`.
|
||||
- Failures: `… konnte nicht geladen werden.` in sentences, `Laden fehlgeschlagen` in short labels.
|
||||
- Confirmations are impersonal questions: `Fenster schließen?`, `Worktree löschen?`
|
||||
- German has no title case. Capitalize nouns and the first word only, even when the English label uses title case. Keep brand names and acronyms as written.
|
||||
- Labels have no final period. Use `…`, not `...`. Use German quotes `„…“` in prose; keep backticks and code quoting as in the source.
|
||||
- Join English terms to German words with a hyphen: `Worktree-Name`, `Branch-Name`, `SSH-Verbindung`, `PR-Titel`, `Git-Repository`.
|
||||
- German strings run about 30% longer than English. Prefer the shorter natural wording for buttons, tabs and badges, but never drop meaning.
|
||||
- Plurals follow English (`_one` / `_other`). Keep `{{count}}` wherever the English form has it.
|
||||
|
||||
## Terms
|
||||
|
||||
Keep Git and developer terms that German developers use in English. Use the noun genders below for articles and adjective endings.
|
||||
|
||||
| English | German | Note |
|
||||
| ---------------------------- | --------------------------------------------- | ---------------------------------------------------------- |
|
||||
| worktree | der Worktree, die Worktrees | Distinct from workspace. |
|
||||
| workspace | der Arbeitsbereich | |
|
||||
| repository / repo | das Repository (die Repositorys) / das Repo | |
|
||||
| branch / base branch | der Branch (die Branches) / Basis-Branch | |
|
||||
| commit | der Commit; verb `committen` | |
|
||||
| pull request / PR | der Pull Request / PR | `merge request` stays `Merge Request` (GitLab). |
|
||||
| merge, rebase, diff, stash | Merge, Rebase, Diff, Stash | Verbs: `mergen`, `rebasen`, `stashen`. |
|
||||
| checkout | Checkout; verb `auschecken` | |
|
||||
| push / pull / fetch | Push / Pull / Fetch | Verbs: `pushen`, `pullen`, `fetchen`. |
|
||||
| remote (Git) / remote host | das Remote / Remote-Host | `remote` as an adjective: `entfernt`. |
|
||||
| issue | das Issue | |
|
||||
| review / code review | das Review / Code-Review | |
|
||||
| check (CI) | der Check | |
|
||||
| label (issue/PR) | das Label, die Labels | |
|
||||
| approve / comment | genehmigen / der Kommentar | |
|
||||
| source control | Quellcodeverwaltung | |
|
||||
| agent | der Agent, die Agenten | |
|
||||
| chat / prompt | der Chat / der Prompt | |
|
||||
| skill / plugin / hook | der Skill / das Plugin / der Hook | |
|
||||
| session | die Sitzung | |
|
||||
| terminal / shell | das Terminal / die Shell | |
|
||||
| tab | der Tab | The keyboard key stays `Tab`. |
|
||||
| pane / panel | der Bereich / das Panel | Split pane: `Bereich teilen`. Keep the two terms distinct. |
|
||||
| sidebar / status bar | die Seitenleiste / die Statusleiste | |
|
||||
| editor / browser | der Editor / der Browser | |
|
||||
| command palette / quick open | Befehlspalette / Schnellöffnen | |
|
||||
| file / folder / directory | Datei / Ordner / Verzeichnis | |
|
||||
| project | das Projekt | |
|
||||
| host / server / SSH host | der Host / der Server / der SSH-Host | |
|
||||
| automation / task | die Automatisierung / die Aufgabe | |
|
||||
| dashboard | das Dashboard | |
|
||||
| settings | die Einstellungen | |
|
||||
| shortcut / keybinding | das Tastenkürzel / die Tastenbelegung | |
|
||||
| sign in / sign out | anmelden / abmelden | |
|
||||
| account / provider / model | das Konto / der Anbieter / das Modell | |
|
||||
| token (LLM or auth) | der Token | |
|
||||
| usage / quota / rate limit | die Nutzung / das Kontingent / das Ratenlimit | |
|
||||
| credits / balance | Credits / das Guthaben | |
|
||||
| notification | die Benachrichtigung | |
|
||||
| delete / remove | löschen / entfernen | Keep the two terms distinct. |
|
||||
| enable / disable / default | aktivieren / deaktivieren / Standard | |
|
||||
| pin / unread / archive | anheften / ungelesen / archivieren | |
|
||||
| environment / sandbox | die Umgebung / die Sandbox | |
|
||||
| dictation | das Diktat; verb `diktieren` | |
|
||||
| clipboard | die Zwischenablage | |
|
||||
| port forwarding | die Portweiterleitung | |
|
||||
|
||||
Do not translate placeholders (`{{value0}}`), markup, commands, paths, keyboard keys, or labels that belong to another product's UI.
|
||||
@@ -20,6 +20,13 @@
|
||||
"dynamic": false,
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"filePath": "src/renderer/src/components/settings/appearance-search.ts",
|
||||
"kind": "object-property:keywords",
|
||||
"text": "Sprache",
|
||||
"dynamic": false,
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"filePath": "src/renderer/src/components/settings/appearance-search.ts",
|
||||
"kind": "object-property:keywords",
|
||||
|
||||
@@ -5,7 +5,7 @@ import process from 'node:process'
|
||||
import { repairTranslatedValue } from './locale-translation-policy.mjs'
|
||||
|
||||
const LOCALES_DIR = path.join('src', 'renderer', 'src', 'i18n', 'locales')
|
||||
const SUPPORTED_LOCALES = new Set(['es', 'fr', 'ja', 'ko', 'ru', 'zh'])
|
||||
const SUPPORTED_LOCALES = new Set(['de', 'es', 'fr', 'ja', 'ko', 'ru', 'zh'])
|
||||
const PLACEHOLDER_RE = /\{\{[^}]+\}\}/g
|
||||
|
||||
function placeholders(value) {
|
||||
|
||||
@@ -28,7 +28,7 @@ Settings are grouped into panes. Everything here is searchable with `Cmd-,` then
|
||||
- Status bar toggles, including **Resource Manager** (CPU/memory/sessions, daemon controls, workspace disk scans).
|
||||
- **Usage percentages** — show provider limits as **% used** or **% remaining** on the status-bar roster.
|
||||
- App Icon — cycle between Classic, Watercolor, and Blue for the icon shown in the Dock and window switcher.
|
||||
- **Language** — primary Interface control. Switch between System (follows the OS), English, 中文(简体), 繁體中文(台灣), 한국어, 日本語, Español, Français, Português (Brasil), Русский, or Türkçe. Settings search also matches native words for “language” (语言 / 語言 / 언어 / 言語 / Idioma / Langue / Язык / Dil) so you can find Language while the UI is still in English.
|
||||
- **Language** — primary Interface control. Switch between System (follows the OS), English, 中文(简体), 繁體中文(台灣), 한국어, 日本語, Español, Français, Português (Brasil), Русский, Türkçe, or Deutsch. Settings search also matches native words for “language” (语言 / 語言 / 언어 / 言語 / Idioma / Langue / Язык / Dil / Sprache) so you can find Language while the UI is still in English.
|
||||
|
||||
## Git
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ const LAZY_LOCALE_LOADERS: Record<
|
||||
Exclude<SupportedUiLocale, 'en'>,
|
||||
() => Promise<{ default: Record<string, unknown> }>
|
||||
> = {
|
||||
de: () => import('../../renderer/src/i18n/locales/de.json'),
|
||||
es: () => import('../../renderer/src/i18n/locales/es.json'),
|
||||
fr: () => import('../../renderer/src/i18n/locales/fr.json'),
|
||||
ja: () => import('../../renderer/src/i18n/locales/ja.json'),
|
||||
|
||||
@@ -77,7 +77,7 @@ it.each([
|
||||
status: 'supported'
|
||||
},
|
||||
{ platform: 'Linux C-only', preferences: ['C.UTF-8', 'C'], locale: 'en', status: 'unknown' },
|
||||
{ platform: 'unsupported', preferences: ['de-DE', 'fr-FR'], locale: 'en', status: 'unsupported' }
|
||||
{ platform: 'unsupported', preferences: ['it-IT', 'fr-FR'], locale: 'en', status: 'unsupported' }
|
||||
])('handles $platform preference lists', async ({ preferences, locale, status }) => {
|
||||
getPreferredSystemLanguages.mockReturnValue(preferences)
|
||||
const { getSystemLanguageDetection } = await import('./system-language')
|
||||
|
||||
@@ -23,9 +23,9 @@ beforeEach(async () => {
|
||||
useLanguageReadiness.setState({ ...useLanguageReadiness.getInitialState(), ready: true })
|
||||
usePluginLanguagePackStore.setState({ entries: [], loaded: true })
|
||||
vi.spyOn(languageReadiness, 'initializeLanguageDetection').mockResolvedValue(
|
||||
detectUiLanguage(['de-DE'], 'native')
|
||||
detectUiLanguage(['it-IT'], 'native')
|
||||
)
|
||||
setRendererLanguageDetection(detectUiLanguage(['de-DE'], 'native'))
|
||||
setRendererLanguageDetection(detectUiLanguage(['it-IT'], 'native'))
|
||||
Object.defineProperty(window, 'api', {
|
||||
configurable: true,
|
||||
value: { feedback: { submit }, telemetryTrack }
|
||||
@@ -321,7 +321,7 @@ describe('onboarding language request', () => {
|
||||
expect(screen.getByRole('radio', { name: 'Français' }).getAttribute('data-state')).toBe(
|
||||
'checked'
|
||||
)
|
||||
expect(screen.getAllByRole('radio')).toHaveLength(10)
|
||||
expect(screen.getAllByRole('radio')).toHaveLength(11)
|
||||
expect(screen.getByRole('radio', { name: 'Русский' })).toBeTruthy()
|
||||
expect(save).not.toHaveBeenCalled()
|
||||
expect(useAppStore.getState().settings?.uiLanguage).toBe('system')
|
||||
@@ -333,7 +333,7 @@ describe('onboarding language request', () => {
|
||||
useAppStore.setState({ settings: { ...getDefaultSettings('/tmp'), uiLanguage: 'fr' } })
|
||||
render(<OnboardingLanguageSwitcher open onOpenChange={vi.fn()} />)
|
||||
expect(
|
||||
screen.getByText('System language: de-DE. This language is not supported yet.')
|
||||
screen.getByText('System language: it-IT. This language is not supported yet.')
|
||||
).toBeTruthy()
|
||||
expect(screen.queryByText(/Orca uses English/)).toBeNull()
|
||||
expect(screen.getByRole('radio', { name: 'Français' }).getAttribute('data-state')).toBe(
|
||||
@@ -460,7 +460,7 @@ describe('onboarding language request', () => {
|
||||
})
|
||||
installWebPreloadApi()
|
||||
render(<OnboardingLanguageSwitcher open onOpenChange={vi.fn()} />)
|
||||
expect(screen.getAllByRole('radio')).toHaveLength(10)
|
||||
expect(screen.getAllByRole('radio')).toHaveLength(11)
|
||||
expect(screen.queryByRole('button', { name: 'Other language…' })).toBeNull()
|
||||
expect(screen.queryByRole('textbox')).toBeNull()
|
||||
} finally {
|
||||
@@ -476,7 +476,7 @@ describe('onboarding language request', () => {
|
||||
const save = vi.fn().mockResolvedValue(undefined)
|
||||
useAppStore.setState({ updateSettingsOrThrow: save })
|
||||
render(<OnboardingLanguageSwitcher open onOpenChange={vi.fn()} />)
|
||||
expect(screen.getAllByRole('radio')).toHaveLength(10)
|
||||
expect(screen.getAllByRole('radio')).toHaveLength(11)
|
||||
expect(screen.queryByRole('button', { name: 'Retry' })).toBeNull()
|
||||
expect(save).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -58,6 +58,7 @@ export const getLanguageEntries = createLocalizedCatalog((): SettingsSearchEntry
|
||||
...translateSearchKeyword('settings.appearance.language.portuguese', 'Português (Brasil)'),
|
||||
...translateSearchKeyword('settings.appearance.language.russian', 'Русский'),
|
||||
...translateSearchKeyword('settings.appearance.language.turkish', 'Türkçe'),
|
||||
...translateSearchKeyword('settings.appearance.language.german', 'Deutsch'),
|
||||
// Index native language words in every locale so users can find their language from an unfamiliar UI.
|
||||
'语言',
|
||||
'語言',
|
||||
@@ -67,6 +68,7 @@ export const getLanguageEntries = createLocalizedCatalog((): SettingsSearchEntry
|
||||
'Langue',
|
||||
'Язык',
|
||||
'Dil',
|
||||
'Sprache',
|
||||
...translateSearchKeyword(
|
||||
'auto.components.settings.appearance.search.language.locale',
|
||||
'locale'
|
||||
|
||||
@@ -7,6 +7,7 @@ import i18next, {
|
||||
import { initReactI18next } from 'react-i18next'
|
||||
|
||||
import enRuntimeRequired from './en-runtime-required.json'
|
||||
import deCatalogUrl from './locales/de.json?url'
|
||||
import esCatalogUrl from './locales/es.json?url'
|
||||
import frCatalogUrl from './locales/fr.json?url'
|
||||
import jaCatalogUrl from './locales/ja.json?url'
|
||||
@@ -32,6 +33,7 @@ export const i18n: I18nInstance = i18next.createInstance()
|
||||
|
||||
// Fetch catalogs as data: Chromium caches rejected module imports for the document's lifetime.
|
||||
const NON_DEFAULT_LOCALE_URLS: Record<Exclude<SupportedUiLocale, 'en'>, string> = {
|
||||
de: deCatalogUrl,
|
||||
es: esCatalogUrl,
|
||||
fr: frCatalogUrl,
|
||||
ja: jaCatalogUrl,
|
||||
|
||||
@@ -24,7 +24,7 @@ afterEach(() => vi.useRealTimers())
|
||||
describe('client-local startup language', () => {
|
||||
it('uses the local native snapshot over Chromium preferences and freezes it for startup', async () => {
|
||||
const probe = vi.fn().mockResolvedValue({
|
||||
detectedTag: 'de-DE',
|
||||
detectedTag: 'it-IT',
|
||||
status: 'unsupported',
|
||||
locale: 'en',
|
||||
source: 'native'
|
||||
@@ -32,7 +32,7 @@ describe('client-local startup language', () => {
|
||||
installBridge(probe)
|
||||
const { initializeLanguageDetection } = await import('./language-readiness')
|
||||
expect(await initializeLanguageDetection()).toMatchObject({
|
||||
detectedTag: 'de-DE',
|
||||
detectedTag: 'it-IT',
|
||||
source: 'native'
|
||||
})
|
||||
await initializeLanguageDetection()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -387,7 +387,8 @@
|
||||
"french": "Français",
|
||||
"portuguese": "Português (Brasil)",
|
||||
"russian": "Русский",
|
||||
"turkish": "Türkçe"
|
||||
"turkish": "Türkçe",
|
||||
"german": "Deutsch"
|
||||
},
|
||||
"statusBar": {
|
||||
"claudeToggleDescription": "Show Claude token and cost usage for the active workspace.",
|
||||
|
||||
@@ -117,7 +117,8 @@
|
||||
"french": "Français",
|
||||
"portuguese": "Português (Brasil)",
|
||||
"russian": "Русский",
|
||||
"turkish": "Türkçe"
|
||||
"turkish": "Türkçe",
|
||||
"german": "Deutsch"
|
||||
},
|
||||
"statusBar": {
|
||||
"claudeToggleDescription": "Muestra el consumo de tokens y el costo de Claude para el espacio de trabajo activo.",
|
||||
|
||||
@@ -290,7 +290,8 @@
|
||||
"french": "Français",
|
||||
"portuguese": "Português (Brasil)",
|
||||
"russian": "Русский",
|
||||
"turkish": "Türkçe"
|
||||
"turkish": "Türkçe",
|
||||
"german": "Deutsch"
|
||||
},
|
||||
"statusBar": {
|
||||
"claudeToggleDescription": "Afficher l'utilisation des tokens et des coûts de Claude pour l'espace de travail actif.",
|
||||
|
||||
@@ -117,7 +117,8 @@
|
||||
"french": "Français",
|
||||
"portuguese": "Português (Brasil)",
|
||||
"russian": "Русский",
|
||||
"turkish": "Türkçe"
|
||||
"turkish": "Türkçe",
|
||||
"german": "Deutsch"
|
||||
},
|
||||
"statusBar": {
|
||||
"claudeToggleDescription": "アクティブなワークスペースの Claude トークンとコストの使用状況を表示します。",
|
||||
|
||||
@@ -117,7 +117,8 @@
|
||||
"french": "Français",
|
||||
"portuguese": "Português (Brasil)",
|
||||
"russian": "Русский",
|
||||
"turkish": "Türkçe"
|
||||
"turkish": "Türkçe",
|
||||
"german": "Deutsch"
|
||||
},
|
||||
"statusBar": {
|
||||
"claudeToggleDescription": "활성 워크스페이스에 대한 Claude 토큰 및 비용 사용량을 표시합니다.",
|
||||
|
||||
@@ -733,7 +733,8 @@
|
||||
"french": "Français",
|
||||
"portuguese": "Português (Brasil)",
|
||||
"russian": "Русский",
|
||||
"turkish": "Türkçe"
|
||||
"turkish": "Türkçe",
|
||||
"german": "Deutsch"
|
||||
},
|
||||
"statusBar": {
|
||||
"claudeToggleDescription": "Mostra o uso de tokens e custo do Claude para o espaço de trabalho ativo.",
|
||||
|
||||
@@ -359,7 +359,8 @@
|
||||
"french": "Français",
|
||||
"portuguese": "Português (Brasil)",
|
||||
"russian": "Русский",
|
||||
"turkish": "Türkçe"
|
||||
"turkish": "Türkçe",
|
||||
"german": "Deutsch"
|
||||
},
|
||||
"statusBar": {
|
||||
"claudeToggleDescription": "Показывать расход токенов и стоимость Claude для активного рабочего пространства.",
|
||||
|
||||
@@ -359,7 +359,8 @@
|
||||
"french": "Français",
|
||||
"portuguese": "Português (Brasil)",
|
||||
"russian": "Русский",
|
||||
"turkish": "Türkçe"
|
||||
"turkish": "Türkçe",
|
||||
"german": "Deutsch"
|
||||
},
|
||||
"statusBar": {
|
||||
"claudeToggleDescription": "Etkin çalışma alanı için Claude token ve maliyet kullanımını göster.",
|
||||
|
||||
@@ -359,7 +359,8 @@
|
||||
"french": "Français",
|
||||
"portuguese": "Português (Brasil)",
|
||||
"russian": "Русский",
|
||||
"turkish": "Türkçe"
|
||||
"turkish": "Türkçe",
|
||||
"german": "Deutsch"
|
||||
},
|
||||
"statusBar": {
|
||||
"claudeToggleDescription": "顯示目前工作區的 Claude token 用量與費用。",
|
||||
|
||||
@@ -117,7 +117,8 @@
|
||||
"french": "Français",
|
||||
"portuguese": "Português (Brasil)",
|
||||
"russian": "Русский",
|
||||
"turkish": "Türkçe"
|
||||
"turkish": "Türkçe",
|
||||
"german": "Deutsch"
|
||||
},
|
||||
"statusBar": {
|
||||
"claudeToggleDescription": "显示当前工作区的 Claude Token 与费用消耗情况。",
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
UI_LANGUAGE_CHINESE_TRADITIONAL,
|
||||
UI_LANGUAGE_ENGLISH,
|
||||
UI_LANGUAGE_FRENCH,
|
||||
UI_LANGUAGE_GERMAN,
|
||||
UI_LANGUAGE_JAPANESE,
|
||||
UI_LANGUAGE_KOREAN,
|
||||
UI_LANGUAGE_PORTUGUESE,
|
||||
@@ -38,7 +39,8 @@ export const UI_LANGUAGE_CHOICES: UiLanguageChoice[] = [
|
||||
{ value: UI_LANGUAGE_FRENCH, labelKey: 'settings.appearance.language.french' },
|
||||
{ value: UI_LANGUAGE_PORTUGUESE, labelKey: 'settings.appearance.language.portuguese' },
|
||||
{ value: UI_LANGUAGE_RUSSIAN, labelKey: 'settings.appearance.language.russian' },
|
||||
{ value: UI_LANGUAGE_TURKISH, labelKey: 'settings.appearance.language.turkish' }
|
||||
{ value: UI_LANGUAGE_TURKISH, labelKey: 'settings.appearance.language.turkish' },
|
||||
{ value: UI_LANGUAGE_GERMAN, labelKey: 'settings.appearance.language.german' }
|
||||
]
|
||||
|
||||
const UI_LANGUAGE_CHOICE_FALLBACKS: Record<BuiltInUiLanguage, string> = {
|
||||
@@ -52,7 +54,8 @@ const UI_LANGUAGE_CHOICE_FALLBACKS: Record<BuiltInUiLanguage, string> = {
|
||||
[UI_LANGUAGE_FRENCH]: 'Français',
|
||||
[UI_LANGUAGE_PORTUGUESE]: 'Português (Brasil)',
|
||||
[UI_LANGUAGE_RUSSIAN]: 'Русский',
|
||||
[UI_LANGUAGE_TURKISH]: 'Türkçe'
|
||||
[UI_LANGUAGE_TURKISH]: 'Türkçe',
|
||||
[UI_LANGUAGE_GERMAN]: 'Deutsch'
|
||||
}
|
||||
|
||||
export function getUiLanguageChoiceLabel(
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
UI_LANGUAGE_CHINESE_TRADITIONAL,
|
||||
UI_LANGUAGE_ENGLISH,
|
||||
UI_LANGUAGE_FRENCH,
|
||||
UI_LANGUAGE_GERMAN,
|
||||
UI_LANGUAGE_JAPANESE,
|
||||
UI_LANGUAGE_KOREAN,
|
||||
UI_LANGUAGE_PORTUGUESE,
|
||||
@@ -28,10 +29,11 @@ describe('normalizeUiLanguage', () => {
|
||||
expect(normalizeUiLanguage(UI_LANGUAGE_PORTUGUESE)).toBe('pt')
|
||||
expect(normalizeUiLanguage(UI_LANGUAGE_RUSSIAN)).toBe('ru')
|
||||
expect(normalizeUiLanguage(UI_LANGUAGE_TURKISH)).toBe('tr')
|
||||
expect(normalizeUiLanguage(UI_LANGUAGE_GERMAN)).toBe('de')
|
||||
})
|
||||
|
||||
it('falls back unknown values to system', () => {
|
||||
expect(normalizeUiLanguage('de')).toBe('system')
|
||||
expect(normalizeUiLanguage('it')).toBe('system')
|
||||
expect(normalizeUiLanguage(null)).toBe('system')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -10,6 +10,7 @@ export const UI_LANGUAGE_FRENCH = 'fr'
|
||||
export const UI_LANGUAGE_PORTUGUESE = 'pt'
|
||||
export const UI_LANGUAGE_RUSSIAN = 'ru'
|
||||
export const UI_LANGUAGE_TURKISH = 'tr'
|
||||
export const UI_LANGUAGE_GERMAN = 'de'
|
||||
|
||||
export type BuiltInUiLanguage =
|
||||
| typeof UI_LANGUAGE_SYSTEM
|
||||
@@ -23,6 +24,7 @@ export type BuiltInUiLanguage =
|
||||
| typeof UI_LANGUAGE_PORTUGUESE
|
||||
| typeof UI_LANGUAGE_RUSSIAN
|
||||
| typeof UI_LANGUAGE_TURKISH
|
||||
| typeof UI_LANGUAGE_GERMAN
|
||||
|
||||
export type PluginUiLanguage = `plugin:${string}`
|
||||
export type UiLanguage = BuiltInUiLanguage | PluginUiLanguage
|
||||
@@ -38,7 +40,8 @@ const UI_LANGUAGE_VALUES = new Set<BuiltInUiLanguage>([
|
||||
UI_LANGUAGE_FRENCH,
|
||||
UI_LANGUAGE_PORTUGUESE,
|
||||
UI_LANGUAGE_RUSSIAN,
|
||||
UI_LANGUAGE_TURKISH
|
||||
UI_LANGUAGE_TURKISH,
|
||||
UI_LANGUAGE_GERMAN
|
||||
])
|
||||
|
||||
const PLUGIN_UI_LANGUAGE_RE =
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
normalizeUiLocaleTag,
|
||||
shouldShowLanguageSwitcher
|
||||
} from './ui-locale'
|
||||
import { UI_LANGUAGE_RUSSIAN, UI_LANGUAGE_SYSTEM } from './ui-language'
|
||||
import { UI_LANGUAGE_GERMAN, UI_LANGUAGE_RUSSIAN, UI_LANGUAGE_SYSTEM } from './ui-language'
|
||||
|
||||
describe('ui-locale', () => {
|
||||
it.each(['ru', 'ru-RU', 'ru-KZ', 'ru_RU.UTF-8'])('resolves Russian %s', (tag) => {
|
||||
@@ -16,6 +16,11 @@ describe('ui-locale', () => {
|
||||
expect(resolveUiLocale(UI_LANGUAGE_RUSSIAN, 'en-US')).toBe('ru')
|
||||
expect(shouldShowLanguageSwitcher(detectUiLanguage([tag], 'native'))).toBe(true)
|
||||
})
|
||||
it.each(['de', 'de-DE', 'de-AT', 'de-CH', 'de_DE.UTF-8'])('resolves German %s', (tag) => {
|
||||
expect(detectUiLanguage([tag], 'native')).toMatchObject({ locale: 'de', status: 'supported' })
|
||||
expect(resolveUiLocale(UI_LANGUAGE_SYSTEM, tag)).toBe('de')
|
||||
expect(resolveUiLocale(UI_LANGUAGE_GERMAN, 'en-US')).toBe('de')
|
||||
})
|
||||
it('maps Traditional Chinese system locales to zh-Hant and keeps Simplified on zh', () => {
|
||||
for (const tag of ['zh-TW', 'zh-HK', 'zh-MO', 'zh-Hant']) {
|
||||
expect(normalizeSupportedUiLocale(tag)).toBe('zh-Hant')
|
||||
@@ -44,7 +49,7 @@ describe('system language detection', () => {
|
||||
['zh-MO', 'zh-Hant', 'supported'],
|
||||
['zh_CN.UTF-8@hant', 'zh-Hant', 'supported'],
|
||||
['zh_TW.UTF-8@hans', 'zh', 'supported'],
|
||||
['de-DE', 'en', 'unsupported'],
|
||||
['it-IT', 'en', 'unsupported'],
|
||||
['en--US', 'en', 'unknown'],
|
||||
['C.UTF-8', 'en', 'unknown'],
|
||||
['POSIX', 'en', 'unknown']
|
||||
@@ -53,8 +58,8 @@ describe('system language detection', () => {
|
||||
})
|
||||
|
||||
it('retains an unsupported primary instead of using a supported secondary', () => {
|
||||
expect(detectUiLanguage(['de-DE', 'fr-FR', 'C'], 'native')).toMatchObject({
|
||||
detectedTag: 'de-DE',
|
||||
expect(detectUiLanguage(['it-IT', 'fr-FR', 'C'], 'native')).toMatchObject({
|
||||
detectedTag: 'it-IT',
|
||||
locale: 'en',
|
||||
status: 'unsupported'
|
||||
})
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
UI_LANGUAGE_CHINESE_TRADITIONAL,
|
||||
UI_LANGUAGE_ENGLISH,
|
||||
UI_LANGUAGE_FRENCH,
|
||||
UI_LANGUAGE_GERMAN,
|
||||
UI_LANGUAGE_JAPANESE,
|
||||
UI_LANGUAGE_KOREAN,
|
||||
UI_LANGUAGE_PORTUGUESE,
|
||||
@@ -24,7 +25,8 @@ export const SUPPORTED_UI_LOCALES = [
|
||||
'fr',
|
||||
'pt',
|
||||
'ru',
|
||||
'tr'
|
||||
'tr',
|
||||
'de'
|
||||
] as const
|
||||
export type SupportedUiLocale = (typeof SUPPORTED_UI_LOCALES)[number]
|
||||
|
||||
@@ -153,6 +155,9 @@ export function resolveUiLocale(
|
||||
if (language === UI_LANGUAGE_TURKISH) {
|
||||
return 'tr'
|
||||
}
|
||||
if (language === UI_LANGUAGE_GERMAN) {
|
||||
return 'de'
|
||||
}
|
||||
return normalizeSupportedUiLocale(systemLocale)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user