feat: AI translations at release, 7 new languages and regional fallback

This commit is contained in:
LukeGus committed 2026-10-11 00:19:05 -05:00
1 parent 6873bc92bb
commit 874ad9f4fd
11 files changed
+356 -134

No files matched your search

+109 -14
View File
@@ -161,15 +161,62 @@ jobs:
echo "sha=$SHA" >> "$GITHUB_OUTPUT"
echo "prev_ref=$PREV" >> "$GITHUB_OUTPUT"
verify-beta:
# Translates new or changed strings with the Termix LLM server and commits
# them to the dev branch, so the beta or stable build includes them. If the
# server is down it warns and the release goes on with what is there.
translate:
needs: [prep]
if: ${{ needs.prep.outputs.skip != 'true' && needs.prep.outputs.mode != 'Overwrite release' }}
runs-on: blacksmith-2vcpu-ubuntu-2404
outputs:
sha: ${{ steps.commit.outputs.sha }}
steps:
- name: Checkout dev branch
uses: actions/checkout@v7
with:
ref: ${{ needs.prep.outputs.sha || needs.prep.outputs.dev_branch }}
fetch-depth: 1
token: ${{ secrets.TERMIX_PAT }}
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version-file: ".nvmrc"
- name: Translate
uses: Termix-SSH/Termix-Registry/translate@main
with:
source: src/ui/locales/en.json
out: src/ui/locales/translated
languages: src/ui/locales/languages.json
api-key: ${{ secrets.TERMIX_LLM_API_KEY }}
- name: Commit translations
id: commit
run: |
git add src/ui/locales
if git diff --cached --quiet; then
echo "No translation changes."
elif [ "${{ needs.prep.outputs.mode }}" = "Dry run" ]; then
echo "DRY RUN: would commit these translation changes:"
git --no-pager diff --cached --stat
else
git config user.name "LukeGus"
git config user.email "bugattiguy527@gmail.com"
git commit -m "chore: update translations"
git push origin HEAD:"${{ needs.prep.outputs.dev_branch }}"
fi
echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
verify-beta:
needs: [prep, translate]
if: ${{ needs.prep.outputs.channel == 'Beta' && needs.prep.outputs.skip != 'true' }}
runs-on: blacksmith-2vcpu-ubuntu-2404
steps:
- name: Checkout beta commit
uses: actions/checkout@v7
with:
ref: ${{ needs.prep.outputs.sha }}
ref: ${{ needs.translate.outputs.sha || needs.prep.outputs.sha }}
fetch-depth: 1
- name: Setup Node.js
@@ -197,7 +244,7 @@ jobs:
run: npm run test
normalize:
needs: [prep]
needs: [prep, translate]
if: ${{ needs.prep.outputs.channel == 'Stable' && needs.prep.outputs.mode != 'Overwrite release' }}
runs-on: blacksmith-2vcpu-ubuntu-2404
steps:
@@ -284,8 +331,8 @@ jobs:
git push origin HEAD:"${{ needs.prep.outputs.dev_branch }}"
merge-to-main:
needs: [prep, normalize, sync-main]
if: ${{ always() && needs.prep.result == 'success' && needs.prep.outputs.channel == 'Stable' && (needs.normalize.result == 'success' || needs.normalize.result == 'skipped') && (needs.sync-main.result == 'success' || needs.sync-main.result == 'skipped') }}
needs: [prep, translate, normalize, sync-main]
if: ${{ always() && needs.prep.result == 'success' && needs.prep.outputs.channel == 'Stable' && (needs.translate.result == 'success' || needs.translate.result == 'skipped') && (needs.normalize.result == 'success' || needs.normalize.result == 'skipped') && (needs.sync-main.result == 'success' || needs.sync-main.result == 'skipped') }}
runs-on: blacksmith-2vcpu-ubuntu-2404
outputs:
main_sha: ${{ steps.merge.outputs.main_sha }}
@@ -353,14 +400,14 @@ jobs:
echo "build_ref=$MAIN_SHA" >> "$GITHUB_OUTPUT"
docker:
needs: [prep, verify-beta, merge-to-main]
needs: [prep, translate, verify-beta, merge-to-main]
if: ${{ always() && needs.prep.result == 'success' && needs.prep.outputs.skip != 'true' && (needs.merge-to-main.result == 'success' || needs.verify-beta.result == 'success') }}
uses: ./.github/workflows/docker.yml
with:
version: ${{ needs.prep.outputs.version }}
build_type: ${{ needs.prep.outputs.channel == 'Beta' && 'Beta' || 'Production' }}
dry_run: ${{ needs.prep.outputs.mode == 'Dry run' }}
source_ref: ${{ needs.merge-to-main.outputs.build_ref || needs.prep.outputs.sha }}
source_ref: ${{ needs.merge-to-main.outputs.build_ref || needs.translate.outputs.sha || needs.prep.outputs.sha }}
secrets: inherit
create-release:
@@ -418,7 +465,7 @@ jobs:
fi
create-beta-release:
needs: [prep, docker]
needs: [prep, translate, docker]
if: ${{ always() && needs.prep.outputs.channel == 'Beta' && needs.docker.result == 'success' && needs.prep.outputs.mode != 'Dry run' }}
runs-on: blacksmith-2vcpu-ubuntu-2404
permissions:
@@ -427,7 +474,7 @@ jobs:
- name: Checkout beta commit
uses: actions/checkout@v7
with:
ref: ${{ needs.prep.outputs.sha }}
ref: ${{ needs.translate.outputs.sha || needs.prep.outputs.sha }}
fetch-depth: 0
- name: Setup Node.js
@@ -440,7 +487,7 @@ jobs:
node scripts/generate-beta-body.cjs \
--version "${{ needs.prep.outputs.version }}" \
--branch "${{ needs.prep.outputs.dev_branch }}" \
--sha "${{ needs.prep.outputs.sha }}" \
--sha "${{ needs.translate.outputs.sha || needs.prep.outputs.sha }}" \
--prev "${{ needs.prep.outputs.prev_ref }}" > BETA_BODY.md
- name: Create beta release
@@ -456,11 +503,19 @@ jobs:
else
gh release create "$TAG" --repo ${{ github.repository }} \
--title "$TITLE" --notes-file BETA_BODY.md \
--prerelease --target "${{ needs.prep.outputs.sha }}"
--prerelease --target "${{ needs.translate.outputs.sha || needs.prep.outputs.sha }}"
fi
electron-release:
needs: [prep, merge-to-main, docker, create-release, create-beta-release]
needs:
[
prep,
translate,
merge-to-main,
docker,
create-release,
create-beta-release,
]
if: ${{ always() && needs.prep.outputs.skip != 'true' && needs.docker.result == 'success' && needs.create-release.result != 'failure' && needs.create-beta-release.result != 'failure' }}
uses: ./.github/workflows/electron.yml
with:
@@ -468,7 +523,7 @@ jobs:
artifact_destination: ${{ needs.prep.outputs.mode == 'Dry run' && 'file' || 'release' }}
release_tag: ${{ needs.prep.outputs.release_tag }}
version_override: ${{ needs.prep.outputs.channel == 'Beta' && needs.prep.outputs.version || '' }}
source_ref: ${{ needs.merge-to-main.outputs.build_ref || needs.prep.outputs.sha }}
source_ref: ${{ needs.merge-to-main.outputs.build_ref || needs.translate.outputs.sha || needs.prep.outputs.sha }}
secrets: inherit
electron-submit:
@@ -541,8 +596,48 @@ jobs:
# The docs site reads the env catalog, API spec and download list from the
# newest stable release.
docs-refresh:
# Docs written for this release live on dev-X.Y.Z in the Docs repo. Squash
# merge it into main once the release is out. No branch means nothing to do.
docs-merge:
needs: [prep, electron-release]
if: ${{ always() && needs.prep.outputs.channel == 'Stable' && needs.electron-release.result == 'success' && needs.prep.outputs.mode != 'Dry run' && needs.prep.outputs.mode != 'Overwrite release' }}
runs-on: blacksmith-2vcpu-ubuntu-2404
steps:
- name: Merge the docs dev branch into main
env:
GH_TOKEN: ${{ secrets.TERMIX_PAT }}
REPO: Termix-SSH/Docs
VERSION: ${{ needs.prep.outputs.version }}
run: |
BRANCH="dev-$VERSION"
if ! gh api "repos/$REPO/branches/$BRANCH" > /dev/null 2>&1; then
echo "$REPO has no $BRANCH branch, nothing to merge."
exit 0
fi
AHEAD=$(gh api "repos/$REPO/compare/main...$BRANCH" -q .ahead_by)
if [ "$AHEAD" = "0" ]; then
echo "$BRANCH has nothing main doesn't, deleting it."
gh api -X DELETE "repos/$REPO/git/refs/heads/$BRANCH"
exit 0
fi
PR_NUMBER=$(gh pr list --repo "$REPO" --head "$BRANCH" --base main --state open --json number -q '.[0].number')
if [ -z "$PR_NUMBER" ]; then
PR_NUMBER=$(gh pr create --repo "$REPO" --base main --head "$BRANCH" \
--title "v$VERSION" --body "Docs for Termix v$VERSION." | grep -oE '[0-9]+$')
fi
gh pr merge "$PR_NUMBER" --repo "$REPO" --squash --admin --delete-branch
STATE=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json state -q .state)
if [ "$STATE" != "MERGED" ]; then
echo "Docs PR #$PR_NUMBER did not merge (state: $STATE)."
exit 1
fi
docs-refresh:
needs: [prep, electron-release, docs-merge]
if: ${{ always() && needs.prep.outputs.channel == 'Stable' && needs.electron-release.result == 'success' && needs.prep.outputs.mode != 'Dry run' }}
runs-on: blacksmith-2vcpu-ubuntu-2404
steps:
+53
View File
@@ -0,0 +1,53 @@
name: Translate
on:
workflow_dispatch:
inputs:
retranslate:
description: "Translate every string again, not only new or changed ones"
type: boolean
default: false
permissions:
contents: write
concurrency:
group: translate-${{ github.ref }}
cancel-in-progress: false
jobs:
translate:
if: ${{ startsWith(github.ref, 'refs/heads/dev-') }}
runs-on: blacksmith-2vcpu-ubuntu-2404
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
fetch-depth: 1
token: ${{ secrets.TERMIX_PAT }}
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version-file: ".nvmrc"
- name: Translate
uses: Termix-SSH/Termix-Registry/translate@main
with:
source: src/ui/locales/en.json
out: src/ui/locales/translated
languages: src/ui/locales/languages.json
retranslate: ${{ inputs.retranslate }}
api-key: ${{ secrets.TERMIX_LLM_API_KEY }}
- name: Commit translations
run: |
git add src/ui/locales
if git diff --cached --quiet; then
echo "No translation changes."
exit 0
fi
git config user.name "LukeGus"
git config user.email "bugattiguy527@gmail.com"
git commit -m "chore: update translations"
git push origin HEAD:"${GITHUB_REF_NAME}"
+15
View File
@@ -1,5 +1,20 @@
# Changelog
## 26.10.1
### Added
- Slovak, Croatian, Slovenian, Lithuanian, Latvian, Estonian and Malay
### Changed
- Every language is now fully translated, in Termix and in every plugin, and stays up to date with each release
- Languages with more than two plural forms, like Russian, Polish and Arabic, now use the right one
### Fixed
- Regional browser languages like `es-MX`, `pt` or `zh-HK` showed English instead of the closest translation
## 26.10.0
### Added
+1 -38
View File
@@ -59,6 +59,7 @@ import {
normalizeLanguageCode,
rememberLoginLanguage,
} from "@/i18n/i18n";
import { LANGUAGES } from "@/i18n/languages";
import {
removeSilentSigninFromSearch,
shouldTriggerSilentSignin,
@@ -66,44 +67,6 @@ import {
import { Select2 } from "@/components/select2";
import { cn } from "@/lib/utils";
const LANGUAGES = [
{ code: "en", label: "English" },
{ code: "af", label: "Afrikaans" },
{ code: "ar", label: "العربية" },
{ code: "bn", label: "বাংলা" },
{ code: "bg", label: "Български" },
{ code: "ca", label: "Català" },
{ code: "zh-CN", label: "中文 (简体)" },
{ code: "zh-TW", label: "中文 (繁體)" },
{ code: "cs", label: "Čeština" },
{ code: "da", label: "Dansk" },
{ code: "nl", label: "Nederlands" },
{ code: "fi", label: "Suomi" },
{ code: "fr", label: "Français" },
{ code: "de", label: "Deutsch" },
{ code: "el", label: "Ελληνικά" },
{ code: "he", label: "עברית" },
{ code: "hi", label: "हिन्दी" },
{ code: "hu", label: "Magyar" },
{ code: "id", label: "Indonesia" },
{ code: "it", label: "Italiano" },
{ code: "ja", label: "日本語" },
{ code: "ko", label: "한국어" },
{ code: "no", label: "Norsk" },
{ code: "pl", label: "Polski" },
{ code: "pt-PT", label: "Português (PT)" },
{ code: "pt-BR", label: "Português (BR)" },
{ code: "ro", label: "Română" },
{ code: "ru", label: "Русский" },
{ code: "sr", label: "Српски" },
{ code: "es-ES", label: "Español" },
{ code: "sv-SE", label: "Svenska" },
{ code: "th", label: "ไทย" },
{ code: "tr", label: "Türkçe" },
{ code: "uk", label: "Українська" },
{ code: "vi", label: "Tiếng Việt" },
];
function LanguageRow({
label,
value,
+19 -41
View File
@@ -3,46 +3,14 @@ import { initReactI18next } from "react-i18next";
import LanguageDetector from "i18next-browser-languagedetector";
import enTranslation from "../locales/en.json";
import { TRANSLATED_LANGUAGES } from "./languages";
type LocaleModule = { default: ResourceKey };
/** i18n code -> translated file name, shared by core and plugin locales. */
export const LOCALE_FILES = {
af: "af_ZA",
ar: "ar_SA",
bn: "bn_BD",
bg: "bg_BG",
ca: "ca_ES",
cs: "cs_CZ",
da: "da_DK",
de: "de_DE",
el: "el_GR",
"es-ES": "es_ES",
fi: "fi_FI",
fr: "fr_FR",
he: "he_IL",
hi: "hi_IN",
hu: "hu_HU",
id: "id_ID",
it: "it_IT",
ja: "ja_JP",
ko: "ko_KR",
nl: "nl_NL",
no: "no_NO",
pl: "pl_PL",
"pt-PT": "pt_PT",
"pt-BR": "pt_BR",
ro: "ro_RO",
ru: "ru_RU",
sr: "sr_SP",
"sv-SE": "sv_SE",
th: "th_TH",
tr: "tr_TR",
uk: "uk_UA",
vi: "vi_VN",
"zh-CN": "zh_CN",
"zh-TW": "zh_TW",
} as const satisfies Record<string, string>;
export const LOCALE_FILES: Record<string, string> = Object.fromEntries(
TRANSLATED_LANGUAGES.map(({ code, file }) => [code, file]),
);
const translatedFiles = import.meta.glob<LocaleModule>(
"../locales/translated/*.json",
@@ -94,8 +62,20 @@ export function normalizeLanguageCode(language?: string | null): string {
);
if (exactMatch) return exactMatch;
const baseLanguage = normalized.split("-")[0];
return supportedLngs.includes(baseLanguage) ? baseLanguage : "en";
const baseLanguage = normalized.split("-")[0].toLowerCase();
if (supportedLngs.includes(baseLanguage)) return baseLanguage;
// Regional variants we don't ship (es-MX, pt, sv) use the closest one we do.
if (baseLanguage === "zh") {
return /-(tw|hk|mo|hant)\b/i.test(normalized) ? "zh-TW" : "zh-CN";
}
if (baseLanguage === "nb" || baseLanguage === "nn") return "no";
if (baseLanguage === "pt") return "pt-BR";
return (
supportedLngs.find((supported) =>
supported.toLowerCase().startsWith(`${baseLanguage}-`),
) ?? "en"
);
}
const localeBackend: BackendModule = {
@@ -106,9 +86,7 @@ const localeBackend: BackendModule = {
if (namespace !== CORE_NAMESPACE) {
const file =
normalizedLanguage === "en"
? "en"
: (LOCALE_FILES as Record<string, string>)[normalizedLanguage];
normalizedLanguage === "en" ? "en" : LOCALE_FILES[normalizedLanguage];
if (!pluginLocaleResolver || !file) {
callback(null, {});
return;
+17
View File
@@ -0,0 +1,17 @@
import languages from "../locales/languages.json";
/** One translated language. The translator in Termix-Registry keeps a copy of this list. */
export interface Language {
code: string;
file: string;
name: string;
label: string;
}
export const TRANSLATED_LANGUAGES: readonly Language[] = languages;
/** Every language the app can show, English first, for language pickers. */
export const LANGUAGES: readonly { code: string; label: string }[] = [
{ code: "en", label: "English" },
...TRANSLATED_LANGUAGES.map(({ code, label }) => ({ code, label })),
];
+6 -2
View File
@@ -1,5 +1,9 @@
# Translations
Only `en.json` needs to be updated. Every other language in `translated/` is filled in automatically for any string that doesn't have a translation yet.
Only `en.json` needs to be updated. Every other language in `translated/` is translated automatically on each release (and by the Translate workflow) with the Termix LLM server, using the translator in [Termix-Registry](https://github.com/Termix-SSH/Termix-Registry/tree/main/translate).
If an automatic translation is wrong, fix it in that language's file. Strings that already have a translation are never overwritten.
New strings and strings whose English changed are translated. `translations-lock.json` tracks which English text each translation was made from, so don't edit it by hand.
If an automatic translation is wrong, fix it in that language's file. A fixed string is kept until its English changes.
To add a language, add it to `languages.json` here and in `translate/languages.json` in Termix-Registry.
+103
View File
@@ -0,0 +1,103 @@
[
{ "code": "af", "file": "af_ZA", "name": "Afrikaans", "label": "Afrikaans" },
{ "code": "ar", "file": "ar_SA", "name": "Arabic", "label": "العربية" },
{ "code": "bn", "file": "bn_BD", "name": "Bengali", "label": "বাংলা" },
{ "code": "bg", "file": "bg_BG", "name": "Bulgarian", "label": "Български" },
{ "code": "ca", "file": "ca_ES", "name": "Catalan", "label": "Català" },
{
"code": "zh-CN",
"file": "zh_CN",
"name": "Simplified Chinese",
"label": "中文 (简体)"
},
{
"code": "zh-TW",
"file": "zh_TW",
"name": "Traditional Chinese (Taiwan)",
"label": "中文 (繁體)"
},
{ "code": "hr", "file": "hr_HR", "name": "Croatian", "label": "Hrvatski" },
{ "code": "cs", "file": "cs_CZ", "name": "Czech", "label": "Čeština" },
{ "code": "da", "file": "da_DK", "name": "Danish", "label": "Dansk" },
{ "code": "nl", "file": "nl_NL", "name": "Dutch", "label": "Nederlands" },
{ "code": "et", "file": "et_EE", "name": "Estonian", "label": "Eesti" },
{ "code": "fi", "file": "fi_FI", "name": "Finnish", "label": "Suomi" },
{ "code": "fr", "file": "fr_FR", "name": "French", "label": "Français" },
{ "code": "de", "file": "de_DE", "name": "German", "label": "Deutsch" },
{ "code": "el", "file": "el_GR", "name": "Greek", "label": "Ελληνικά" },
{ "code": "he", "file": "he_IL", "name": "Hebrew", "label": "עברית" },
{ "code": "hi", "file": "hi_IN", "name": "Hindi", "label": "हिन्दी" },
{ "code": "hu", "file": "hu_HU", "name": "Hungarian", "label": "Magyar" },
{
"code": "id",
"file": "id_ID",
"name": "Indonesian",
"label": "Indonesia"
},
{ "code": "it", "file": "it_IT", "name": "Italian", "label": "Italiano" },
{ "code": "ja", "file": "ja_JP", "name": "Japanese", "label": "日本語" },
{ "code": "ko", "file": "ko_KR", "name": "Korean", "label": "한국어" },
{ "code": "lv", "file": "lv_LV", "name": "Latvian", "label": "Latviešu" },
{
"code": "lt",
"file": "lt_LT",
"name": "Lithuanian",
"label": "Lietuvių"
},
{ "code": "ms", "file": "ms_MY", "name": "Malay", "label": "Bahasa Melayu" },
{
"code": "no",
"file": "no_NO",
"name": "Norwegian Bokmål",
"label": "Norsk"
},
{ "code": "pl", "file": "pl_PL", "name": "Polish", "label": "Polski" },
{
"code": "pt-PT",
"file": "pt_PT",
"name": "European Portuguese",
"label": "Português (PT)"
},
{
"code": "pt-BR",
"file": "pt_BR",
"name": "Brazilian Portuguese",
"label": "Português (BR)"
},
{ "code": "ro", "file": "ro_RO", "name": "Romanian", "label": "Română" },
{ "code": "ru", "file": "ru_RU", "name": "Russian", "label": "Русский" },
{
"code": "sr",
"file": "sr_SP",
"name": "Serbian (Cyrillic script)",
"label": "Српски"
},
{ "code": "sk", "file": "sk_SK", "name": "Slovak", "label": "Slovenčina" },
{
"code": "sl",
"file": "sl_SI",
"name": "Slovenian",
"label": "Slovenščina"
},
{
"code": "es-ES",
"file": "es_ES",
"name": "Spanish (Spain)",
"label": "Español"
},
{ "code": "sv-SE", "file": "sv_SE", "name": "Swedish", "label": "Svenska" },
{ "code": "th", "file": "th_TH", "name": "Thai", "label": "ไทย" },
{ "code": "tr", "file": "tr_TR", "name": "Turkish", "label": "Türkçe" },
{
"code": "uk",
"file": "uk_UA",
"name": "Ukrainian",
"label": "Українська"
},
{
"code": "vi",
"file": "vi_VN",
"name": "Vietnamese",
"label": "Tiếng Việt"
}
]
@@ -14,6 +14,7 @@ import {
UI_FONTS,
} from "@/lib/theme";
import { changeAppLanguage, normalizeLanguageCode } from "@/i18n/i18n";
import { LANGUAGES } from "@/i18n/languages";
import { getUserPreferences, saveUserPreferences } from "@/api/open-tabs-api";
import type { FontSizeId, ThemeId, UiFontId } from "@/types/ui-types";
@@ -48,44 +49,6 @@ const THEME_PALETTES: Record<
gruvbox: { bg: "#282828", panel: "#3c3836", text: "#ebdbb2" },
};
export const LANGUAGES = [
{ code: "en", label: "English" },
{ code: "af", label: "Afrikaans" },
{ code: "ar", label: "العربية" },
{ code: "bn", label: "বাংলা" },
{ code: "bg", label: "Български" },
{ code: "ca", label: "Català" },
{ code: "zh-CN", label: "中文 (简体)" },
{ code: "zh-TW", label: "中文 (繁體)" },
{ code: "cs", label: "Čeština" },
{ code: "da", label: "Dansk" },
{ code: "nl", label: "Nederlands" },
{ code: "fi", label: "Suomi" },
{ code: "fr", label: "Français" },
{ code: "de", label: "Deutsch" },
{ code: "el", label: "Ελληνικά" },
{ code: "he", label: "עברית" },
{ code: "hi", label: "हिन्दी" },
{ code: "hu", label: "Magyar" },
{ code: "id", label: "Indonesia" },
{ code: "it", label: "Italiano" },
{ code: "ja", label: "日本語" },
{ code: "ko", label: "한국어" },
{ code: "no", label: "Norsk" },
{ code: "pl", label: "Polski" },
{ code: "pt-PT", label: "Português (PT)" },
{ code: "pt-BR", label: "Português (BR)" },
{ code: "ro", label: "Română" },
{ code: "ru", label: "Русский" },
{ code: "sr", label: "Српски" },
{ code: "es-ES", label: "Español" },
{ code: "sv-SE", label: "Svenska" },
{ code: "th", label: "ไทย" },
{ code: "tr", label: "Türkçe" },
{ code: "uk", label: "Українська" },
{ code: "vi", label: "Tiếng Việt" },
];
export const THEME_LABEL_KEYS: Record<ThemeId, string> = {
system: "newUi.sidebar.userProfile.themeSystem",
light: "newUi.sidebar.userProfile.themeLight",
+2 -1
View File
@@ -66,7 +66,8 @@ import type { FontSizeId, ThemeId, UiFontId } from "@/types/ui-types";
import { toast } from "sonner";
import { changeAppLanguage, normalizeLanguageCode } from "@/i18n/i18n";
import { Select2 } from "@/components/select2";
import { LANGUAGES, THEMES } from "@/settings/appearance/appearance-controls";
import { THEMES } from "@/settings/appearance/appearance-controls";
import { LANGUAGES } from "@/i18n/languages";
import { clearLocalAdaptivePreferences } from "@/lib/local-adaptive-preferences";
import { reportBetaFeedbackUrl } from "@/lib/issue-url";
import { isPrerelease } from "@/plugins/plugin-model";
+30
View File
@@ -3,9 +3,11 @@ import { describe, expect, it, beforeEach } from "vitest";
import {
changeAppLanguage,
consumeLoginLanguage,
LOCALE_FILES,
normalizeLanguageCode,
rememberLoginLanguage,
} from "../../i18n/i18n";
import { LANGUAGES, TRANSLATED_LANGUAGES } from "../../i18n/languages";
describe("i18n language handling", () => {
beforeEach(() => {
@@ -20,6 +22,34 @@ describe("i18n language handling", () => {
expect(normalizeLanguageCode("unknown")).toBe("en");
});
it("maps regional variants to the closest shipped language", () => {
expect(normalizeLanguageCode("es-MX")).toBe("es-ES");
expect(normalizeLanguageCode("es")).toBe("es-ES");
expect(normalizeLanguageCode("sv")).toBe("sv-SE");
expect(normalizeLanguageCode("pt")).toBe("pt-BR");
expect(normalizeLanguageCode("pt-AO")).toBe("pt-BR");
expect(normalizeLanguageCode("zh")).toBe("zh-CN");
expect(normalizeLanguageCode("zh-HK")).toBe("zh-TW");
expect(normalizeLanguageCode("zh-Hant-TW")).toBe("zh-TW");
expect(normalizeLanguageCode("nb-NO")).toBe("no");
expect(normalizeLanguageCode("de-AT")).toBe("de");
expect(normalizeLanguageCode("sk-SK")).toBe("sk");
});
it("ships a translated file for every listed language", () => {
const files = new Set(
Object.keys(import.meta.glob("../../locales/translated/*.json")).map(
(path) => path.split("/").pop()?.replace(".json", ""),
),
);
for (const language of TRANSLATED_LANGUAGES) {
expect(LOCALE_FILES[language.code]).toBe(language.file);
expect(files.has(language.file), language.file).toBe(true);
}
expect(LANGUAGES[0]).toEqual({ code: "en", label: "English" });
expect(LANGUAGES).toHaveLength(TRANSLATED_LANGUAGES.length + 1);
});
it("stores the normalized language after a successful switch", async () => {
await expect(changeAppLanguage("zh_CN")).resolves.toBe("zh-CN");
expect(localStorage.getItem("i18nextLng")).toBe("zh-CN");