国际化与本地化
I18n(internationalization,国际化的缩写)是把程序设计为支持多种语言的方式。L10n(localization,本地化的缩写)是把文本翻译为用户语言的过程。Minecraft 使用 Component 来实现这两者。
Component
Component 是一段带有元数据的文本,其元数据包含诸如文本格式之类的信息。它可以用以下方式之一创建(以下都是 Component 接口中的静态方法):
| 方法 | 说明 |
|---|---|
empty | 创建一个空组件。 |
literal | 创建一个带有给定文本的组件,并直接显示该文本而不进行翻译。 |
nullToEmpty | 传入 null 时创建一个空组件,否则创建一个字面量组件。 |
translatable | 创建一个可翻译组件。给定的字符串随后会被解析为一个翻译键(见下文)。 |
keybind | 创建一个包含给定按键绑定(已翻译)显示名称的组件。 |
nbt | 创建一个表示给定路径处 NBT 的组件。 |
score | 创建一个包含记分板目标值的组件。 |
selector | 创建一个组件,其中包含给定实体选择器所选实体名称的列表。 |
object | 创建一个包含任意字体(例如图集精灵)的组件。 |
Component.translatable() 此外还有一个可变参数(vararg),接受字符串插值元素。它的工作方式类似于 Java 的 String#format,但始终使用 %s,而不是 %i、%d、%f 或任何其他格式说明符,并在需要时调用 #toString()。
每个 Component 都可以用 #getString() 解析。解析通常是惰性的,也就是说,服务端可以指定一个 Component 并把它发送给各客户端,然后各客户端各自解析这些 Component(不同语言可能得到不同的文本)。Minecraft 中许多地方也会直接接受 Component 并为你完成解析。
绝不要让服务端翻译 Component。始终把 Component 发送到客户端并在那里解析它们。
MutableComponent
所有构造出来的组件通常都是 MutableComponent。MutableComponent 提供了一些方法,用于添加新的同级组件(sibling,会被追加到该组件末尾)以及设置文本的样式。构造或修改组件时应始终针对 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!");
// Create a new style.
Style blue = Style.EMPTY.withColor(0x0000FF);
Style greenShadow = Style.EMPTY.withShadowColor(0xFF00FF00);
// Styles use a builder-like pattern.
Style blueItalic = Style.EMPTY.withColor(0x0000FF).withItalic(true);
// Besides italic, we can also make styles bold, underlined, strikethrough, or 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);
// Let's merge some styles together!
Style merged = blueItalic.applyTo(bold).applyTo(strikethrough);
// Set a style on a component.
text.setStyle(merged);
// Merge a new style into it.
text.withStyle(Style.EMPTY.withColor(0xFF0000));
另一种更精细的格式化方式是使用点击事件与悬停事件:
// We have a total of 8 options for a click event, and a total of 3 options for a hover event.
ClickEvent clickEvent;
HoverEvent hoverEvent;
// Opens the given URL in your default browser when clicked.
clickEvent = new ClickEvent.OpenUrl(URI.create("http://example.com/"));
// Opens the given file when clicked. For security reasons, this cannot be sent from a server.
clickEvent = new ClickEvent.OpenFile("C:/example.txt");
// Runs the given command when clicked.
clickEvent = new ClickEvent.RunCommand("/gamemode creative");
// Suggests the given command in the chat when clicked.
clickEvent = new ClickEvent.SuggestCommand("/gamemode creative");
// Changes a book page when clicked. Irrelevant outside of a book screen context.
clickEvent = new ClickEvent.ChangePage("1");
// Copies the given text to the clipboard.
clickEvent = new ClickEvent.CopyToClipboard("Hello World!");
// Opens a dialog screen (assume we have some Holder<Dialog> dialog)
clickEvent = new ClickEvent.ShowDialog(dialog);
// Sends an identifier and payload to the server (by default, logs the action and payload sent)
clickEvent = new ClickEvent.Custom(Identifier.fromNamespaceAndPath("examplemod", "custom"), Optional.empty());
// Shows the given component when hovered. May be formatted as well.
// Keep in mind that click or hover events won't work in a hover tooltip.
hoverEvent = new HoverEvent.ShowText(Component.literal("Hello World!"));
// Shows a complete tooltip of the given stack template when hovered.
hoverEvent = new HoverEvent.ShowItem(new ItemStackTemplate(...));
// Shows a complete tooltip of the given entity when hovered.
// See the possible constructors of EntityTooltipInfo.
hoverEvent = new HoverEvent.ShowEntity(new HoverEvent.EntityTooltipInfo(...));
// Apply the events to a style.
Style clickable = Style.EMPTY.withClickEvent(clickEvent);
Style hoverable = Style.EMPTY.withHoverEvent(hoverEvent);
甚至连组件所用的字体也可以设置:
// Set the font to be used by a style.
// Must match an available FontDescription.Resource (found in `assets/<namespace>/font/<path>.json`)
// Points to `assets/minecraft/font/illageralt.json`
Identifier fontLocation = Identifier.withDefaultNamespace("illageralt");
// Apply the font to a style
Style customFont = Style.EMPTY.withFont(new FontDescription.Resource(fontLocation));
在样式中使用 FontDescription.Resource 以外的字体描述类型时会抛出异常。若要使用其他或自定义的 FontDescription,请创建一个 ObjectInfo 并通过 ObjectContents 提供给组件。
语言文件
语言文件是 JSON 文件,包含从翻译键(见下文)到实际名称的映射。它们位于 assets/<modid>/lang/language_name.json。例如,mod id 为 examplemod 的 Mod 的美式英语翻译位于 assets/examplemod/lang/en_us.json。Minecraft 支持的完整语言列表可在此处找到。
语言文件通常看起来像这样:
{
"translation.key.1": "Translation 1",
"translation.key.2": "Translation 2"
}
翻译键
翻译键是翻译中使用的键。许多情况下,它们遵循 registry.modid.name 的格式。例如,一个 mod id 为 examplemod 的 Mod 提供了一个名为 example_block 的方块,那么它很可能希望为键 block.examplemod.example_block 提供翻译。不过,你基本上可以使用任意字符串作为翻译键。
如果某个翻译键在所选语言中没有对应的翻译,且美式英语(en_us)尚不是所选语言,游戏会回退到美式英语。如果美式英语也没有翻译,翻译将静默失败,转而显示原始的翻译键。
Minecraft 中有些地方为你提供了获取翻译键的辅助方法。例如,方块和物品都提供 #getDescriptionId 方法。对于物品,这些不仅可以查询,还可以在需要时通过 Item.Properties#overrideDescription 更改。如果物品根据其底层数据组件拥有不同的名称,可以通过在 ItemStack 上设置 CUSTOM_NAME 数据组件为所需的可翻译组件来覆盖这些名称。Item#getName 也有一个接受 ItemStack 参数的变体,用于设置物品的默认组件。而 BlockItem 则通过调用 Item.Properties#useBlockDescriptionPrefix 来设置 description id。Identifier 也提供了一种使用 toLanguageKey 构造翻译键的方式,它接收小节或注册表以及一个可选的后缀。
翻译键的唯一用途是本地化。不要把它们用于游戏逻辑,那是注册名的职责。
翻译 Mod 元数据
翻译文件可以使用以下键覆盖 Mod 信息的某些部分(其中 modid 应替换为实际的 mod id):
| 翻译键 | 覆盖内容 | |
|---|---|---|
| 描述 | fml.menu.mods.info.description.modid | 也可以在 [[mods]] 小节中放置一个名为 description 的字段。 |
数据生成
语言文件可以通过数据生成产生。为此,扩展 LanguageProvider 类,并在 addTranslations() 方法中添加你的翻译:
public class MyLanguageProvider extends LanguageProvider {
public MyLanguageProvider(PackOutput output) {
super(
// Provided by the `GatherDataEvent.Client`.
output,
// Your mod id.
"examplemod",
// The locale to use. You may use multiple language providers for different locales.
"en_us"
);
}
@Override
protected void addTranslations() {
// Adds a translation with the given key and the given value.
this.add("translation.key.1", "Translation 1");
// Helpers are available for various common object types. Most helpers have two variants: an add() variant
// for the object itself, and an addTypeHere() variant that accepts a supplier for the object.
// The different names for the supplier variants are required due to generic type erasure.
// All following examples assume the existence of the values as suppliers of the needed type.
// Adds a block translation.
this.add(MyBlocks.EXAMPLE_BLOCK.get(), "Example Block");
this.addBlock(MyBlocks.EXAMPLE_BLOCK, "Example Block");
// Adds an item translation.
this.add(MyItems.EXAMPLE_ITEM.get(), "Example Item");
this.addItem(MyItems.EXAMPLE_ITEM, "Example Item");
// Adds an item stack translation. This is mainly for items that have NBT-specific names.
this.add(MyItems.EXAMPLE_ITEM_STACK.get(), "Example Item");
this.addItemStack(MyItems.EXAMPLE_ITEM_STACK, "Example Item");
// Adds an entity type translation.
this.add(MyEntityTypes.EXAMPLE_ENTITY_TYPE.get(), "Example Entity");
this.addEntityType(MyEntityTypes.EXAMPLE_ENTITY_TYPE, "Example Entity");
// Adds a mob effect translation.
this.add(MyMobEffects.EXAMPLE_MOB_EFFECT.get(), "Example Effect");
this.addEffect(MyMobEffects.EXAMPLE_MOB_EFFECT, "Example Effect");
// Adds a tag key translation.
this.add(MyTags.EXAMPLE_TAG, "Example Tag");
// Adds a dimension translation.
this.addDimension(MyDimensions.EXAMPLE_DIMENSION_KEY, "Example Dimension");
// Adds a biome translation.
this.addBiome(MyBiomes.EXAMPLE_BIOME_KEY, "Example Biome");
}
}
然后,像注册任何其他提供器一样,在 GatherDataEvent.Client 中注册该提供器。
@SubscribeEvent // on the mod event bus
public static void onGatherData(GatherDataEvent.Client event) {
// Call event.createDatapackRegistryObjects(...) first if adding datapack objects
event.createProvider(MyLanguageProvider::new);
}