消消方块阵换皮表情
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

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);
}
});
});
}
}