正在载入

等待时间过长时请刷新页面

编写于

最近更新

语言引用

本文介绍语言对象中的 LangTextLangListLangMap。配置中的引用写法见在文件中引用 ⇱,配置对象中的文本类型见配置文本 ⇱

LangText 引用语言文件中的一个文本字段,保存字段路径,并在使用时向语言服务查询当前值。与直接保存 String 不同,保存 LangText 后仍能读取重载后的文本。列表和映射也有对应的引用类型:LangListLangMap

如果命令、GUI 等组件需要长期保存语言字段,并随语言重载更新文本,建议使用语言引用。

定义引用字段

将字段类型改为 LangText,并通过 LangText.of(...) 提供默认值:

import api.linlang.file.file.LangText;
import api.linlang.file.file.annotations.LangPack;

@LangPack(filePath = "main")
public class MainLanguage {
    public Command command = new Command();

    public static class Command {
        public AddLore addLore = new AddLore();
    }

    public static class AddLore {
        // 引用字段需要通过 of 声明默认值。
        public LangText description = LangText.of("添加新的行");
        public LangText line = LangText.of("添加的描述");
    }
}

语言资源和生成文件的格式不变:

command:
  add-lore:
    description: "添加新的行"
    line: "添加的描述"

绑定完成后,这两个字段的路径分别为 command.add-lore.descriptioncommand.add-lore.line。路径遵循语言对象已有的 @Key@NamingStyle 规则,不需要在 LangText.of(...) 中重复填写。

注意,LangText.of(...) 返回的是尚未绑定的声明对象。语言服务绑定语言类时,会把字段替换为受管理引用。因此,应先通过 LangService.bind(...) 取得语言对象,不要直接构造语言类并把其中的引用交给其他模块。

解析文本

读取当前全局语言:

String description = lang.command.addLore.description.resolve();

显式读取指定 locale:

String description = lang.command.addLore.description.resolve("en_GB");

查找指定 locale 时,语言服务仍然应用语言包的归一化、默认语言和字段默认值规则。没有可用文件值时,返回 LangText.of(...) 声明的文本。

不要依赖字符串拼接隐式调用 toString()。需要进行替换、格式化或交给只接受 String 的 API 时,应先显式调用 resolve(),使取值时机在代码中保持清楚。

与 String 的区别

普通字段在绑定和重载时由语言服务直接写入:

public String reloaded = "重载完成";

读取后得到的是当时的字符串快照:

String message = lang.reloaded;

即使语言对象中的 reloaded 字段后来被刷新,局部变量 message 也不会变化。

引用字段则声明为:

public LangText reloaded = LangText.of("重载完成");

绑定后取得的引用可以被长期保存:

LangText message = lang.reloaded;

同一个语言服务执行 reload() 或切换活动 locale 后,message.resolve() 会从新的语言快照中读取文本。语言服务不会在普通重载时替换已经安装的 LangText 对象。

列表与映射引用

LinFile 还提供了 ListMap 的引用版本,您可以使用 LangListLangMap

import api.linlang.file.file.LangList;
import api.linlang.file.file.LangMap;

@LangPack(filePath = "main")
public class MainLanguage {
    public LangList tips = LangList.of("默认提示");
    public LangMap buttons = LangMap.of();
}

对应语言文件仍然使用原生列表和映射结构:

tips:
  - "第一条提示"
  - "第二条提示"
buttons:
  confirm: "确认"
  cancel: "取消"

解析当前语言下的不可修改集合快照:

List<String> tips = lang.tips.resolve();
Map<String, String> buttons = lang.buttons.resolve();

也可以显式指定 locale:

List<String> englishTips = lang.tips.resolve("en_GB");
String confirm = lang.buttons.resolve("en_GB").get("confirm");

应长期保存 LangListLangMap 引用,而不是保存某次 resolve() 返回的集合。每次解析得到的列表和映射都是不可修改的快照,调用者不能通过 addput 等方法修改语言服务中的数据。

LangList tipsRef = lang.tips;

langService.reload();

List<String> latestTips = tipsRef.resolve();

LangMap 的值必须是字符串、数字、布尔值等可以转换为字符串的标量。嵌套 Map 或嵌套 List 不属于 LangMap 的数据模型;字段结构不符合要求时,解析结果回退到代码声明的默认值。

非空的 LangMap.of(...) 默认映射会参与缺失键补齐,适合键集合固定的场景:

public LangMap buttons = LangMap.of(
        "confirm", "确认",
        "cancel", "取消"
);

如果 Map 的键由语言文件或用户自由定义,应使用空默认映射,避免默认键被自动补入:

public LangMap customMessages = LangMap.of();

LangListLangMap 表示集合整体的引用。请不要声明 List<LangText>Map<String, LangText> 或包含引用对象的嵌套集合;这些结构不支持上述集合引用的重载行为。

三种引用字段都可以放在带有 @LangPack 的语言对象及其嵌套对象中。资源目录、文件格式、默认语言、归一化和写回规则仍由语言对象类上的 @LangPack 统一决定。

命令国际化

命令服务会长期保存命令描述和参数标签,适合直接传入语言引用:

registry.register(
        "re + <line:string{.+}>",
        ctx -> {
            Player player = (Player) ctx.sender();
            String lore = normalize(ctx.get("line"));
            itemEditor.addLore(player, lore);
        },
        CommandOptions.options()
                .permission("rainbowedit.lore")
                .player()
                .i18n(
                        lang.command.addLore.description,
                        "line", lang.command.addLore.line
                )
);

命令规范中的 line 仍然是参数解析和 ctx.get("line") 使用的稳定名称。语言引用只改变帮助与用法信息中的显示文本,不会令不同语言使用不同的命令参数名。

使用 CommandOptions 后不需要在参数后添加 @i18n。命令服务发现与参数名匹配的标签引用时会优先使用引用;引用不存在或解析为空时,再使用内联说明,最后回退到参数名。

原有的 DescLabelsregisterLazy@i18n 写法仍然可用。

与消息服务配合

LangText 同时实现了高级字符串使用的 TextSource 接口,可以直接交给 Messenger,不需要先调用 resolve()

messenger.send(
        player,
        lang.command.success,
        "item", itemName
);

Messenger 会在每次发送时解析语言引用,并把语言文件中的内容作为高级字符串处理。普通语言重载后,使用同一个引用即可发送新的文本。

作为变量传入时,LangTextLinText 会被视为可渲染的高级文本;String 与其他普通对象会被转义后作为普通文本插入。这可以避免玩家名称、命令参数等外部输入意外生成点击行为。

旧版 sendKey() 通过路径字符串查找语言的方式已经弃用。新代码应保存并传递语言对象中的字段引用,使语言结构重命名能够在 Java 编译阶段被发现。

消息发送、动作栏、标题和完整投递策略请见:消息服务 ⇱。描述符格式请见:高级字符串 ⇱

实现

flowchart TD
    Define["LangText、LangList 或 LangMap"] --> Bind["bind(LanguageClass)"]
    Bind --> Path["按字段结构确定路径"]
    Path --> Reference["安装受管理引用"]
    Bind --> Snapshot["读取并校验语言文件"]
    Reload["reload 或 locale 变化"] --> Snapshot
    Snapshot --> Swap["替换语言包文本快照"]
    Reference --> Resolve["resolve"]
    Swap --> Resolve

语言文件会先被完整读取为新的文本快照,再交给引用解析。已经保存的引用不需要逐个接收更新,也不会持有注册命令时的旧字符串。

重载与关闭

LangService.reload() 属于同一语言服务内的普通重载。此时语言对象和已经取得的语言引用都可以继续使用。

lin.settings().apply() 原地切换语言,不替换服务,已有语言引用和命令注册仍然有效。关闭 Facade 后,应停止使用它的服务及引用;重新初始化插件时再绑定新对象。

语言引用字段也不应被业务代码重新赋值。重新赋值只会改变语言对象中的字段,不会改变已经交给命令或 GUI 的旧引用。需要修改翻译时,应修改语言文件后调用 reload()

讨论

请登录账号