You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
255 lines
8.0 KiB
255 lines
8.0 KiB
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<string, unknown>;
|
|
|
|
/** 多语言 */
|
|
export class Lang extends Singleton {
|
|
private _current: LangCode = LangType.zh;
|
|
/** key -> 当前语言下的译文 */
|
|
private readonly _langMap = new Map<string, string>();
|
|
/** 最近一次成功构建 Map 后为 true */
|
|
private _langReady = false;
|
|
/** 递增,用于丢弃过期的异步加载结果 */
|
|
private _loadRequestId = 0;
|
|
/** init / 首次加载 的 Promise,便于外部等待 */
|
|
private _initPromise: Promise<void> | 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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<Record<LangCode, string>> = {
|
|
[LangType.zh]: '¥',
|
|
[LangType.hk]: 'HK$',
|
|
[LangType.en]: '$',
|
|
[LangType.ja]: '¥',
|
|
[LangType.ko]: '₩',
|
|
[LangType.ar]: '﷼',
|
|
};
|
|
return table[this._current] ?? '$';
|
|
}
|
|
|
|
/**
|
|
* 重新加载当前语言的 JSON 并刷新 Map(切换语言时已自动调用;也可手动刷新)。
|
|
*/
|
|
async loadLang(): Promise<void> {
|
|
const id = ++this._loadRequestId;
|
|
await this._loadForRequestId(id);
|
|
}
|
|
|
|
private async _loadForRequestId(requestId: number): Promise<void> {
|
|
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<AssetManager.Bundle | null> {
|
|
return new Promise<AssetManager.Bundle | null>((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<Asset | null> {
|
|
return new Promise<Asset | null>((resolve) => {
|
|
bundle.load(assetPath, type, (err, asset) => {
|
|
if (err) {
|
|
console.warn(`[Lang] 资源失败 ${bundle.name}/${assetPath}`, err);
|
|
resolve(null);
|
|
} else {
|
|
resolve(asset);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|