未发布版本 v6.3.0-SNAPSHOT。 本页内容来自 alpha 分支,随时可能变更,不属于任何已发布版本。

commit 0ed0d39 · 注入于 2026-10-06 00:56 UTC

Skip to content

I18n 多语言 ​

UltiTools 提供了一个易用的多语言 API,让你可以轻松的为你的插件添加多语言支持。

创建语言文件 ​

在 resources 文件夹中创建一个 lang 文件夹。按照你的需求放入你的插件语言文件。

json
{
  "test": "测试",
  "test2": "测试2"
}

将文件名命名为 zh.json,其中 zh 为语言代码。

语言代码可参照此表。

YAML 语言文件 ​

也支持 .yml/.yaml 字典

v6.3.0 起,模块的 lang 目录也可以使用 .yml/.yaml 而不是 .json——Language.fromYaml 会把嵌套键用点号展开,且分区节点本身不会变成字符串条目。

只提供 .yml 的模块仍可能加载不到自己的字典

每个内部模块与核心 UltiTools 插件共用同一个类加载器,所以对 jar 内 lang/<语言码>.json 的查找可能先解析到核心自己的 .json 文件,.yml 永远不会被尝试,i18n(...) 因此只会返回原始 key(issue #412)——在这个问题解决之前,请在 .yml 旁再放一份 lang/<语言码>.json(哪怕内容近似)作为临时办法。

基于来源记录的文件刷新 ​

v6.3.0 起,未改动过的语言文件会自动刷新

saveResources() 会记录每个提取出的 lang/<语言码><扩展名> 文件的 SHA-256。下次启动时,若某个 文件记录的哈希值仍与磁盘上的字节匹配,就会被替换成当前 jar 中的版本,并输出一行 INFO 日志说明该文件。

v6.3.0 起,记录之后被直接改动过的文件同样会被恢复(见下文官方文件与自定义语言文件)。(从未记录过 哈希的文件是另一种情况,见下文升级到 6.3.0:没有记录的文件。)只有在无法写回时——文件只读或是符号链接—— 改过的文件才会保留,此时只有确实丢失了占位符的具体键才会改用 jar 的值,并各输出一次 WARN 日志,注明模 块、文件与键名;其余键仍保留改过的措辞。由于模块字典可能使用两种占位符风格中的任意一种,两者都会被检查:

  • String.format 占位符(%s、%d,或显式的 %1$s 索引)——只要你的键与 jar 中键的占位符数 量不同(无论增多还是减少),你的键就会被覆盖。
  • {0}/{PLAYER} 风格的花括号占位符(v6.3.0 起)——通过普通的 String#replace 替换,而非 Formatter。只有当 jar 的值包含一个你的值完全没有的占位符时,你的键才会被覆盖;你自己额外添加的花 括号占位符,或对已有占位符周围文字的任何改写,都不会被触碰。这个检查刻意设计为单向的:它只标记消失 的占位符,绝不会标记你自己做的定制 (issue #524)。

要强制刷新一个已自定义的文件,删除它并重启该模块:框架会重新从 jar 中提取该文件,并记录新的基准哈希。v6.3.0 起,模块在这次提取之后才选择语言文件,所以即使旁边还留着旧的 lang/<语言码>.yml,这次启动用的也是刚提取的新文件 (issue #540)。

来源记录的写入只在框架接受该模块之后进行。加载时被拒绝的副本(已加载模块的旧版副本,或为更新版本框架构建的模块) 不会刷新、替换或记录任何文件(issue #460)。

升级到 6.3.0:没有记录的文件 ​

升级后第一次启动时,没有记录的语言文件会被替换,改过的也一样

6.3.0 之前提取的文件没有哈希记录,框架分不清你的修改和旧版 jar 的原文。 升级后第一次启动时,这样的文件只要与新 jar 自带的版本不同,就会被替换为新版,旧文件保留为备份。 维护者于 2026-09-29 书面接受:改过的文件也会被替换。

每个没有哈希记录的 lang/<语言码><扩展名> 文件的处理 (issue #459):

  • 与 jar 自带版本不同: 替换为 jar 的版本并记录。旧文件保留在同一个 lang/ 目录下,名为 <文件名>.bak (例如 lang/en.json.bak);该名字已被占用时依次为 <文件名>.1.bak、<文件名>.2.bak……绝不覆盖已有文件, 备份名也不会以 .json、.yml 或 .yaml 结尾,因此备份永远不会被当作语言文件读取。每个文件输出一行 WARNING, 写明文件和备份。
  • 与 jar 自带版本逐字节相同: 只记录,不备份,不输出日志。
  • 无法替换(只读或符号链接文件、目录不可写、记录写不进去):保持原样,不记录,不留下备份;上文的占位符检查仍然生效。

第二次启动不会再有任何变化。请不要把备份改回原名:v6.3.0 起官方文件每次启动都会被恢复,改回原名后会再次被替换。 要保留修改,请把备份复制为自定义语言文件(见下一节)。框架不使用历次发布文件的指纹清单来区分改过的文件。

字面 % 必须写成 %%

语言字典的值会经过 java.util.Formatter。 裸 % 后面紧跟 s、d 或数字会被当作真正的转换符——String.format("Save 90%discount", 5) 返回 "Save 905iscount",参数缺失时则抛出 MissingFormatArgumentException。 如需输出字面上的百分号,请写成 %%。

你自己添加的花括号占位符永远不会被标记

{0}/{PLAYER} 检查只沿着"从 jar 的值到你的值"这一个方向比较——你在自己译文里加的类似 {备注} 这样的花括号,无论怎么写,都不会被误认成丢失的占位符。

官方文件与自定义语言文件 ​

官方语言文件归 UltiTools 所有

v6.3.0 起,请修改副本,不要修改官方文件:直接修改的官方语言文件每次启动都会被恢复。

官方语言文件指框架和各模块写出的 lang/* 文件:框架自己的 en.json、zh.json 位于 plugins/UltiTools/lang/(v6.3.0 起写到这里),各模块的位于 plugins/UltiTools/pluginConfig/<模块>/lang/。每次启动时,字节与自带版本不同的官方文件会被恢复为自带版本,原文件保留为 <文件名>.bak(命名规则同上),并输出一行 WARNING 写明两者,提示服主改用下面的方法自定义。/ul reload 对框架的两个文件和各模块当前语言的文件做同样处理。由旧版本写出、无人修改过的文件会直接更新,不留备份;只读或符号链接文件保持原样,并输出一行说明(issue #608)。

服主自定义文本的方法:

  1. 在要修改的目录里,把官方文件复制为以语言代码加连字符开头的新名称,保留扩展名——zh.json → zh-myserver.json;
  2. 修改这个副本;
  3. 在 plugins/UltiTools/config.yml 中设置 language: zh-myserver,然后重启或执行 /ul reload。

这一个设置同时为框架和所有模块选择语言,没有按模块的设置。副本中没有的文本使用名称开头对应的官方语言(zh-myserver 对应 zh;多个语言代码都匹配时取最长的,所以模块自带 zh-CN 时,zh-CN-myserver 对应 zh-CN),因此副本只需保留要改的条目;没有 zh-myserver 文件的模块直接使用官方 zh。名称不以已有语言代码开头(如 myserver)时,缺少的文本使用英文,并输出一行警告;含有 ASCII 字母、数字、_、- 以外字符的名称永远不会被当作文件名。副本中 %s/%d 个数与官方基础语言自带文本不同、或缺少其中某个 {TOKEN} 的条目(自己添加的占位符会保留),会在内存中改用自带文本,并输出一行警告写明文件和键——即原先直接修改官方文件时得到的同一项保护——文件本身不会被修改。请使用模块不会自带的名称(如 zh-myserver):不自带所配置官方语言的模块会把它当作自定义名称,日后版本开始自带的语言代码会让该文件变成官方文件。自定义文件在任何启动、重载、升级或模块更新中都不会被写入、替换、备份或登记。

模块作者:getLanguageCode() 与 getConfiguredLanguage() ​

i18n(...) 已经会先查自定义文件、再查其下的官方语言,模块无需为自定义语言文件写任何代码。与语言相关的行为——往配置文件里写哪种语言的出厂文本、创建哪种语言的示例内容——应使用 getLanguageCode():v6.3.0 起它返回文本所基于的官方语言,language: zh-myserver 时返回 zh,配置了模块没有的名称时返回模块实际使用的回退语言。配置的名称本身由 v6.3.0 新增的 getConfiguredLanguage() 返回。v6.3.0 之前 getLanguageCode() 返回配置的名称,对所有官方语言代码两者相同。

java
String code = getLanguageCode();             // language: zh-myserver 时为 "zh"
String configured = getConfiguredLanguage(); // "zh-myserver"

请把每种语言打包为 lang/<语言码>.<扩展名>:这些语言码就是自定义名称可以基于的官方语言。

注册语言代码 ​

实际使用的语言由 UltiTools 主插件配置决定

实际使用的语言取自 UltiTools 主插件配置中的 language 键,由 getConfiguredLanguage() 返回;getLanguageCode() 返回它所基于的官方语言。v6.3.0 起,构造该语言之前会先查询 supported():配置的语言码不受支持时会输出警告,并退回一个确实存在的语言,而不再静默加载空字典。

supported() 的默认实现取自模块自身的 lang/*.json 文件,因此只要把语言文件命名为 lang/<语言码>.json 就无需重写它。重写它依然有效并优先生效,适用于模块想声明支持某个语言码、却没有对应文件的少数情形。

UltiTools 需要知道你的插件支持哪些语言,因此你需要在你的插件继承了 UltiToolsPlugin 的类进行注册

一种很简便的方法就是在继承了 UltiToolsPlugin 的类添加 @I18n 并添加语言代码:

java
@I18n({"zh", "en"})

当然你也可以重写 supported() 方法,返回一个含有语言代码的 List<String> 即可:

java
@Override
public List<String> supported() {
    return Arrays.asList("zh", "en");
}

使用多语言 ​

在你的插件继承了 UltiToolsPlugin 的类中,有一个 i18n 方法,用于获取多语言字符串。

java
String test = i18n("test");

// 输出:测试

如果语言文件中不存在该字符串,将会返回该字符串本身。

java
String test3 = i18n("test3");

// 输出:test3

TIP

在仅有两种语言的情况下,你可以仅创建一个语言文件,其中的键值对的键为原文,值为译文。

框架自身的消息 ​

自 v6.3.0 起,UltiTools 自己显示的文字遵循主插件配置中的 language 键,沿用上文的语言文件约定:键是中文原文,lang/zh.json 把它映射到自身,lang/en.json 给出英文。这包括模块的命令体抛出异常时每个命令发送者收到的回复(language: en 时为 Command execution failed: <reason>)、@AsyncCommand 默认的处理中提示(language: en 时为 Processing...)、框架自己的控制台输出,以及它推送到面板实时日志的行。v6.3.0 之前这些文字始终是中文;language: zh 时文字不变。v6.3.0 起,框架的官方语言文件也会写到 plugins/UltiTools/lang/,自定义语言名称会像模块一样选择那里的副本(见官方文件与自定义语言文件);官方文本在所有平台上都按 UTF-8 读取。

框架发送的验证邮件保持中英双语,每一行中文都配有对应的英文,因为收件人的语言不一定是服务器的语言。

贡献者

暂无相关贡献者

基于 MIT 许可发布