forked from baron/baron-sso
149 lines
3.6 KiB
TypeScript
149 lines
3.6 KiB
TypeScript
const LOCALE_STORAGE_KEY = "locale";
|
|
const DEFAULT_LOCALE = "ko";
|
|
const SUPPORTED_LOCALES = ["ko", "en"] as const;
|
|
|
|
type Locale = (typeof SUPPORTED_LOCALES)[number];
|
|
|
|
type TomlValue = string | TomlObject;
|
|
|
|
interface TomlObject {
|
|
[key: string]: TomlValue;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// eslint-disable-next-line import/no-unresolved
|
|
import enRaw from "../locales/en.toml?raw";
|
|
// Vite ?raw import는 런타임 상수로 번들됩니다.
|
|
// eslint-disable-next-line import/no-unresolved
|
|
import koRaw from "../locales/ko.toml?raw";
|
|
|
|
const translations: Record<Locale, TomlObject> = {
|
|
ko: parseToml(koRaw),
|
|
en: parseToml(enRaw),
|
|
};
|
|
|
|
function formatTemplate(
|
|
template: string,
|
|
vars?: Record<string, string | number>,
|
|
): string {
|
|
if (!vars) {
|
|
return template;
|
|
}
|
|
return template.replace(/\{\{\s*(\w+)\s*\}\}/g, (match, key) => {
|
|
const value = vars[key];
|
|
if (value === undefined || value === null) {
|
|
return match;
|
|
}
|
|
return String(value);
|
|
});
|
|
}
|
|
|
|
export 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);
|
|
}
|
|
return formatTemplate(fallback ?? key, vars);
|
|
}
|