mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 06:05:24 +00:00
feat: AI translations at release, 7 new languages and regional fallback
This commit is contained in:
1 parent
6873bc92bb
commit
874ad9f4fd
11 files changed
+356
-134
No files matched your search
+109
-14
@@ -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:
|
||||
|
||||
@@ -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}"
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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 })),
|
||||
];
|
||||
@@ -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.
|
||||
@@ -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",
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in new issue
Block a user