国际化(I18n)与本地化(L10n)
I18n(internationalization 的缩写,国际化)指的是将程序设计为支持多种语言的方式。L10n(localization 的缩写,本地化)则是将文本翻译为用户所用语言的过程。Minecraft 通过 Component(组件)来实现这些功能。
Component(组件)
Component(组件)是带有元数据的一段文本,元数据可以包含文本格式等信息。你可以通过以下方式之一来创建 Component(所有方法均为 Component 接口的静态方法):
| 方法 | 描述 |
|---|---|
empty | 创建一个空组件。 |
literal | 创建一个包含指定文本的组件,直接显示该文本,不进行翻译。 |
nullToEmpty | 如果参数为 null,则创建一个空组件,否则创建一个文本组件。 |
translatable | 创建一个可翻译组件。传入的字符串会被当作翻译键(见下文)进行解析。 |
keybind | 创建一个包含指定按键绑定(keybind)显示名称(已翻译)的组件。 |
nbt | 创建一个表示指 定路径下 NBT 的组件。 |
score | 创建一个包含记分板目标值的组件。 |
selector | 创建一个包含给定 实体 选择器(entity selector)所选实体名称列表的组件。 |
Component.translatable() 方法还带有一个可变参数,用于插入字符串。这类似于 Java 的 String#format,但始终使用 %s 而不是 %i、%d、%f 以及其他格式化符号,并在需要时调用 #toString()。
每个 Component 都可以通过 #getString() 进行解析。解析通常是惰性的,这意味着服务器可以指定一个 Component,将其发送给客户端,然后客户端各自解析该 Component(不同语言的客户端解析结果可能不同)。Minecraft 的许多地方也会直接接受 Component 并自动为你处理解析。
切勿让服务器端翻译 Component。应始终将 Component 发送到客户端,并在客户端进行解析。
MutableComponent(可变组件)
所有构造出的组件通常都是 MutableComponent(可变组件)。MutableComponent 提供了添加新的组件子节点(这些子节点会被追加到当前组件末尾)以及设置文本 样式 的方法。构建或修改组件时应始终使用 MutableComponent。
文本格式化
Component(组件)可以通过 Style(样式)进行格式化。Style 是不可变对象,每次修改都会创建一个新的 Style 实例,因此可以只创建一次并在需要时复用。
只能通过 MutableComponent(可变组件)的方法来设置样式,因此请确保你的 Component 是一个 MutableComponent。
通常可以使用 Style.EMPTY 作为基础来构建样式。多个 Style 可 以通过 Style#applyTo(Style other) 方法合并,该方法会返回一个新的 Style。新样式会优先采用调用 applyTo() 方法的 Style 的设置,如果该设置不存在,则采用作为参数传入的 Style 的对应设置。你可以像下面这样将 Style 应用于组件:
MutableComponent text = Component.literal("Hello World!");
// 创建一个新的样式,设置为蓝色。
Style blue = Style.EMPTY.withColor(0x0000FF);
// 样式采用类似构建器的模式。
Style blueItalic = Style.EMPTY.withColor(0x0000FF).withItalic(true);
// 除了斜体(italic),我们还可以让样式变为粗体(bold)、带下划线(underlined)、删除线(strikethrough)或乱码(obfuscated)。
Style bold = Style.EMPTY.withBold(true);
Style underlined = Style.EMPTY.withUnderlined(true);
Style strikethrough = Style.EMPTY.withStrikethrough(true);
Style obfuscated = Style.EMPTY.withObfuscated(true);
// 让我们合并几个样式试试!
Style merged = blueItalic.applyTo(bold).applyTo(strikethrough);
// 为组件设置样式。
text.setStyle(merged);
// 合并一个新的样式。
text.withStyle(Style.EMPTY.withColor(0xFF0000));
另一种更丰富的格式化方式是使用点击和悬停事件:
// 我们一共有 6 种点击事件(click event)选项,以及 3 种悬停事件(hover event)选项。
ClickEvent clickEvent;
HoverEvent hoverEvent;
// 点击时在默认浏览器中打开指定 URL。
clickEvent = new ClickEvent(ClickEvent.Action.OPEN_URL, "http://example.com/");
// 点击时打开指定文件。出于安全原因,该操作无法由服务器端发送。
clickEvent = new ClickEvent(ClickEvent.Action.OPEN_FILE, "C:/example.txt");
// 点击时执行指定命令。
clickEvent = new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/gamemode creative");
// 点击时在聊天栏中建议(填充)指定命令。
clickEvent = new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, "/gamemode creative");
// 点击时切换书页。仅在书本界面中有意义。
clickEvent = new ClickEvent(ClickEvent.Action.CHANGE_PAGE, "1");
// 点击时将指定文本复 制到剪贴板。
clickEvent = new ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, "Hello World!");
// 悬停时显示指定组件。也可以进行格式化。
// 注意:在悬浮提示框(hover tooltip)中,点击或悬停事件不会生效,这是显而易见的。
hoverEvent = new HoverEvent(HoverEvent.Action.SHOW_TEXT, Component.literal("Hello World!"));
// 悬停时显示指定物品堆(item stack)的完整提示信息。
// 参见 ItemStackInfo 的可能构造方法。
hoverEvent = new HoverEvent(HoverEvent.Action.SHOW_ITEM, new HoverEvent.ItemStackInfo(...));
// 悬停时显示指定实体(entity)的完整提示信息。
// 参见 EntityTooltipInfo 的可能构造方法。
hoverEvent = new HoverEvent(HoverEvent.Action.SHOW_ENTITY, new HoverEvent.EntityTooltipInfo(...));
// 将事件应用到样式(style)上。
Style clickable = Style.EMPTY.withClickEvent(clickEvent);
Style hoverable = Style.EMPTY.withHoverEvent(hoverEvent);
语言文件(Language Files)
语言文件是 JSON 文件,用于将翻译键(translation key,见下文)映射到实际名称。它们位于 assets/<modid>/lang/language_name.json 路径下。例如,mod id 为 examplemod 的模组的美式英语(US English)翻译文件路径为 assets/examplemod/lang/en_us.json。Minecraft 支持的所有语言列表可在 这里 查询。
一个典型的语言文件格式如下:
{
"translation.key.1": "Translation 1",
"translation.key.2": "Translation 2"
}
翻译键(Translation Keys)
翻译键(translation key)是用于翻译的键值。在许多情况下,它们遵循 registry.modid.name 的格式。例如,id 为 examplemod 的模组提供了一个名为 example_block 的方块(block),那么通常需要为 block.examplemod.example_block 这个键提供翻译。当然,你也可以使用几乎任何字符串作为翻译键。
如果某个翻译键(translation key)在当前选择的语言中没有对应的翻译,游戏会回退到美式英语(en_us),前提是当前语言不是美式英语。如果美式英语中也没有该翻译,则会静默失败,界面上将直接显示原始的翻译键。
Minecraft 的某些部分为你提供了获取翻译键的辅助方法。例如,方块(block)和物品(item)都提供了 #getDescriptionId 方法。对于物品,这些方法不仅可以用于查询,还可以在需要时通过 Item$Properties#overrideDescription 进行修改。如果物品的名称会根据其底层 数据组件 不同而变化,可以通过在 ItemStack 上设置带有期望可翻译组件的 CUSTOM_NAME 数据组件来覆盖名称。此外,Item#getName 还有一个变体方法,接受一个 ItemStack 参数,用于设置物品的默认组件。另一方面,BlockItem 会通过调用 Item$Properties#useBlockDescriptionPrefix 来设置描述 id。
翻译键(translation key)唯一的用途就是用于本地化(localization)。不要将它们用于游戏逻辑,这类用途应该使用 注册表名称(registry name)。
翻译 Mod 元数据(Translating Mod Metadata)
从 NeoForge 20.4.179 开始,翻译文件可以使用以下键覆盖部分 mod 信息(其中 modid 需要替换为实际的 mod id):
| 翻译键(Translation Key) | 覆盖方式(Overriding) | |
|---|---|---|
| 描述(Description) | fml.menu.mods.info.description.modid | 也可以在 [[mods]] 区块中直接添加一个名为 description 的字段。 |
数据生成(Datagen)
语言文件可以通过 数据生成(datagenned) 的方式创建。为此,你需要继承 LanguageProvider 类,并在 addTranslations() 方法中添加你的翻译内容:
public class MyLanguageProvider extends LanguageProvider {
public MyLanguageProvider(PackOutput output) {
super(
// 由 `GatherDataEvent.Client` 提供。
output,
// 你的模组 id。
"examplemod",
// 要使用的语言区域。你可以为不同的区域使用多个语言提供器。
"en_us"
);
}
@Override
protected void addTranslations() {
// 使用给定的键和值添加一条翻译。
this.add("translation.key.1", "Translation 1");
// 对于各种常见的对象类型,提供了辅助方法。每个辅助方法都有两种变体:add() 变体
// 用于对象本身,addTypeHere() 变体则接受一个对象的供应器(supplier)。
// 由于泛型类型擦除,不同的供应器变体需要不同的名称。
// 下列所有示例都假设已经存在所需类型的供应器变量。
// 添加一个方块(block)的翻译。
this.add(MyBlocks.EXAMPLE_BLOCK.get(), "Example Block");
this.addBlock(MyBlocks.EXAMPLE_BLOCK, "Example Block");
// 添加一个物品(item)的翻译。
this.add(MyItems.EXAMPLE_ITEM.get(), "Example Item");
this.addItem(MyItems.EXAMPLE_ITEM, "Example Item");
// 添加一个物品堆(item stack)的翻译。主要用于具有特定 NBT 名称的物品。
this.add(MyItems.EXAMPLE_ITEM_STACK.get(), "Example Item");
this.addItemStack(MyItems.EXAMPLE_ITEM_STACK, "Example Item");
// 添加一个实体类型(entity type)的翻译。
this.add(MyEntityTypes.EXAMPLE_ENTITY_TYPE.get(), "Example Entity");
this.addEntityType(MyEntityTypes.EXAMPLE_ENTITY_TYPE, "Example Entity");
// 添加一个附魔(enchantment)的翻译。
this.add(MyEnchantments.EXAMPLE_ENCHANTMENT.get(), "Example Enchantment");
this.addEnchantment(MyEnchantments.EXAMPLE_ENCHANTMENT, "Example Enchantment");
// 添加一个生物效果(mob effect)的翻译。
this.add(MyMobEffects.EXAMPLE_MOB_EFFECT.get(), "Example Effect");
this.addEffect(MyMobEffects.EXAMPLE_MOB_EFFECT, "Example Effect");
}
}
然后,像注册其他数据提供器一样,在 GatherDataEvent.Client 中注册该语言提供器。