forked from baron/baron-sso
공통 i18n 레이어 추가
This commit is contained in:
1
common/core/auth/.gitkeep
Normal file
1
common/core/auth/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
11
common/core/i18n/index.ts
Normal file
11
common/core/i18n/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export { createTomlTranslator } from "./loader";
|
||||
export {
|
||||
DEFAULT_LOCALE,
|
||||
LOCALE_STORAGE_KEY,
|
||||
SUPPORTED_LOCALES,
|
||||
type Locale,
|
||||
type TomlObject,
|
||||
type TomlValue,
|
||||
type TranslatorInput,
|
||||
type TranslatorOptions,
|
||||
} from "./types";
|
||||
176
common/core/i18n/loader.ts
Normal file
176
common/core/i18n/loader.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
import {
|
||||
DEFAULT_LOCALE,
|
||||
LOCALE_STORAGE_KEY,
|
||||
SUPPORTED_LOCALES,
|
||||
type Locale,
|
||||
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);
|
||||
};
|
||||
}
|
||||
20
common/core/i18n/types.ts
Normal file
20
common/core/i18n/types.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
export const LOCALE_STORAGE_KEY = "locale";
|
||||
export const DEFAULT_LOCALE = "ko";
|
||||
export const SUPPORTED_LOCALES = ["ko", "en"] as const;
|
||||
|
||||
export type Locale = (typeof SUPPORTED_LOCALES)[number];
|
||||
|
||||
export type TomlValue = string | TomlObject;
|
||||
|
||||
export interface TomlObject {
|
||||
[key: string]: TomlValue;
|
||||
}
|
||||
|
||||
export interface TranslatorOptions {
|
||||
normalizeEscapedNewlines?: boolean;
|
||||
}
|
||||
|
||||
export interface TranslatorInput {
|
||||
en: string[];
|
||||
ko: string[];
|
||||
}
|
||||
1
common/core/query/.gitkeep
Normal file
1
common/core/query/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
1
common/core/session/.gitkeep
Normal file
1
common/core/session/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
1
common/core/utils/.gitkeep
Normal file
1
common/core/utils/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
Reference in New Issue
Block a user