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

* fix: proper plural forms for count strings and full translations

* fix: plural forms for selected, hidden and active counts

* fix: synced plugin updates failing on desktop and server-only plugins shown as stopped

* fix: restore rounded corners on app icons and favicon

* feat: desktop app auto updates with automatic, ask first or never modes

* feat: community and custom plugin registries

* feat: plugin guide for AI tools in the SDK with a lint check

* fix: keep the terminal WebSocket open past nginx's 60s default (#1588)

* chore: update changelog

* feat: show plugin download counts only and stop CI builds inflating them

* feat: hide installed plugins in the browse tab by default

* chore: pin bundled plugins to latest releases

* chore: update translations

* fix: focusing a saved password field and saving no longer erases it

* fix: tell desktop users when their 2.8 data needs 2.9 first

* fix: point windows update info at the x64 installer

* fix: keep desktop client tunnel sockets alive behind proxies

* chore: pin bundled plugins to latest releases

* fix: cover client tunnel keepalive in the c2s runtime tests

* fix: typing after opening a host no longer goes to the host search

* fix: accept tab and = separators in SSH config import (#1598)

* chore: update changelog

* chore: drop plugin versions from core changelog

* chore: update translations

* chore: lint, format, and bump version to 26.10.1

---------

Co-authored-by: Darnel Hunter <68697450+innotelinc@users.noreply.github.com>
Co-authored-by: Iain <Iain1970@protonmail.com>
This commit is contained in:
authored and GitHub committed 2026-10-12 11:35:48 -05:00
1 parent 6873bc92bb
commit 62ac931468
174 files changed
+94334 -39995

No files matched your search

+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;