mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-06 21:49:08 +02:00
* [client] Add RTL layout support to the desktop UI <html dir> follows the active language, Radix primitives get the matching dir, physical spacing/positioning uses logical utilities, and directional icons, animations, arrow-key navigation and tooltip sides flip in RTL. * [client] Address RTL review feedback Force LTR with isolation for monospace values, let truncated names take their direction from their content, and keep the profile name field in the UI direction. * [client] Keep translated monospace labels in their own direction The forced-LTR rule for monospace values now skips elements with an explicit dir, translated development labels use dir=auto, and monospace values rendered through TruncatedText opt into LTR. --------- Co-authored-by: Edward <43848523+thomashacker@users.noreply.github.com>
105 lines
3.3 KiB
TypeScript
105 lines
3.3 KiB
TypeScript
import i18next from "i18next";
|
|
import { initReactI18next } from "react-i18next";
|
|
import { Events } from "@wailsio/runtime";
|
|
|
|
import { Preferences, I18n } from "@bindings/services";
|
|
import { type LanguageCode } from "@bindings/i18n/models.js";
|
|
|
|
// Relative path on purpose — alias globs (`@/…`) silently match nothing in some Vite dev setups.
|
|
type BundleEntry = { message: string; description?: string };
|
|
const bundleModules = import.meta.glob<Record<string, BundleEntry>>(
|
|
"../../../i18n/locales/*/common.json",
|
|
{ eager: true, import: "default" },
|
|
);
|
|
|
|
const resources: Record<string, { common: Record<string, string> }> = {};
|
|
for (const path in bundleModules) {
|
|
const match = /locales\/([^/]+)\/common\.json$/.exec(path);
|
|
if (match) {
|
|
const entries = bundleModules[path];
|
|
const messages: Record<string, string> = {};
|
|
for (const key in entries) {
|
|
messages[key] = entries[key].message;
|
|
}
|
|
resources[match[1]] = { common: messages };
|
|
}
|
|
}
|
|
|
|
function detectBrowserLanguage(available: string[]): string | null {
|
|
const tags = [navigator.language, ...(navigator.languages ?? [])].filter(
|
|
(tag): tag is string => typeof tag === "string" && tag.length > 0,
|
|
);
|
|
const byLower = new Map(available.map((code) => [code.toLowerCase(), code]));
|
|
for (const tag of tags) {
|
|
const lower = tag.toLowerCase();
|
|
const exact = byLower.get(lower);
|
|
if (exact) return exact;
|
|
const base = byLower.get(lower.split("-")[0]);
|
|
if (base) return base;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// An empty persisted language code is the Go-side signal for first run.
|
|
export async function initI18n(): Promise<void> {
|
|
const available = Object.keys(resources);
|
|
let language = "en";
|
|
let firstRun = false;
|
|
try {
|
|
const prefs = await Preferences.Get();
|
|
if (prefs?.language) {
|
|
language = prefs.language;
|
|
} else {
|
|
firstRun = true;
|
|
language = detectBrowserLanguage(available) ?? "en";
|
|
}
|
|
} catch (e) {
|
|
console.warn("read preferences for language failed, defaulting to en", e);
|
|
}
|
|
|
|
if (firstRun) {
|
|
Preferences.SetLanguage(language as LanguageCode).catch((err: unknown) =>
|
|
console.warn("persist detected language failed", err),
|
|
);
|
|
}
|
|
|
|
await i18next.use(initReactI18next).init({
|
|
lng: language,
|
|
fallbackLng: "en",
|
|
defaultNS: "common",
|
|
ns: ["common"],
|
|
resources,
|
|
interpolation: {
|
|
prefix: "{",
|
|
suffix: "}",
|
|
escapeValue: false,
|
|
},
|
|
returnNull: false,
|
|
});
|
|
|
|
syncDocumentLang();
|
|
i18next.on("languageChanged", syncDocumentLang);
|
|
|
|
Events.On("netbird:preferences:changed", (e) => {
|
|
const next = e.data?.language;
|
|
if (next && next !== i18next.language) {
|
|
i18next.changeLanguage(next).catch((err: unknown) => {
|
|
console.error("changeLanguage failed", err);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
function syncDocumentLang() {
|
|
if (typeof document !== "undefined") {
|
|
document.documentElement.lang = i18next.language;
|
|
document.documentElement.dir = i18next.dir(i18next.language);
|
|
}
|
|
}
|
|
|
|
export async function loadLanguages() {
|
|
return I18n.Languages();
|
|
}
|
|
|
|
export default i18next;
|