1
0
forked from baron/baron-sso
Files
baron-sso/common/core/i18n/loader.ts

183 lines
4.3 KiB
TypeScript

import {
DEFAULT_LOCALE,
LOCALE_STORAGE_KEY,
type Locale,
SUPPORTED_LOCALES,
type TomlObject,
type TomlValue,
type TranslatorInput,
type TranslatorOptions,
} from "./types";
function mergeTomlObjects(base: TomlObject, override: TomlObject): TomlObject {
const result: TomlObject = { ...base };
for (const [key, value] of Object.entries(override)) {
const currentValue = result[key];
if (
typeof currentValue === "object" &&
currentValue !== null &&
typeof value === "object" &&
value !== null
) {
result[key] = mergeTomlObjects(currentValue as TomlObject, value);
continue;
}
result[key] = value;
}
return result;
}
function isSupportedLocale(value: string): value is Locale {
return (SUPPORTED_LOCALES as readonly string[]).includes(value);
}
function parseToml(raw: string): TomlObject {
const lines = raw.split(/\r?\n/);
const root: TomlObject = {};
let currentPath: string[] = [];
for (const rawLine of lines) {
const line = rawLine.trim();
if (!line || line.startsWith("#")) {
continue;
}
if (line.startsWith("[") && line.endsWith("]")) {
const sectionName = line.slice(1, -1).trim();
currentPath = sectionName
? sectionName
.split(".")
.map((part) => part.trim())
.filter(Boolean)
: [];
continue;
}
const eqIndex = line.indexOf("=");
if (eqIndex === -1) {
continue;
}
const key = line.slice(0, eqIndex).trim();
const valueRaw = line.slice(eqIndex + 1).trim();
if (!key) {
continue;
}
let value = valueRaw;
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
value = value.slice(1, -1);
}
let cursor: TomlObject = root;
for (const section of currentPath) {
if (!cursor[section] || typeof cursor[section] === "string") {
cursor[section] = {};
}
cursor = cursor[section] as TomlObject;
}
cursor[key] = value;
}
return root;
}
function getValue(target: TomlObject, key: string): string | undefined {
const parts = key.split(".");
let cursor: TomlValue = target;
for (const part of parts) {
if (typeof cursor !== "object" || cursor === null) {
return undefined;
}
cursor = (cursor as TomlObject)[part];
if (cursor === undefined) {
return undefined;
}
}
return typeof cursor === "string" ? cursor : undefined;
}
function detectLocale(): Locale {
if (typeof window === "undefined") {
return DEFAULT_LOCALE;
}
const stored = window.localStorage.getItem(LOCALE_STORAGE_KEY);
if (stored && isSupportedLocale(stored)) {
return stored;
}
const pathLocale = window.location.pathname.split("/")[1];
if (pathLocale && isSupportedLocale(pathLocale)) {
return pathLocale;
}
const browserLang = window.navigator.language.toLowerCase();
if (browserLang.startsWith("ko")) {
return "ko";
}
return DEFAULT_LOCALE;
}
function formatTemplate(
template: string,
vars?: Record<string, string | number>,
options?: TranslatorOptions,
): string {
const normalizedTemplate = options?.normalizeEscapedNewlines
? template.replace(/\\n/g, "\n")
: template;
if (!vars) {
return normalizedTemplate;
}
return normalizedTemplate.replace(/\{\{\s*(\w+)\s*\}\}/g, (match, key) => {
const value = vars[key];
if (value === undefined || value === null) {
return match;
}
return String(value);
});
}
export function createTomlTranslator(
input: TranslatorInput,
options?: TranslatorOptions,
) {
const translations: Record<Locale, TomlObject> = {
ko: input.ko
.map((raw) => parseToml(raw))
.reduce<TomlObject>(
(merged, current) => mergeTomlObjects(merged, current),
{},
),
en: input.en
.map((raw) => parseToml(raw))
.reduce<TomlObject>(
(merged, current) => mergeTomlObjects(merged, current),
{},
),
};
return function t(
key: string,
fallback?: string,
vars?: Record<string, string | number>,
): string {
const locale = detectLocale();
const value = getValue(translations[locale], key);
if (value && value.length > 0) {
return formatTemplate(value, vars, options);
}
return formatTemplate(fallback ?? key, vars, options);
};
}