国际化与本地化

国际化与本地化

I18n 是 internationalization(国际化)的缩写,指让程序支持多种语言的设计方式。L10n 是 localization(本地化)的缩写,指将文字翻译为用户语言的过程。Minecraft 通过 Component 实现它们。

Component

Component 是包含格式等元数据的一段文本,可以通过 Component 接口的以下静态方法创建:

方法 说明
empty 创建空组件。
literal 直接显示给定文字,不进行翻译。
nullToEmpty 参数为 null 时创建空组件,否则创建字面文本组件。
translatable 创建可翻译组件,将给定字符串作为翻译键解析,见下文。
keybind 显示指定按键绑定的本地化名称。
nbt 显示给定路径的 NBT。
score 显示记分板目标值。
selector 显示实体选择器选中的实体名称列表。
object 显示图集精灵等任意字形内容。

Component.translatable() 还接受可变参数,用于字符串插值。它与 Java 的 String#format 类似,但无论是整数、小数还是其他格式,一律使用 %s,需要时调用 #toString()。

每个组件都可以通过 #getString() 解析。解析通常是惰性的:服务端指定组件后发给客户端,各客户端按自己的语言解析,因此可能显示不同文字。Minecraft 的许多位置也直接接受组件并负责解析。

注意 不要让服务端翻译组件。始终将组件发送到客户端,再在客户端解析。

MutableComponent

创建出的组件通常都是 MutableComponent。它提供追加同级组件(放在当前组件末尾)及设置文本样式的方法。构建或修改组件时应使用 MutableComponent。

文本格式

组件通过 Style 设置格式。样式不可变,修改会创建新对象,因此可以创建一次并按需复用。

提示 只有 MutableComponent 的方法可以设置样式,所以应先确认组件的类型。

通常以 Style.EMPTY 为基础。Style#applyTo(Style other) 可以合并多个样式并返回新样式:优先采用调用方中已设置的值,仅在某项缺失时使用参数样式的设置。应用方式如下:

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。例如 ID 为 examplemod 的 Mod 的美式英语文件是 assets/examplemod/lang/en_us.json。Minecraft 支持的完整语言列表见这里。

文件通常如下:

{
    "translation.key.1": "Translation 1",
    "translation.key.2": "Translation 2"
}

翻译键

翻译键是本地化使用的键,很多时候采用 registry.modid.name 格式。例如 examplemod 添加名为 example_block 的方块时,通常提供 block.examplemod.example_block 的翻译。不过,基本上任意字符串都可作为翻译键。

如果当前语言没有对应翻译,游戏会回退到美式英语 en_us,除非当前已选用该语言。若美式英语也没有翻译,游戏会静默失败,直接显示原始翻译键。

Minecraft 在一些位置提供获取翻译键的便捷方法。例如方块和物品都有 #getDescriptionId。物品的键还可以通过 Item.Properties#overrideDescription 修改。若名称依赖底层数据组件,可在 ItemStack 的 CUSTOM_NAME 组件中设置所需的可翻译组件。Item#getName 还有接受 ItemStack 参数的变体,用于设置物品的默认组件。BlockItem 则通过 Item.Properties#useBlockDescriptionPrefix 设置描述 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);
}

来源: NeoForged/Documentation(26.1)。中文翻译依据提交 4a680cdd4d15。