Skip to content
Kz edited this page Oct 17, 2026 · 10 revisions

Lang 模块

把玩家可见文本放进可重载的 YAML 语言文件,统一处理 MiniMessage / 旧式颜色、安全命名占位符,以及聊天栏、Action Bar、Title、Boss Bar 的发送。

模块名 制品 自动带入
lang me.kzheart.klib:klib-lang core、config

适合:

  • 把玩家可见文本移出业务代码,让服主可以修改;
  • 同时兼容 Java 8 / Spigot 1.12.2 与较新服务端的富文本发送;
  • 统一处理前缀、命名占位符和可选的外部占位符;
  • 让命令错误、帮助与业务消息共享同一套语言文件;
  • 按消息选择聊天栏、Action Bar、Title 或 Boss Bar。

快速开始

klib { modules { lang() } }

依赖、直接引用制品和打包方式见 构建与打包。手动打包时需包含 Klib、Kotlin/协程、配置与 MiniMessage 运行时,并按构建页处理重定位。

在资源目录提供语言文件:

# src/main/resources/lang/zh_CN.yml
messages:
  common:
    prefix: "<gray>[<gold>MyPlugin</gold>]</gray> "
  welcome: "{prefix}<green>欢迎 {player}!</green>"
  reward: "actionbar: <yellow>获得 {amount} 枚金币</yellow>"
  announcement:
    - "<gold>服务器公告</gold>"
    - "<gray>祝你游戏愉快</gray>"

安装运行时并发送消息:

@Override
protected void setup() {
    Scope root = context().scope();
    LangRuntime lang = LangModule.install(
            root,
            getServer(),
            getDataFolder().toPath(),
            getClassLoader(),
            "zh_CN",
            null);

    root.on(PlayerJoinEvent.class, event -> lang.send(
            event.getPlayer(),
            "welcome",
            Placeholders.of("player", event.getPlayer().getName())));
}
  • 首次启动把资源文件提取到插件数据目录的 lang/zh_CN.yml。
  • 之后通过 lang.configDocument().reload() 或文件监听更新消息,无需重建 LangRuntime。
  • 最后一个参数是外部占位符适配器 PlaceholderApi,不需要时传 null(见 占位符)。

install 还会把运行时注册为作用域能力,各处不必传递引用:

LangRuntime lang = root.requireCapability(LangRuntime.class);
  • 能力遵循作用域的常规查找规则,子作用域可以直接取到父作用域安装的运行时。
  • 同一作用域安装多个地区实例时,能力指向首个安装的运行时,其余实例仍通过返回值使用。

语言文件与回退

业务消息放在根节点 messages 下,嵌套配置节展开为点分键:

messages:
  arena:
    joined: "<green>已加入竞技场</green>"

对应键为 arena.joined。

情况 结果
缺少 messages 根节点 自定义消息目录为空,继续回退到内置消息
messages 存在但类型错误 加载失败
地区名写法不规范 自动规范化,如 zh-cn → zh_CN
请求的地区资源不存在 整个运行时回退到内置默认地区 zh_CN,只创建 lang/zh_CN.yml,不会把中文默认内容写进不存在的其他地区文件;通过 java.util.logging 记一条 WARNING,写明请求的地区和实际回退的地区
用户文件缺少 Klib 内置消息键 回退到内置目录
业务自定义键没有回退值 渲染为红色的 [missing:消息键],并对该键记录一次警告

地区回退的 WARNING 用来发现拼错或未打包的语言资源。

消息格式

MiniMessage 与旧式颜色

字符串消息支持 MiniMessage,并兼容常用 & 颜色和格式码:

messages:
  modern: "<gradient:#ff0000:#0000ff><bold>渐变标题</bold></gradient>"
  legacy: "&a旧式绿色 &l粗体"
  clickable: "<click:run_command:/shop><hover:show_text:'打开商店'>点击打开商店</hover></click>"
接收者 / 环境 表现
较旧服务端 无法表达的 RGB 色降级为最接近的传统颜色
玩家聊天、Action Bar 优先尝试富组件发送,失败时回退到 legacy 文本
控制台和非玩家接收者 始终收到无颜色、无点击、无悬停元数据的纯文本

多行消息

YAML 字符串列表以换行连接:

messages:
  rules:
    - "第一条规则"
    - "第二条规则"

结构化富文本条目

用 YAML 字段描述单段富文本时,写一个含字符串 text 的配置节:

messages:
  docs:
    text: "查看项目文档"
    color: gold
    bold: true
    hover: "点击打开"
    click:
      type: open_url
      value: "https://example.com/docs"
  • 结构化条目适合单一文本片段;多段嵌套样式优先用 MiniMessage。
  • 只要某个配置节有字符串 text 子项,就会被当成结构化富文本,而不是普通嵌套命名空间。

占位符

命名占位符写作 {name}:

lang.send(
        player,
        "reward",
        Placeholders.of(
                "amount", Integer.valueOf(10),
                "reason", "daily"));
  • Placeholders.of 接受成对的“键、值”,返回只读映射;也可以传普通 Map<String, ?>。
  • {prefix} 是保留占位符,只能来自语言目录中的 common.prefix,调用方传入同名值不会覆盖它。

消息管线顺序固定:

  1. 查找消息键并提取路由前缀;
  2. 解析模板的 MiniMessage,再把 {prefix} 替换为同样经 MiniMessage 解析的前缀片段;
  3. 如果提供了 PlaceholderApi,对模板文本执行外部占位符展开;
  4. 把 {name} 形式的调用方值作为字面文本插入;
  5. 路由到接收者。

安全边界:调用方的命名值不会再进入 MiniMessage、路由或外部占位符解析。玩家输入即使含 <click:...>、actionbar: 或 %some_placeholder%,也只显示为文字,无法注入样式和操作。

外部占位符系统:安装模块时传入自己的 PlaceholderApi 适配器,不需要时传 null。外部展开只处理语言文件模板,不处理之后插入的命名值。

消息路由

在消息开头写路由前缀:

messages:
  chat-message: "chat: 普通聊天消息"
  progress: "actionbar: <green>进度 {current}/{total}</green>"
  completed: "title: <gold>完成!</gold>|<gray>奖励已发放</gray>"
  warning: "bossbar: <red>区域即将关闭</red>"
前缀 发送到
chat: 聊天栏,也是没有前缀时的默认路由
actionbar: Action Bar
title: Title,第一个 | 之后作为副标题
bossbar: 白色实心 Boss Bar,默认显示 100 tick
  • 前缀只在消息最开头识别,不区分大小写;前缀后的一个空格会被移除。
  • 同一玩家收到新 Boss Bar 时替换旧实例;有 Core 调度能力时自动到期;作用域关闭会移除所有仍在显示的 Boss Bar。

在代码中覆盖目录声明的路由:

lang.send(
        MessageRecipient.commandSender(player),
        MessageRoute.ACTION_BAR,
        "progress",
        Placeholders.of("current", 3, "total", 10));

只渲染,不发送

适配其他输出系统时,只取解析结果:

RichText text = lang.pipeline().render(
        MessageRecipient.commandSender(sender),
        "welcome",
        Placeholders.of("player", sender.getName()));

String plain = text.plainText();
String legacy = text.legacyText();

RichText 与 RichTextSegment 不向业务 API 暴露 Adventure 类型,便于跨服务端版本使用。

与命令模块共用消息

安装 Command 时传入同一个消息管线,命令错误、帮助和内置子命令就从 messages.command.* 读取:

LangRuntime lang = LangModule.install(
        root,
        getServer(),
        getDataFolder().toPath(),
        getClassLoader(),
        "zh_CN",
        null);

CommandModule.install(
        root,
        BukkitCommandRegistrar.discover(this, "myplugin"),
        lang.pipeline());

缺少命令消息时仍回退到 Klib 内置中英文目录。

Paper 宿主 Adventure

展开:用 BukkitAdventure 直接操作 Paper 原生组件

BukkitAdventure 处理 Paper 原生组件与插件内嵌组件之间的类加载隔离。

import me.kzheart.klib.lang.BukkitAdventure;

String plain = BukkitAdventure.plain(BukkitAdventure.parse("<gold>欢迎</gold>"));
BukkitAdventure.actionbar(player, "<green>完成</green>");
BukkitAdventure.book(player, "说明", "服务器", pages);
方法 作用
parse 返回宿主组件的 Object,不要强转成内嵌的 Adventure 类型
miniMessage、plain、legacy 把内容转成文本
actionbar、book 向玩家发送 Action Bar、打开书本
displayName、lore 读取物品的原生组件
inventory 创建组件标题的物品栏,可作为 UI 的 MenuInventoryFactory
available() 只检查宿主是否提供 Adventure;具体能力仍取决于 Paper API
  • 游戏对象操作要求主线程;纯文本序列化不访问服务器可变状态。
  • 旧版 Bukkit 缺少宿主组件时会明确失败,不会尝试把重定位后的类型传给服务器。
  • 反射同时处理泛型擦除后的序列化方法。

Java 与 Kotlin 共用的消息管线

语言目录、占位符、富文本解析和 Bukkit 路由由 Kotlin 实现。Java 与 Kotlin 都通过 LangRuntime / MessagePipeline 提交消息键,不必分别维护配置回退、颜色转换或每种发送通道的宿主检测。

  • 目录重载替换消息快照,已安装的管线继续使用新内容,不要求业务重建运行时。
  • Paper 原生组件与插件私有 Adventure 的转换留在 BukkitAdventure 边界,业务不直接交换不同加载器的组件对象。
  • 消息目录、路由和发送 SPI 保留 Java 平台类型;RichTextSegment 的薄 Java 外壳保留颜色、悬停和点击等可选值的平台返回契约。实现语言不改变消息转义和线程规则。

线程与生命周期

  • 语言配置文档、路由器、Boss Bar 和重载监听器都属于安装时传入的作用域。
  • 作用域关闭后,LangRuntime 的目录、管线和发送入口不可再用。
  • 在 KPlugin 中,语言文件变更经 Core 调度到主线程后替换目录与前缀。
  • Bukkit 的聊天、Action Bar、Title 和 Boss Bar API 应在主线程调用。不要从任意异步回调直接调用 lang.send,用 thenSync 或同步调度器切回主线程。
  • 消息目录替换是完整快照替换;正在发送的消息使用本次解析得到的稳定结果。

常见坑

  • 有字符串 text 子项的配置节会被当成结构化富文本,不能再当嵌套命名空间用。
  • 对控制台发送 Title、Action Bar 或 Boss Bar 会降级为纯文本,不会创建对应的 Bukkit UI。
  • lang.configDocument().reload() 的监听语义与 Config 模块一致;需要等待监听器完成时用 reloadAsync()。
  • 不要把用户输入直接拼进语言模板字符串。作为命名占位符值传入,才能保留字面插入的安全边界。
  • 地区名拼错或语言资源未打包时会整体回退到 zh_CN,注意日志里的 WARNING。

相关页面

  • Core:作用域、同步调度与资源清理。
  • Config:语言文件提取、监听和原子重载。
  • Command:复用语言管线显示错误与帮助。
  • Hook:PlaceholderAPI 等可选插件集成。

Clone this wiki locally