forked from baron/baron-sso
200 lines
4.8 KiB
TypeScript
200 lines
4.8 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 setTomlValue(
|
|
target: TomlObject,
|
|
path: string[],
|
|
value: TomlValue,
|
|
): void {
|
|
let cursor: TomlObject = target;
|
|
for (let index = 0; index < path.length - 1; index += 1) {
|
|
const key = path[index];
|
|
const current = cursor[key];
|
|
if (!current || typeof current === "string") {
|
|
cursor[key] = {};
|
|
}
|
|
cursor = cursor[key] as TomlObject;
|
|
}
|
|
cursor[path[path.length - 1]] = value;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
setTomlValue(cursor, key.split("."), 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);
|
|
};
|
|
}
|