commit 93a583c78155665d1b0106c9324cf15237d266ba Author: yhy <1763917516@qq.com> Date: Mon Jun 1 07:06:35 2026 +0800 初始化项目 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..537eda4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,57 @@ +<<<<<<< HEAD +target/ +out/ +lib/ +libs/ +.vscode/ +.codex/ +.idea/ +*.iml +*.class +*.log +logs/ +*.db +*.sqlite +*.sqlite3 +.DS_Store +Thumbs.db +======= +# ---> YHPlugin +# Maven +target/ + +# Build +out/ + +# IDE +.idea/ +*.iml +.vscode/ + +# AI +.codex/ + +# Local libraries +lib/ +libs/ + +# Tests +src/test/ + +# Logs +logs/ +*.log + +# Database +*.db +*.sqlite +*.sqlite3 + +# Java +*.class + +# OS +.DS_Store +Thumbs.db + +>>>>>>> 12c7b3f43c6113b170b9e83dedf75650d51b9040 diff --git a/README.md b/README.md new file mode 100644 index 0000000..56d2823 --- /dev/null +++ b/README.md @@ -0,0 +1,459 @@ +<<<<<<< HEAD +# 1.LangUtil 是什么? + +LangUtil 是一个 Minecraft 1.12.2 Bukkit / Spigot 多语言管理插件。 + +它的作用不是替代业务插件,而是给其他插件提供统一的语言 API: + +- 每个插件维护自己的语言文件 +- 玩家可以切换自己的语言 +- 不同玩家可以看到不同语言 +- 修改语言文件后可以不重启服务器直接重载 +- 支持物品名称、物品 lore、普通消息、占位符和颜色代码 + +简单理解: + +```yml +LangUtil = 公共语言管理器 +其他插件 = 自己提供 lang 文件 +玩家 = 自己选择 zh_CN / en_US 等语言 +``` + +------ + +# 2.目录结构 + +## LangUtil 自己的数据 + +玩家语言选择会保存到: + +```yml +plugins/LangUtil/player-language.yml +``` + +格式示例: + +```yml +player-language: + 550e8400-e29b-41d4-a716-446655440000: zh_CN +``` + +## 其他插件的语言文件 + +其他插件必须把语言文件放在自己的插件目录下: + +```yml +plugins/OtherPlugin/lang/zh_CN.yml +plugins/OtherPlugin/lang/en_US.yml +``` + +注意: + +```yml +语言文件不是放在 LangUtil 的 lang 目录 +语言文件是放在调用 register 的插件自己的 lang 目录 +``` + +------ + +# 3.其他插件如何注册? + +其他插件需要依赖 LangUtil。 + +## plugin.yml + +```yml +depend: + - LangUtil +``` + +## 插件入口 + +```java +import com.io.yaohun.langutil.LangUtil; +import org.bukkit.plugin.java.JavaPlugin; + +public final class OtherPlugin extends JavaPlugin { + @Override + public void onEnable() { + LangUtil.register(this); + } +} +``` + +当执行: + +```java +LangUtil.register(this); +``` + +LangUtil 会读取: + +```yml +plugins/OtherPlugin/lang/*.yml +``` + +------ + +# 4.语言文件怎么写? + +## zh_CN.yml + +```yml +message: + reload: "&a语言文件已重载" + no-permission: "&c你没有权限使用该命令。" + +item: + flame-sword: + name: "&c烈焰长剑" + lore: + - "&7伤害: &c{damage}" + - "&7品质: &6{quality}" + - "" + - "&e右键释放火焰" +``` + +## en_US.yml + +```yml +message: + reload: "&aLanguage files reloaded" + no-permission: "&cYou do not have permission." + +item: + flame-sword: + name: "&cFlame Sword" + lore: + - "&7Damage: &c{damage}" + - "&7Quality: &6{quality}" + - "" + - "&eRight-click to release fire" +``` + +文件名就是语言 ID: + +```yml +zh_CN.yml → zh_CN +en_US.yml → en_US +``` + +------ + +# 5.玩家命令 + +## 查看可用语言 + +```yml +/lang +``` + +玩家会看到当前已经加载的语言 ID。 + +## 切换语言 + +```yml +/lang zh_CN +/lang en_US +``` + +切换后会按玩家 UUID 保存。 + +玩家退出服务器再进入,仍然保持上次选择。 + +## 重载语言文件 + +```yml +/lang reload +``` + +修改语言文件后,不需要重启服务器,执行该命令即可重新读取所有已注册插件的语言文件。 + +控制台也可以执行: + +```yml +lang reload +``` + +------ + +# 6.权限节点 + +```yml +langutil.use # 允许玩家使用 /lang 查看和切换语言 +langutil.reload # 允许使用 /lang reload +``` + +推荐配置: + +```yml +普通玩家: langutil.use +管理员: langutil.use + langutil.reload +``` + +------ + +# 7.API 调用 + +## 获取普通消息 + +```java +String message = LangUtil.get(player, this, "message.reload"); +``` + +## 发送普通消息 + +```java +LangUtil.send(player, this, "message.reload"); +``` + +## 发送消息并播放音效 + +```java +LangUtil.send(player, this, "message.reload", Sound.LEVEL_UP); +``` + +## 使用占位符 + +```java +Map placeholders = new HashMap(); +placeholders.put("damage", "12"); +placeholders.put("quality", "史诗"); + +String name = LangUtil.get(player, this, "item.flame-sword.name", placeholders); +``` + +语言文件: + +```yml +item: + flame-sword: + name: "&c烈焰长剑" +``` + +结果: + +```yml +烈焰长剑 +``` + +------ + +# 8.物品 name 和 lore 怎么做? + +物品 name 是字符串,可以用: + +```java +LangUtil.get(player, this, "item.flame-sword.name", placeholders); +``` + +物品 lore 是 List,可以用: + +```java +LangUtil.getList(player, this, "item.flame-sword.lore", placeholders); +``` + +完整示例: + +```java +Map placeholders = new HashMap(); +placeholders.put("damage", "12"); +placeholders.put("quality", "史诗"); + +ItemMeta meta = item.getItemMeta(); +meta.setDisplayName(LangUtil.get(player, this, "item.flame-sword.name", placeholders)); +meta.setLore(LangUtil.getList(player, this, "item.flame-sword.lore", placeholders)); +item.setItemMeta(meta); +``` + +------ + +# 9.推荐封装方式 + +为了让业务代码更短,其他插件可以自己封装一个 `Lang` 工具类。 + +```java +public final class Lang { + private static JavaPlugin plugin; + + private Lang() { + } + + public static void init(JavaPlugin javaPlugin) { + plugin = javaPlugin; + LangUtil.register(javaPlugin); + } + + public static void send(CommandSender sender, String path) { + LangUtil.send(sender, plugin, path); + } + + public static String get(CommandSender sender, String path) { + return LangUtil.get(sender, plugin, path); + } + + public static List getList(CommandSender sender, String path) { + return LangUtil.getList(sender, plugin, path); + } +} +``` + +插件启动时: + +```java +Lang.init(this); +``` + +业务代码: + +```java +Lang.send(player, "message.reload"); +``` + +------ + +# 10.重载机制说明 + +LangUtil 不会每次发送消息都读取本地 YAML。 + +语言文件读取时机: + +```yml +插件启动 register 时 +/lang reload 时 +再次调用 register 时 +``` + +平时调用: + +```java +LangUtil.get(...) +LangUtil.getList(...) +LangUtil.send(...) +``` + +都是从内存缓存读取。 + +这样可以避免每次发消息都读磁盘,降低主线程压力。 + +------ + +# 11.异常与回退规则 + +## 玩家没有选择语言 + +默认使用: + +```yml +zh_CN +``` + +## 玩家选择的语言文件不存在 + +回退: + +```yml +zh_CN +``` + +## zh_CN 也不存在 + +返回 path 原文: + +```java +LangUtil.get(player, this, "message.not-found"); +``` + +返回: + +```yml +message.not-found +``` + +## lore 不存在 + +返回单行 List: + +```yml +- item.xxx.lore +``` + +## YAML 格式错误 + +控制台会输出: + +```yml +插件名 +语言文件名 +错误原因 +``` + +不会因为单个语言文件错误导致服务器关闭。 + +------ + +# 12.实战使用流程 + +## 第一步:安装 LangUtil + +把插件放入服务器: + +```yml +plugins/LangUtil-1.2.0.jar +``` + +启动服务器。 + +## 第二步:业务插件依赖 LangUtil + +```yml +depend: + - LangUtil +``` + +## 第三步:业务插件注册语言文件 + +```java +LangUtil.register(this); +``` + +## 第四步:创建语言文件 + +```yml +plugins/OtherPlugin/lang/zh_CN.yml +plugins/OtherPlugin/lang/en_US.yml +``` + +## 第五步:玩家切换语言 + +```yml +/lang zh_CN +/lang en_US +``` + +## 第六步:业务插件发送消息 + +```java +LangUtil.send(player, this, "message.reload"); +``` + +## 第七步:修改语言后重载 + +```yml +/lang reload +``` + +------ + +# 13.注意事项 + +- 推荐始终使用带 `JavaPlugin plugin` 参数的 API。 +- 不推荐使用 `LangUtil.get(String path)`,因为它无法稳定判断读取哪个插件的语言。 +- 其他插件必须先调用 `LangUtil.register(this)`,否则读取不到自己的语言文件。 +- 语言文件必须放在调用注册方法的插件自己的 `lang` 目录。 +- 物品 lore 必须用 YAML List 格式编写。 +- 修改语言文件后必须执行 `/lang reload` 才会进入缓存。 +======= +# LangUtil + +>>>>>>> 12c7b3f43c6113b170b9e83dedf75650d51b9040 diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..41e193f --- /dev/null +++ b/pom.xml @@ -0,0 +1,57 @@ + + 4.0.0 + + com.douhun + LangUtil + 1.2.1 + jar + + LangUtil + Minecraft 1.12.2 多插件语言管理插件 + + + UTF-8 + 1.8 + 1.8 + + + + + spigot-repo + https://hub.spigotmc.org/nexus/content/repositories/snapshots/ + + + + + + org.spigotmc + spigot-api + 1.12.2-R0.1-SNAPSHOT + provided + + + + + LangUtil-${project.version} + + + src/main/resources + true + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 1.8 + 1.8 + UTF-8 + + + + + diff --git a/src/main/java/com/io/yaohun/langutil/LangCommand.java b/src/main/java/com/io/yaohun/langutil/LangCommand.java new file mode 100644 index 0000000..5df94d5 --- /dev/null +++ b/src/main/java/com/io/yaohun/langutil/LangCommand.java @@ -0,0 +1,129 @@ +package com.io.yaohun.langutil; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; +import org.bukkit.entity.Player; + +final class LangCommand implements CommandExecutor, TabCompleter { + private static final String PERMISSION_USE = "langutil.use"; + private static final String PERMISSION_RELOAD = "langutil.reload"; + + private final LanguageRegistry languageRegistry; + private final PlayerLanguageStore playerLanguageStore; + + LangCommand(LanguageRegistry languageRegistry, PlayerLanguageStore playerLanguageStore) { + this.languageRegistry = languageRegistry; + this.playerLanguageStore = playerLanguageStore; + } + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + if (args.length == 1 && "reload".equalsIgnoreCase(args[0])) { + return handleReload(sender); + } + + if (args.length == 0) { + return handleList(sender); + } + + if (args.length == 1) { + return handleSwitch(sender, args[0]); + } + + LangUtil.sendMessage(sender, "&c用法:/lang [语言ID|reload]"); + return true; + } + + @Override + public List onTabComplete(CommandSender sender, Command command, String alias, String[] args) { + if (args.length != 1) { + return Collections.emptyList(); + } + + List suggestions = new ArrayList(); + String prefix = args[0].toLowerCase(); + if ("reload".startsWith(prefix) && sender.hasPermission(PERMISSION_RELOAD)) { + suggestions.add("reload"); + } + if (sender.hasPermission(PERMISSION_USE)) { + for (String languageId : this.languageRegistry.getAvailableLanguageIds()) { + if (languageId.toLowerCase().startsWith(prefix)) { + suggestions.add(languageId); + } + } + } + return suggestions; + } + + private boolean handleList(CommandSender sender) { + if (!sender.hasPermission(PERMISSION_USE)) { + LangUtil.sendMessage(sender, "&c你没有权限使用该命令。"); + return true; + } + + List languageIds = this.languageRegistry.getAvailableLanguageIds(); + if (languageIds.isEmpty()) { + LangUtil.sendMessage(sender, "&e当前没有已加载的语言文件。"); + return true; + } + + LangUtil.sendMessage(sender, "&a当前可用语言:&f" + join(languageIds)); + return true; + } + + private boolean handleSwitch(CommandSender sender, String languageId) { + if (!(sender instanceof Player)) { + LangUtil.sendMessage(sender, "&c该命令仅玩家可用。"); + return true; + } + if (!sender.hasPermission(PERMISSION_USE)) { + LangUtil.sendMessage(sender, "&c你没有权限使用该命令。"); + return true; + } + if (!this.languageRegistry.isLanguageAvailable(languageId)) { + LangUtil.sendMessage(sender, "&c该语言不存在。"); + return true; + } + + Player player = (Player) sender; + this.playerLanguageStore.setLanguage(player, languageId); + LangUtil.sendMessage(sender, "&a你的语言已切换为 " + languageId); + return true; + } + + private boolean handleReload(CommandSender sender) { + if (!sender.hasPermission(PERMISSION_RELOAD)) { + LangUtil.sendMessage(sender, "&c你没有权限使用该命令。"); + return true; + } + + ReloadSummary summary = this.languageRegistry.reloadAll(); + if (summary.hasFailures()) { + LangUtil.sendMessage(sender, "&c语言文件重载完成,但存在失败项:"); + for (LanguageLoadFailure failure : summary.getFailures()) { + LangUtil.sendMessage(sender, "&c- " + failure.getPluginName() + "/" + failure.getFileName() + + ":" + failure.getReason()); + } + return true; + } + + LangUtil.sendMessage(sender, "&a语言文件已重载。已加载:" + join(summary.getLoadedMessages())); + return true; + } + + private String join(List values) { + StringBuilder builder = new StringBuilder(); + for (int index = 0; index < values.size(); index++) { + if (index > 0) { + builder.append(", "); + } + builder.append(values.get(index)); + } + return builder.toString(); + } +} diff --git a/src/main/java/com/io/yaohun/langutil/LangService.java b/src/main/java/com/io/yaohun/langutil/LangService.java new file mode 100644 index 0000000..ce3b9a2 --- /dev/null +++ b/src/main/java/com/io/yaohun/langutil/LangService.java @@ -0,0 +1,37 @@ +package com.io.yaohun.langutil; + +import java.util.List; +import java.util.Map; +import org.bukkit.command.CommandSender; +import org.bukkit.plugin.java.JavaPlugin; + +final class LangService { + private final LanguageRegistry languageRegistry; + private final PlayerLanguageStore playerLanguageStore; + + LangService(LanguageRegistry languageRegistry, PlayerLanguageStore playerLanguageStore) { + this.languageRegistry = languageRegistry; + this.playerLanguageStore = playerLanguageStore; + } + + void register(JavaPlugin plugin) { + this.languageRegistry.register(plugin); + } + + String getDefault(String path) { + return MessageFormatter.color(this.languageRegistry.getDefaultMessage(path)); + } + + String get(CommandSender sender, JavaPlugin plugin, String path, Map placeholders) { + String languageId = this.playerLanguageStore.getLanguage(sender); + String rawMessage = this.languageRegistry.getMessage(plugin, languageId, path); + String replacedMessage = MessageFormatter.replace(rawMessage, placeholders); + return MessageFormatter.color(replacedMessage); + } + + List getList(CommandSender sender, JavaPlugin plugin, String path, Map placeholders) { + String languageId = this.playerLanguageStore.getLanguage(sender); + List rawMessages = this.languageRegistry.getMessageList(plugin, languageId, path); + return MessageFormatter.formatList(rawMessages, placeholders); + } +} diff --git a/src/main/java/com/io/yaohun/langutil/LangUtil.java b/src/main/java/com/io/yaohun/langutil/LangUtil.java new file mode 100644 index 0000000..0ad641d --- /dev/null +++ b/src/main/java/com/io/yaohun/langutil/LangUtil.java @@ -0,0 +1,102 @@ +package com.io.yaohun.langutil; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import org.bukkit.Sound; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.bukkit.plugin.java.JavaPlugin; + +public final class LangUtil { + private static volatile LangService service; + + private LangUtil() { + } + + static void initialize(LangService langService) { + service = langService; + } + + static void shutdown() { + service = null; + } + + public static void register(JavaPlugin plugin) { + LangService current = service; + if (current == null) { + throw new IllegalStateException("LangUtil 尚未加载,无法注册语言文件。"); + } + current.register(plugin); + } + + /** + * 不推荐使用:该方法缺少调用方插件上下文,无法稳定判断应读取哪个插件的语言文件。 + */ + public static String get(String path) { + LangService current = service; + return current == null ? fallbackPath(path) : current.getDefault(path); + } + + public static String get(CommandSender sender, JavaPlugin plugin, String path) { + return get(sender, plugin, path, Collections.emptyMap()); + } + + public static String get(CommandSender sender, JavaPlugin plugin, String path, Map placeholders) { + LangService current = service; + return current == null ? fallbackPath(path) : current.get(sender, plugin, path, placeholders); + } + + public static List getList(CommandSender sender, JavaPlugin plugin, String path) { + return getList(sender, plugin, path, Collections.emptyMap()); + } + + public static List getList(CommandSender sender, JavaPlugin plugin, String path, Map placeholders) { + LangService current = service; + return current == null ? Collections.singletonList(fallbackPath(path)) : current.getList(sender, plugin, path, placeholders); + } + + public static void sendMessage(CommandSender sender, String message) { + if (sender == null || message == null) { + return; + } + sender.sendMessage(MessageFormatter.color(message)); + } + + public static void sendMessage(CommandSender sender, String message, Sound sound) { + sendMessage(sender, message); + playSound(sender, sound); + } + + public static void send(CommandSender sender, JavaPlugin plugin, String path) { + send(sender, plugin, path, Collections.emptyMap()); + } + + public static void send(CommandSender sender, JavaPlugin plugin, String path, Sound sound) { + send(sender, plugin, path); + playSound(sender, sound); + } + + public static void send(CommandSender sender, JavaPlugin plugin, String path, Map placeholders) { + if (sender == null) { + return; + } + sender.sendMessage(get(sender, plugin, path, placeholders)); + } + + public static void send(CommandSender sender, JavaPlugin plugin, String path, Map placeholders, Sound sound) { + send(sender, plugin, path, placeholders); + playSound(sender, sound); + } + + private static void playSound(CommandSender sender, Sound sound) { + if (sender instanceof Player && sound != null) { + Player player = (Player) sender; + player.playSound(player.getLocation(), sound, 1.0F, 1.0F); + } + } + + private static String fallbackPath(String path) { + return path == null ? "" : path; + } +} diff --git a/src/main/java/com/io/yaohun/langutil/LangUtilPlugin.java b/src/main/java/com/io/yaohun/langutil/LangUtilPlugin.java new file mode 100644 index 0000000..06707ec --- /dev/null +++ b/src/main/java/com/io/yaohun/langutil/LangUtilPlugin.java @@ -0,0 +1,42 @@ +package com.io.yaohun.langutil; + +import org.bukkit.command.PluginCommand; +import org.bukkit.plugin.java.JavaPlugin; + +public final class LangUtilPlugin extends JavaPlugin { + private LanguageRegistry languageRegistry; + private PlayerLanguageStore playerLanguageStore; + private LangService langService; + + @Override + public void onEnable() { + this.playerLanguageStore = new PlayerLanguageStore(this); + this.playerLanguageStore.load(); + + this.languageRegistry = new LanguageRegistry(this); + this.langService = new LangService(this.languageRegistry, this.playerLanguageStore); + LangUtil.initialize(this.langService); + + LangCommand langCommand = new LangCommand(this.languageRegistry, this.playerLanguageStore); + PluginCommand command = getCommand("lang"); + if (command != null) { + command.setExecutor(langCommand); + command.setTabCompleter(langCommand); + } + + LangUtil.register(this); + getLogger().info("LangUtil 已加载。"); + } + + @Override + public void onDisable() { + if (this.playerLanguageStore != null) { + this.playerLanguageStore.save(); + } + if (this.languageRegistry != null) { + this.languageRegistry.clear(); + } + LangUtil.shutdown(); + getLogger().info("LangUtil 已卸载。"); + } +} diff --git a/src/main/java/com/io/yaohun/langutil/LanguageLoadFailure.java b/src/main/java/com/io/yaohun/langutil/LanguageLoadFailure.java new file mode 100644 index 0000000..10c0fca --- /dev/null +++ b/src/main/java/com/io/yaohun/langutil/LanguageLoadFailure.java @@ -0,0 +1,25 @@ +package com.io.yaohun.langutil; + +final class LanguageLoadFailure { + private final String pluginName; + private final String fileName; + private final String reason; + + LanguageLoadFailure(String pluginName, String fileName, String reason) { + this.pluginName = pluginName; + this.fileName = fileName; + this.reason = reason; + } + + String getPluginName() { + return this.pluginName; + } + + String getFileName() { + return this.fileName; + } + + String getReason() { + return this.reason; + } +} diff --git a/src/main/java/com/io/yaohun/langutil/LanguageLoadResult.java b/src/main/java/com/io/yaohun/langutil/LanguageLoadResult.java new file mode 100644 index 0000000..d50fe06 --- /dev/null +++ b/src/main/java/com/io/yaohun/langutil/LanguageLoadResult.java @@ -0,0 +1,28 @@ +package com.io.yaohun.langutil; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import org.bukkit.configuration.file.YamlConfiguration; + +final class LanguageLoadResult { + private final Map languages; + private final List failures = new ArrayList(); + + LanguageLoadResult(Map languages) { + this.languages = languages; + } + + Map getLanguages() { + return this.languages; + } + + void addFailure(LanguageLoadFailure failure) { + this.failures.add(failure); + } + + List getFailures() { + return Collections.unmodifiableList(this.failures); + } +} diff --git a/src/main/java/com/io/yaohun/langutil/LanguageLoader.java b/src/main/java/com/io/yaohun/langutil/LanguageLoader.java new file mode 100644 index 0000000..5e2c8f8 --- /dev/null +++ b/src/main/java/com/io/yaohun/langutil/LanguageLoader.java @@ -0,0 +1,70 @@ +package com.io.yaohun.langutil; + +import java.io.File; +import java.io.FileFilter; +import java.io.IOException; +import java.util.Arrays; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.Map; +import org.bukkit.configuration.InvalidConfigurationException; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.plugin.java.JavaPlugin; + +final class LanguageLoader { + private final JavaPlugin owner; + + LanguageLoader(JavaPlugin owner) { + this.owner = owner; + } + + LanguageLoadResult load(JavaPlugin plugin) { + Map languages = new LinkedHashMap(); + LanguageLoadResult result = new LanguageLoadResult(languages); + File langFolder = new File(plugin.getDataFolder(), "lang"); + + if (!langFolder.exists() || !langFolder.isDirectory()) { + this.owner.getLogger().info("插件 " + plugin.getName() + " 未提供 lang 目录,已跳过语言加载。"); + return result; + } + + File[] files = langFolder.listFiles(new FileFilter() { + @Override + public boolean accept(File file) { + return file.isFile() && file.getName().toLowerCase().endsWith(".yml"); + } + }); + if (files == null || files.length == 0) { + return result; + } + + Arrays.sort(files, new Comparator() { + @Override + public int compare(File left, File right) { + return left.getName().compareToIgnoreCase(right.getName()); + } + }); + + for (File file : files) { + String languageId = file.getName().substring(0, file.getName().length() - ".yml".length()); + YamlConfiguration configuration = new YamlConfiguration(); + try { + configuration.load(file); + languages.put(languageId, configuration); + } catch (IOException exception) { + recordFailure(result, plugin, file, exception); + } catch (InvalidConfigurationException exception) { + recordFailure(result, plugin, file, exception); + } + } + return result; + } + + private void recordFailure(LanguageLoadResult result, JavaPlugin plugin, File file, Exception exception) { + String reason = exception.getMessage() == null ? exception.getClass().getSimpleName() : exception.getMessage(); + LanguageLoadFailure failure = new LanguageLoadFailure(plugin.getName(), file.getName(), reason); + result.addFailure(failure); + this.owner.getLogger().warning("语言文件加载失败,插件=" + failure.getPluginName() + + ",文件=" + failure.getFileName() + ",原因=" + failure.getReason()); + } +} diff --git a/src/main/java/com/io/yaohun/langutil/LanguageRegistry.java b/src/main/java/com/io/yaohun/langutil/LanguageRegistry.java new file mode 100644 index 0000000..673b25c --- /dev/null +++ b/src/main/java/com/io/yaohun/langutil/LanguageRegistry.java @@ -0,0 +1,180 @@ +package com.io.yaohun.langutil; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.plugin.java.JavaPlugin; + +final class LanguageRegistry { + static final String DEFAULT_LANGUAGE = "zh_CN"; + + private final JavaPlugin owner; + private final LanguageLoader languageLoader; + private final Map bundles = new LinkedHashMap(); + + LanguageRegistry(JavaPlugin owner) { + this.owner = owner; + this.languageLoader = new LanguageLoader(owner); + } + + synchronized void register(JavaPlugin plugin) { + if (plugin == null) { + throw new IllegalArgumentException("注册语言文件时 plugin 不能为空。"); + } + LanguageLoadResult result = this.languageLoader.load(plugin); + this.bundles.put(plugin.getName(), new RegisteredLanguageBundle(plugin, result.getLanguages())); + } + + synchronized ReloadSummary reloadAll() { + ReloadSummary summary = new ReloadSummary(); + List plugins = new ArrayList(); + for (RegisteredLanguageBundle bundle : this.bundles.values()) { + plugins.add(bundle.getPlugin()); + } + + for (JavaPlugin plugin : plugins) { + LanguageLoadResult result = this.languageLoader.load(plugin); + this.bundles.put(plugin.getName(), new RegisteredLanguageBundle(plugin, result.getLanguages())); + summary.addLoaded(plugin.getName(), result.getLanguages().size()); + summary.addFailures(result.getFailures()); + } + return summary; + } + + synchronized String getMessage(JavaPlugin plugin, String languageId, String path) { + if (path == null || path.length() == 0) { + return ""; + } + RegisteredLanguageBundle bundle = findBundle(plugin); + if (bundle == null) { + return path; + } + + String selectedMessage = bundle.getMessage(languageId, path); + if (selectedMessage != null) { + return selectedMessage; + } + + String defaultMessage = bundle.getMessage(DEFAULT_LANGUAGE, path); + return defaultMessage == null ? path : defaultMessage; + } + + synchronized List getMessageList(JavaPlugin plugin, String languageId, String path) { + if (path == null || path.length() == 0) { + return Collections.emptyList(); + } + RegisteredLanguageBundle bundle = findBundle(plugin); + if (bundle == null) { + return Collections.singletonList(path); + } + + List selectedMessages = bundle.getMessageList(languageId, path); + if (selectedMessages != null) { + return selectedMessages; + } + + List defaultMessages = bundle.getMessageList(DEFAULT_LANGUAGE, path); + return defaultMessages == null ? Collections.singletonList(path) : defaultMessages; + } + + synchronized String getDefaultMessage(String path) { + if (path == null || path.length() == 0) { + return ""; + } + RegisteredLanguageBundle ownerBundle = this.bundles.get(this.owner.getName()); + if (ownerBundle != null) { + String message = ownerBundle.getMessage(DEFAULT_LANGUAGE, path); + if (message != null) { + return message; + } + } + for (RegisteredLanguageBundle bundle : this.bundles.values()) { + String message = bundle.getMessage(DEFAULT_LANGUAGE, path); + if (message != null) { + return message; + } + } + return path; + } + + synchronized boolean isLanguageAvailable(String languageId) { + if (languageId == null || languageId.length() == 0) { + return false; + } + for (RegisteredLanguageBundle bundle : this.bundles.values()) { + if (bundle.hasLanguage(languageId)) { + return true; + } + } + return false; + } + + synchronized List getAvailableLanguageIds() { + Set languageIds = new LinkedHashSet(); + for (RegisteredLanguageBundle bundle : this.bundles.values()) { + languageIds.addAll(bundle.getLanguageIds()); + } + List result = new ArrayList(languageIds); + Collections.sort(result); + return result; + } + + synchronized void clear() { + this.bundles.clear(); + } + + private RegisteredLanguageBundle findBundle(JavaPlugin plugin) { + if (plugin != null) { + RegisteredLanguageBundle bundle = this.bundles.get(plugin.getName()); + if (bundle != null) { + return bundle; + } + } + return this.bundles.get(this.owner.getName()); + } + + private static final class RegisteredLanguageBundle { + private final JavaPlugin plugin; + private final Map languages; + + private RegisteredLanguageBundle(JavaPlugin plugin, Map languages) { + this.plugin = plugin; + this.languages = new LinkedHashMap(languages); + } + + private JavaPlugin getPlugin() { + return this.plugin; + } + + private boolean hasLanguage(String languageId) { + return this.languages.containsKey(languageId); + } + + private Set getLanguageIds() { + return this.languages.keySet(); + } + + private String getMessage(String languageId, String path) { + YamlConfiguration configuration = this.languages.get(languageId); + return configuration == null ? null : configuration.getString(path); + } + + private List getMessageList(String languageId, String path) { + YamlConfiguration configuration = this.languages.get(languageId); + if (configuration == null || !configuration.contains(path)) { + return null; + } + if (configuration.isList(path)) { + return configuration.getStringList(path); + } + + String singleLine = configuration.getString(path); + return singleLine == null ? null : Collections.singletonList(singleLine); + } + } +} diff --git a/src/main/java/com/io/yaohun/langutil/MessageFormatter.java b/src/main/java/com/io/yaohun/langutil/MessageFormatter.java new file mode 100644 index 0000000..36c963e --- /dev/null +++ b/src/main/java/com/io/yaohun/langutil/MessageFormatter.java @@ -0,0 +1,48 @@ +package com.io.yaohun.langutil; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import org.bukkit.ChatColor; + +final class MessageFormatter { + private MessageFormatter() { + } + + static String replace(String message, Map placeholders) { + if (message == null) { + return ""; + } + if (placeholders == null || placeholders.isEmpty()) { + return message; + } + + String result = message; + for (Map.Entry entry : placeholders.entrySet()) { + if (entry.getKey() == null) { + continue; + } + String value = entry.getValue() == null ? "" : entry.getValue(); + result = result.replace("{" + entry.getKey() + "}", value); + } + return result; + } + + static String color(String message) { + if (message == null) { + return ""; + } + return ChatColor.translateAlternateColorCodes('&', message); + } + + static List formatList(List messages, Map placeholders) { + List result = new ArrayList(); + if (messages == null) { + return result; + } + for (String message : messages) { + result.add(color(replace(message, placeholders))); + } + return result; + } +} diff --git a/src/main/java/com/io/yaohun/langutil/PlayerLanguageStore.java b/src/main/java/com/io/yaohun/langutil/PlayerLanguageStore.java new file mode 100644 index 0000000..5ca5087 --- /dev/null +++ b/src/main/java/com/io/yaohun/langutil/PlayerLanguageStore.java @@ -0,0 +1,82 @@ +package com.io.yaohun.langutil; + +import java.io.File; +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.UUID; +import org.bukkit.command.CommandSender; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.Player; +import org.bukkit.plugin.java.JavaPlugin; + +final class PlayerLanguageStore { + private static final String ROOT_PATH = "player-language"; + + private final JavaPlugin plugin; + private final File file; + private final Map languages = new LinkedHashMap(); + + PlayerLanguageStore(JavaPlugin plugin) { + this.plugin = plugin; + this.file = new File(plugin.getDataFolder(), "player-language.yml"); + } + + synchronized void load() { + this.languages.clear(); + if (!this.file.exists()) { + return; + } + + YamlConfiguration configuration = YamlConfiguration.loadConfiguration(this.file); + ConfigurationSection section = configuration.getConfigurationSection(ROOT_PATH); + if (section == null) { + return; + } + + for (String key : section.getKeys(false)) { + try { + UUID uuid = UUID.fromString(key); + String languageId = section.getString(key); + if (languageId != null && languageId.length() > 0) { + this.languages.put(uuid, languageId); + } + } catch (IllegalArgumentException exception) { + this.plugin.getLogger().warning("玩家语言数据包含无效 UUID:" + key); + } + } + } + + synchronized void save() { + if (!this.plugin.getDataFolder().exists() && !this.plugin.getDataFolder().mkdirs()) { + this.plugin.getLogger().warning("无法创建插件数据目录:" + this.plugin.getDataFolder().getAbsolutePath()); + return; + } + + YamlConfiguration configuration = new YamlConfiguration(); + for (Map.Entry entry : this.languages.entrySet()) { + configuration.set(ROOT_PATH + "." + entry.getKey().toString(), entry.getValue()); + } + + try { + configuration.save(this.file); + } catch (IOException exception) { + this.plugin.getLogger().warning("保存玩家语言数据失败:" + exception.getMessage()); + } + } + + synchronized String getLanguage(CommandSender sender) { + if (sender instanceof Player) { + Player player = (Player) sender; + String languageId = this.languages.get(player.getUniqueId()); + return languageId == null ? LanguageRegistry.DEFAULT_LANGUAGE : languageId; + } + return LanguageRegistry.DEFAULT_LANGUAGE; + } + + synchronized void setLanguage(Player player, String languageId) { + this.languages.put(player.getUniqueId(), languageId); + save(); + } +} diff --git a/src/main/java/com/io/yaohun/langutil/ReloadSummary.java b/src/main/java/com/io/yaohun/langutil/ReloadSummary.java new file mode 100644 index 0000000..5f0b237 --- /dev/null +++ b/src/main/java/com/io/yaohun/langutil/ReloadSummary.java @@ -0,0 +1,30 @@ +package com.io.yaohun.langutil; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +final class ReloadSummary { + private final List loadedMessages = new ArrayList(); + private final List failures = new ArrayList(); + + void addLoaded(String pluginName, int languageCount) { + this.loadedMessages.add(pluginName + ": " + languageCount); + } + + void addFailures(List newFailures) { + this.failures.addAll(newFailures); + } + + List getLoadedMessages() { + return Collections.unmodifiableList(this.loadedMessages); + } + + List getFailures() { + return Collections.unmodifiableList(this.failures); + } + + boolean hasFailures() { + return !this.failures.isEmpty(); + } +} diff --git a/src/main/resources/lang/zh_CN.yml b/src/main/resources/lang/zh_CN.yml new file mode 100644 index 0000000..e677360 --- /dev/null +++ b/src/main/resources/lang/zh_CN.yml @@ -0,0 +1,14 @@ +message: + reload: "&a语言文件已重载" + no-permission: "&c你没有权限使用该命令。" +command: + lang: + changed: "&a你的语言已切换为 {lang}" +item: + flame-sword: + name: "&c烈焰长剑" + lore: + - "&7伤害: &c{damage}" + - "&7品质: &6{quality}" + - "" + - "&e右键释放火焰" \ No newline at end of file diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml new file mode 100644 index 0000000..6d265a4 --- /dev/null +++ b/src/main/resources/plugin.yml @@ -0,0 +1,16 @@ +name: LangUtil +main: com.io.yaohun.langutil.LangUtilPlugin +version: ${project.version} +author: Douhun +description: Minecraft 1.12.2 多插件语言管理插件 +commands: + lang: + description: 查看、切换或重载语言文件 + usage: /lang [语言ID|reload] +permissions: + langutil.use: + description: 允许玩家查看和切换个人语言 + default: true + langutil.reload: + description: 允许重载所有已注册插件的语言文件 + default: op