Files
pokopiawiki.tootaio.com/frontend/src/i18n.ts
xiaomai 976a2a2482 feat(i18n): implement dynamic system wording management
Add database schema and API endpoints for system wording keys and values
Replace hardcoded translations in frontend and backend with dynamic messages
Add System Wordings management interface to Admin view
2026-05-02 11:48:11 +08:00

129 lines
3.8 KiB
TypeScript

import { createI18n } from 'vue-i18n';
import { defaultLocale, systemWordingMessages, type SystemWordingTree } from '../../system-wordings';
export { defaultLocale } from '../../system-wordings';
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:3001';
const localeStorageKey = 'pokopia_locale';
const localeChangeEvent = 'pokopia-locale-change';
const messages = systemWordingMessages as unknown as Record<string, SystemWordingTree>;
const loadedWordingLocales = new Set<string>();
const pendingWordingLoads = new Map<string, Promise<void>>();
type SystemWordingsResponse = {
locale: string;
messages: SystemWordingTree;
};
export type MessageKey = keyof typeof messages.en;
export const i18n = createI18n({
legacy: false,
globalInjection: true,
locale: readStoredLocale(),
fallbackLocale: defaultLocale,
messages
});
function readStoredLocale(): string {
if (typeof localStorage === 'undefined') {
return defaultLocale;
}
const storedLocale = localStorage.getItem(localeStorageKey);
return storedLocale && storedLocale.trim() !== '' ? storedLocale : defaultLocale;
}
function globalLocaleRef() {
return i18n.global.locale as unknown as { value: string };
}
export function getCurrentLocale(): string {
return globalLocaleRef().value || defaultLocale;
}
function isMessageTree(value: SystemWordingTree[string] | undefined): value is SystemWordingTree {
return typeof value === 'object' && value !== null;
}
function mergeMessageTrees(...trees: Array<SystemWordingTree | undefined>): SystemWordingTree {
const merged: SystemWordingTree = {};
for (const tree of trees) {
if (!tree) {
continue;
}
for (const [key, value] of Object.entries(tree)) {
const current = merged[key];
merged[key] = isMessageTree(value) && isMessageTree(current) ? mergeMessageTrees(current, value) : value;
}
}
return merged;
}
function builtInMessagesFor(locale: string): SystemWordingTree {
return mergeMessageTrees(messages[defaultLocale], messages[locale]);
}
export async function loadSystemWordings(locale = getCurrentLocale(), force = false): Promise<void> {
const targetLocale = locale || defaultLocale;
if (!force && loadedWordingLocales.has(targetLocale)) {
return;
}
const pendingLoad = pendingWordingLoads.get(targetLocale);
if (pendingLoad) {
await pendingLoad;
return;
}
const loadPromise = (async () => {
try {
const response = await fetch(`${apiBaseUrl}/api/system-wordings?locale=${encodeURIComponent(targetLocale)}`);
if (!response.ok) {
throw new Error(`System wordings failed (${response.status})`);
}
const data = (await response.json()) as SystemWordingsResponse;
i18n.global.setLocaleMessage(
targetLocale,
mergeMessageTrees(messages[defaultLocale], messages[targetLocale], data.messages) as never
);
loadedWordingLocales.add(targetLocale);
} catch {
i18n.global.setLocaleMessage(targetLocale, builtInMessagesFor(targetLocale) as never);
} finally {
pendingWordingLoads.delete(targetLocale);
}
})();
pendingWordingLoads.set(targetLocale, loadPromise);
await loadPromise;
}
export function setCurrentLocale(locale: string): void {
const nextLocale = locale || defaultLocale;
globalLocaleRef().value = nextLocale;
if (typeof document !== 'undefined') {
document.documentElement.lang = nextLocale;
}
if (typeof localStorage !== 'undefined') {
localStorage.setItem(localeStorageKey, nextLocale);
}
if (typeof window !== 'undefined') {
window.dispatchEvent(new Event(localeChangeEvent));
}
}
export function onLocaleChange(callback: () => void): () => void {
window.addEventListener(localeChangeEvent, callback);
return () => window.removeEventListener(localeChangeEvent, callback);
}
setCurrentLocale(getCurrentLocale());