import { Asset, assetManager, AssetManager, JsonAsset, sys } from 'cc'; import { GEvent } from '../event/GEvent'; import { GBundle } from 'db://assets/script/game/ConfigRes'; import { Singleton } from '../../tools/Singleton'; const LANG_STORAGE_KEY = 'lang'; /** 支持的语言代码,与资源路径 `config/lang_${code}` / `lang_${code}` 一致 */ export const LangType = { zh: 'cn', en: 'en', ja: 'ja', ko: 'ko', ar: 'ar', hk: 'hk', } as const; export type LangCode = (typeof LangType)[keyof typeof LangType]; const ALL_CODES: readonly LangCode[] = [ LangType.zh, LangType.en, LangType.ja, LangType.ko, LangType.ar, LangType.hk, ]; function isLangCode(s: string): s is LangCode { return (ALL_CODES as readonly string[]).includes(s); } type LangTableRow = { key: string } & Record; /** 多语言 */ export class Lang extends Singleton { private _current: LangCode = LangType.zh; /** key -> 当前语言下的译文 */ private readonly _langMap = new Map(); /** 最近一次成功构建 Map 后为 true */ private _langReady = false; /** 递增,用于丢弃过期的异步加载结果 */ private _loadRequestId = 0; /** init / 首次加载 的 Promise,便于外部等待 */ private _initPromise: Promise | null = null; /** 当前语言代码 */ get Current(): LangCode { return this._current; } set Current(v: LangCode) { if (!isLangCode(v)) { console.warn('[Lang] 非法语言代码', v); return; } if (this._current === v && this._langReady) { return; } this._current = v; sys.localStorage.setItem(LANG_STORAGE_KEY, v); const id = ++this._loadRequestId; void this._loadForRequestId(id); } /** @deprecated 请使用 {@link Current} */ get Curent(): LangCode { return this._current; } /** @deprecated 请使用 {@link Current} */ set Curent(v: LangCode) { this.Current = v; } /** * 初始化并加载语言表;重复调用会复用进行中的 Promise。 */ async init(lang: LangCode | string = LangType.zh): Promise { if (this._initPromise) { return this._initPromise; } this._initPromise = this._doInit(lang); try { await this._initPromise; } finally { this._initPromise = null; } } private async _doInit(defaultLang: LangCode | string): Promise { const stored = sys.localStorage.getItem(LANG_STORAGE_KEY); this._current = stored && isLangCode(stored) ? stored as LangCode : defaultLang as LangCode; const id = ++this._loadRequestId; await this._loadForRequestId(id); } /** 语言表已就绪(成功加载过至少一次) */ get isReady(): boolean { return this._langReady; } /** * 等待语言表就绪(已在 init 后通常立即 resolve)。 */ whenReady(): Promise { if (this._langReady) { return Promise.resolve(); } return this.init(this._current); } /** * 取文案。未就绪时返回 key 或对 key 做占位符替换(不触发异步加载,请保证已 await init)。 * 占位符:`{1}`、`{2}` … 对应 `arr[0]`、`arr[1]`(1-based,与旧逻辑一致)。 */ get(key: string, arr: unknown[] = []): string { let s: string; if (this._langReady) { const raw = this._langMap.get(key); s = raw != null && raw !== '' ? raw : key; } else { s = key; } if (arr.length === 0) { return s === '' ? key : s; } const out = this._applyPlaceholders(s, arr); return out === '' ? key : out; } private _applyPlaceholders(template: string, arr: unknown[]): string { return template.replace(/\{(\d+)\}/g, (_m, g1: string) => { const idx = parseInt(g1, 10); if (idx < 1 || idx > arr.length) { return `{${g1}}`; } return String(arr[idx - 1]); }); } /** 当前语言的货币符号(可按项目继续扩展) */ getMoneySign(): string { const table: Partial> = { [LangType.zh]: '¥', [LangType.hk]: 'HK$', [LangType.en]: '$', [LangType.ja]: '¥', [LangType.ko]: '₩', [LangType.ar]: '﷼', }; return table[this._current] ?? '$'; } /** * 重新加载当前语言的 JSON 并刷新 Map(切换语言时已自动调用;也可手动刷新)。 */ async loadLang(): Promise { const id = ++this._loadRequestId; await this._loadForRequestId(id); } private async _loadForRequestId(requestId: number): Promise { const lang = this._current; try { let b = assetManager.getBundle(GBundle.First); if (!b) { b = await this._loadBundle(GBundle.First); } if (!b) { console.warn('[Lang] First bundle 不可用,跳过语言加载'); if (requestId === this._loadRequestId) { this._langMap.clear(); this._langReady = false; } return; } const path = `config/lang_${lang}`; const jsonAsset = (await this._loadRes(b, path, JsonAsset)) as JsonAsset | null; if (requestId !== this._loadRequestId) { return; } if (lang !== this._current) { return; } if (!jsonAsset?.json) { console.warn(`[Lang] 语言资源为空: ${path}`); this._langMap.clear(); this._langReady = false; GEvent.Ins.emit(GEvent.EVENT_LANG_CHANGE); return; } this._rebuildMap(jsonAsset.json, lang); this._langReady = true; console.log('[Lang] 加载成功', lang); GEvent.Ins.emit(GEvent.EVENT_LANG_CHANGE); } catch (e) { console.error('[Lang] loadLang 异常', e); if (requestId === this._loadRequestId) { this._langMap.clear(); this._langReady = false; } } } private _rebuildMap(raw: unknown, lang: LangCode): void { this._langMap.clear(); if (!Array.isArray(raw)) { console.warn('[Lang] 语言 JSON 应为数组'); return; } for (const item of raw) { if (!item || typeof item !== 'object') { continue; } const row = item as LangTableRow; if (typeof row.key !== 'string' || row.key === '') { continue; } const text = row[lang]; this._langMap.set(row.key, typeof text === 'string' ? text : ''); } } private async _loadBundle(bundleName: string): Promise { return new Promise((resolve) => { assetManager.loadBundle(bundleName, (err, bundle) => { if (err) { console.warn(`[Lang] 加载 Bundle:${bundleName}`, err); resolve(null); } else { resolve(bundle); } }); }); } private async _loadRes(bundle: AssetManager.Bundle, assetPath: string, type: typeof Asset): Promise { return new Promise((resolve) => { bundle.load(assetPath, type, (err, asset) => { if (err) { console.warn(`[Lang] 资源失败 ${bundle.name}/${assetPath}`, err); resolve(null); } else { resolve(asset); } }); }); } }