小白也能写框架之【九、组件篇:多语言】

接之前的教程:
小白也能写框架之【零、框架实际应用演示】 - Creator 3.x - Cocos中文社区
小白也能写框架之【一、新建框架工程】 - Creator 3.x - Cocos中文社区
小白也能写框架之【二、带颜色的日志管理器】 - Creator 3.x - Cocos中文社区
小白也能写框架之【三、带进度的分包管理器】 - Creator 3.x - Cocos中文社区
小白也能写框架之【四、带加密的数据管理器】 - Creator 3.x - Cocos中文社区
小白也能写框架之【五、资源管理器】 - Creator 3.x - Cocos中文社区
小白也能写框架之【六、音频管理器】 - Creator 3.x - Cocos中文社区
小白也能写框架之【七、事件管理器】 - Creator 3.x - Cocos中文社区
小白也能写框架之【八、任务管理器】 - Creator 3.x - Cocos中文社区

今天给大家分享常用的组件封装【文本和精灵实现多语言】
借鉴了商店里的多语言组件,结合【五、资源管理器】和【七、事件管理器】实现

一、上主菜:

1、文件代码一:assets\Core\Scripts\Components\I18n\I18nBase.ts

import { _decorator, Component } from 'cc';

import { eventMgr } from '../../Managers/EventMgr';

const { ccclass } = _decorator;

/** 多语言抽象组件基类 */

@ccclass('I18nBase')

export abstract class I18nBase extends Component {

    /** 组件加载时调用,注册语言更改事件 */

    onLoad(): void {

        eventMgr.on('langChange', this.refresh, this);

        this.refresh();

    }

    /** 组件销毁时调用,注销语言更改事件 */

    onDestroy(): void {

        eventMgr.off('langChange', this.refresh);

    }

    /** 刷新多语言,子类需实现具体逻辑 */

    public abstract refresh(): void;

}

2、文件代码二:assets\Core\Scripts\Components\I18n\I18nLabel.ts

import { _decorator, Label, RichText } from "cc";

import { EDITOR } from "cc/env";

import { I18nBase } from "./I18nBase";

import { logMgr } from "../../Managers/LogMgr";

import { langMgr } from "../../Managers/LangMgr";

const { ccclass, property, requireComponent } = _decorator;

/** 用于显示多语言文本,并根据语言变化自动更新内容 */

@ccclass("I18nLabel")

@requireComponent(Label)

@requireComponent(RichText)

export class I18nLabel extends I18nBase {

    @property({ displayName: "多语言 key" })

    private code: string = "";

    /** 多语言标签组件变量 */

    private lbl: Label | RichText | null = null;

    /** 获取多语言 key */

    public get key(): string {

        return this.code;

    }

    /** 设置多语言 key 并刷新内容 */

    public set key(value: string) {

        this.code = value;

        this.refresh();

    }

    /** 刷新文本内容,根据当前语言设置更新组件的 string 属性 */

    public refresh(): void {

        if (EDITOR) return; // 编辑器模式下不刷新

        if (!this.lbl) {

            this.lbl = this.getComponent(Label) || this.getComponent(RichText);

            if (!this.lbl) {

                logMgr.err("未找到 Label 或 RichText 组件。");

                return;

            }

        }

        this.lbl.string = langMgr.getLanguage(this.key);

    }

}

3、文件代码三:assets\Core\Scripts\Components\I18n\I18nSprite.ts

import { _decorator, Sprite, SpriteFrame } from "cc";

import { EDITOR } from "cc/env";

import { I18nBase } from "./I18nBase";

import { logMgr } from "../../Managers/LogMgr";

import { langMgr } from "../../Managers/LangMgr";

const { ccclass, property, requireComponent } = _decorator;

/** 存储语言代码与对应的精灵帧 */

@ccclass("I18nSpriteData")

export class I18nSpriteData {

    @property({ displayName: "语言代码", tooltip: "如:en、zh等" })

    langCode: string = "";

    @property({ displayName: "对应精灵", type: SpriteFrame })

    spriteFrame: SpriteFrame | null = null;

}

/** 根据当前语言自动更新精灵帧 */

@ccclass("I18nSprite")

@requireComponent(Sprite)

export class I18nSprite extends I18nBase {

    @property({ displayName: "多语言精灵数据列表", type: [I18nSpriteData] })

    public spList: I18nSpriteData[] = [];

    /** 多语言精灵组件变量 */

    private sp: Sprite | null = null;

    /** 刷新精灵帧,根据当前语言设置更新 Sprite 的 spriteFrame 属性 */

    public refresh(): void {

        if (EDITOR) return; // 编辑器模式下不刷新

        if (!this.sp) {

            this.sp = this.getComponent(Sprite);

            if (!this.sp) {

                logMgr.err("未找到 Sprite 组件。");

                return;

            }

        }

        const spriteData = this.spList.find(data => data.langCode === langMgr.lang);

        if (spriteData?.spriteFrame) {

            this.sp.spriteFrame = spriteData.spriteFrame;

        } else {

            logMgr.err(`未找到语言代码为 ${langMgr.lang} 的精灵帧。`);

        }

    }

}

4、文件代码四:assets\Core\Scripts\Managers\LangMgr.ts

import { JsonAsset, director, Director } from "cc";

import { EDITOR } from "cc/env";

import { dataMgr } from "./DataMgr";

import { resMgr } from "./ResMgr";

import { eventMgr } from "./EventMgr";

/**

 * 语言管理器

 * 提供语言本地化:加载语言包、更改语言设置、获取翻译文本或精灵。

 */

class LangMgr {

    /** 记录已加载的分包及其语言 */

    private loadedBundles: Record<string, Set<string>> = {};

    /** 当前选择的语言 */

    private currLang: string;

    /** 缓存的语言数据 */

    private langData: Record<string, Record<string, string>> = {};

    /** 私有构造函数,确保外部无法直接通过new创建实例 */

    private constructor() {

        if (!EDITOR) {

            director.once(Director.EVENT_AFTER_SCENE_LAUNCH, this.init, this);

        }

    }

    /** 单例实例 */

    public static readonly instance: LangMgr = new LangMgr();

    /** 初始化多语言 */

    private async init(): Promise<void> {

        this.currLang = dataMgr.getText("language") || "zh";

        await this.changeLang(this.currLang);

    }

    /** 获取当前语言 */

    public get lang(): string {

        return this.currLang;

    }

    /** 更改当前语言,并加载必要的语言包 */

    public async changeLang(langCode: string): Promise<void> {

        if (this.currLang === langCode) return;

        this.currLang = langCode;

        await Promise.all(

            Object.keys(this.loadedBundles).map(bundleName =>

                this.loadLanguageData(bundleName, langCode)

            )

        );

        dataMgr.setData("language", langCode);

        eventMgr.emit("langChange");

    }

    /** 异步加载指定分包的指定语言数据 */

    public async loadLanguageData(bundleName: string, langCode: string = this.currLang): Promise<void> {

        const loadedLanguages = this.loadedBundles[bundleName] || new Set<string>();

        if (loadedLanguages.has(langCode)) return;

        const path = `${bundleName}/Res/Lang/Lable/${langCode}`;

        const langAsset = await resMgr.loadRes<JsonAsset>(path);

        this.langData[langCode] = { ...this.langData[langCode], ...langAsset.json };

        loadedLanguages.add(langCode);

        this.loadedBundles[bundleName] = loadedLanguages;

    }

    /** 根据键获取对应的翻译文本 */

    public getLanguage(key: string, def: string = ""): string {

        return this.langData[this.currLang]?.[key] ?? def;

    }

}

/** 导出单例实例 */

export const langMgr = LangMgr.instance;

二、结构预览

三、代码示例
1、先建立一个分包,如下图:

2、给分包添加UI,如下图:
先,绑定Game.ts到预制体根节点

然后,依次添加组件,如下图:


3、说明:上面过程中,多语言key和精灵来自分包里的资源,看图:

4、模拟一个游戏设置里,切换语言的功能,打开附加到Game预制体的Game.ts

import { _decorator, Component, Node, Button } from 'cc';

const { ccclass, property } = _decorator;

@ccclass('Game')

export class Game extends Component {

    /** 切换语言按钮 */

    private btn: Node = null;

    private lang: string = "zh";

    /** 加载 */

    onLoad() {

        this.btn = this.node.getChildByName('Button');

        if (this.btn) {

            this.btn.on(Button.EventType.CLICK, this.onButtonClick, this);

        }

    }

    /** 按钮点击事件处理 */

    private onButtonClick() {

        if (this.lang == 'zh') {

            this.lang = 'en';

        } else {

            this.lang = 'zh';

        }

        app.lang.changeLang(this.lang);

    }

}

5、最后就是主场景对多语言的调用,看图:

四、效果展示:

五、简单说明:
1、支持Label、RichText、SpriteFrame的多语言
2、通过事件触发多语言的切换,立即生效,无需重启
3、每个分包都有自己独立的多语言资源文件,实现单独更新,减轻更新量
4、支持静态拖拽式多语言(示例中就是静态的),也支持动态调用app.lang.getLanguage(‘key’),一般Tip多语言提示消息使用

3赞