# NukkitLearn章外篇之二-插件中的多语言解决方案 参与编写者: [innc11](https://github.com/innc11) ##### 知识点: 配置文件、HashMap、枚举、文本替换、可变参数、正则表达式 本章外篇教程innc11制作,目的尽可能地简化插件开发过程中对多语言的处理。不得不说对多语言的支持是插件的一大加分项,特别是将插件发布到Nukkit的国际论坛上更是如此,如果有任何的疑问欢迎随时提issue。 ## 0.HashMap 所谓HashMap就是保存着一个个键值对的类,一个存储数据的类,形象的描述HashMap就相当于中药铺的药材柜子,柜子上有着数不清的小屉子,每个小屉子里都放着不同的药材,比如胖大海、金银花、荷叶等等(值),每个小屉子上都有一个标签,写着这个屉子里放着什么药材(键),这样就形成了一个关系,一个标签对应一个药材,这就是键值对的关系,一对一的关系,也是HashMap的存储结构,但也有一些限制,比如不能出现两个一模一样的标签,不然HashMap就无法判断到底需要哪一个小屉子的药材;但可以允许两个屉子里的东西一模一样;屉子可以是空的,但标签绝对不能为空。 HashMap能够处理一些数组无法处理的数据,比如一个统计玩家击杀数的插件,当某个玩家干掉10个怪物以后就给其发放奖励,这时候就需要对每个玩家进行记录,把玩家名作为标签,击杀数作为屉子里的东西,每次只需要根据玩家名把对应屉子打开,把里面的数据拿出来+1然后放回去关上屉子即可。这时候便出现了一个关系,每一个玩家名对应一个击杀数。 ``` zhangsan = 5 lisi = 3 wangwu = 8 ``` 玩家名就是键,击杀数就是值,大致就是这个结构,可以随时根据玩家名获取到对应击杀数,这是HashMap的工作方式。 ## 1. 原理 插件首先从配置文件加载所有语言到一个HashMap里,在需要时从这个HashMap里读出来,再进行相应变量替换后,显示给玩家。 ## 2. 创建项目 1. 首先创建一个项目,在这里我使用一个当玩家破坏方块时,给玩家发送一个消息,告诉玩家破坏的方块的ID,这里插件名字就叫 Tips,配置好依赖后,首先是```plugin.yml``` ```yaml name: Tips main: exam.miner.TipsPlugin version: "1.0" api: ["1.0.0"] ``` ## 3. 使用语言类 3. 首先我们声明一个语言类,这个类非常简单,仅仅包含一个HashMap、构造方法、获取语言的方法 ```lang```负责存储所有的语言文本,```String getLang()```负责从```lang```里面获取对应的文本并做参数替换,在构造方法里我们往```lang```里面添加2个语言,其中```BROKE_MESSAGE```和```PLACE_MESSAGE```是关键字,我们通过传递给```getLang()```一个关键字来获取对应的文本,```getLang()```会在```lang```里面用关键字去进行查找,并返回对应的文本 ```java public class MyLang { HashMap lang = new HashMap(); public MyLang() { lang.put("BROKE_MESSAGE", "你破坏了一个方块"); lang.put("PLACE_MESSAGE", "你放置了一个方块"); } public String getLang(String key) { return lang.get(key); } } ``` 接下来是主类: 在主类中使用刚才的```MyLang```类,并注册监听器,当玩家在破坏或者放置一个方块时,去获取对应的文本,然后发送给玩家 ```java public class TipsPlugin extends PluginBase implements Listener { MyLang lang; @Override public void onEnable() { lang = new MyLang(); getServer().getPluginManager().registerEvents(this, this); } @EventHandler public void onPlayerBrokeBlock(BlockBreakEvent e) { String message = lang.getLang("BROKE_MESSAGE"); // 你放置了一个方块 e.getPlayer().sendMessage(message); } @EventHandler public void onPlayerPlaceBlock(BlockPlaceEvent e) { String message = lang.getLang("PLACE_MESSAGE"); // 你破坏了一个方块 e.getPlayer().sendMessage(message); } } ``` 这就是最简单的方式,但实际开发中语言往往是从配置文件进行加载的,而不是写死在代码里,接下来就是如何从yml文件进行读取加载 ## 3. 从yaml配置文件加载语言 1. 我们使用language.yml文件用来保存语言文本 ```yaml # language.yml PLACE_MESSAGE: "你放置了一个方块" BROKE_MESSAGE: "你破坏了一个方块" ``` 1. 接着我们需要修改我们的语言文件,使其从配置文件进行加载,首先需要在```MyLang```类里额外添加一个```Config config```变量,和一个```void reload()```方法,我们手动调用```reload()```方法来从配置文件加载语言文本。 2. 构造方法我们需要添加一个参数,用来告诉```MyLang```类应该读取哪一个yml文件,不建议在构造方法中立即调用```reload()```,因为当对象构造的时候```language.yml```可能根本就不存在。非常建议在插件主类中保存默认配置文件后手动调用```reload()``` 3. 在新添加的```void reload()```方法中,首先是命令```config```(重新)加载一下,然后把```lang```中已经存在的数据全部删除掉,接着就是使用```getKeys(false)```来获取```config```中所有的key,就是上面yml中的```PLACE_MESSAGE```和```BROKE_MESSAGE```,这个方法会以```Set```的形式返回,我们使用foreach进行遍历即可,需要说明的是参数中的```false```指```boolean child```,我们只需要根节点上的key不需要子节点上的key,传```false```即可 4. 在foreach中我们定义一个变量value来放置获取到的"key对应的值"也就是```你放置了一个方块```和```你破坏了一个方块```接下来我们需要进行一个判断,如果这个值是```String```类型的,我们就把它添加到```lang```里面,如果不是,比如```int```,```bool```,或者```list```类型,则跳过。 ```java public class MyLang { Config config; HashMap lang = new HashMap(); public MyLang(String languageFileName) { config = new Config(new File(getDataFolder(), languageFileName), Config.YAML); } public void reload() { config.reload(); lang.clear(); for(String key : config.getKeys(false)) { Object value = config.get(key.name()); if(value instanceof String) { lang.put(key, (String) value); } } } public String getLang(String key) { return lang.get(key); } } ``` 5. 主类需要在```new MyLang()```时传递文件名。当然也要把```language.yml```以前打包进插件里。在onEnable()里要调用```saveResource("language.yml", false)```把```language.yml```写入到插件**DataFolder**里 ```java public class TipsPlugin extends PluginBase implements Listener { MyLang lang; @Override public void onEnable() { saveResource("language.yml", false); lang = new MyLang("language.yml"); lang.reload(); getServer().getPluginManager().registerEvents(this, this); } @EventHandler public void onPlayerBrokeBlock(BlockBreakEvent e) { String message = lang.getLang("BROKE_MESSAGE"); // 你放置了一个方块 e.getPlayer().sendMessage(message); } @EventHandler public void onPlayerPlaceBlock(BlockPlaceEvent e) { String message = lang.getLang("PLACE_MESSAGE"); // 你破坏了一个方块 e.getPlayer().sendMessage(message); } } ``` 7. 在实际使用中,我们只需要修改```language.yml```中文字,然后使用指令调用```MyLang.reload()```重新加载即可,但在复杂的插件中,只有这些功能时远远不够的,语言文件不能一成不变,有时候需要将文字中的一部分字符替换成各种实际数据,比如商店插件在交易完成时会显示这笔交易花费了多少多少钱,玩家死亡时会显示被谁谁谁干掉了,其中的"钱"和"击杀者"就是实际的数据,需要根据实际情景来决定具体应该是什么。这就涉及到参数化的问题,将文本中一部分文字使用实际数据进行替换。 ## 4. 参数化 1. 参数化必然会涉及到**占位符**这个概念,拿一个例子来说 ```yaml PLACE_MESSAGE: "你放置了ID为 ${BLOCK_ID} 的方块" ``` 其中的**${BLOCK_ID}**就是占位符,他只是给实际的数据占个位置而已,并不会被显示出来。当然风格可以自己定义,在这个例子中,我们使用```${占位符名字}```这种风格。 2. 我们修改我们的```MyLang```类的```getLang()```方法,使其可以动态替换占位符,具体的调用方式为```MyLang.getLang("PLACE_MESSAGE", "{BLOCK_ID}", String.valueOf(block.getId()));``` 3. 多个参数的调用方式 ``` MyLang.getLang("PLACE_MESSAGE", "{BLOCK_ID}", String.valueOf(block.getId(), "{PLAYER_NAME}", player.getName(), )); ``` 4. 无参数的调用方式 ``` MyLang.getLang("PLACE_MESSAGE")); ``` 5. 后面的占位符和实际数据总是成双成对的出现,这可以大幅加快开发效率,当然这需要```MyLang.getLang()```具有对应的支持,具体看下面的示例代码。 ```java public class MyLang { Config config; HashMap lang = new HashMap(); public MyLang(String languageFileName) { config = new Config(new File(getDataFolder(), languageFileName), Config.YAML); } public void reload() { config.reload(); lang.clear(); for(String key : config.getKeys(false)) { Object value = config.get(key.name()); if(value instanceof String) { lang.put(key, (String) value); } } } public String getLang(String key, String... argsPair) // 这里使用可变参数,当做数组一样处理即可 { String rawStr = lang.get(key); int argCount = argsPair.length / 2; // 计算出有多少"对"参数,末尾的孤立参数会被舍弃 for(int i=0;i 将MyLang.L作为键(key)以提高效率 HashMap lang = new HashMap(); public MyLang(String languageFileName) { config = new Config(new File(getDataFolder(), languageFileName), Config.YAML); } public void reload() { config.reload(); lang.clear(); // 一个标志,如果有缺少的关键字,会被补全,然后保存config,以便调试 boolean supplement = false; // 现在是以Lang.values()进行遍历,而不是config.getKeys(),注意 for(L key : L.values()) { Object value = config.get(key.name()); // 如果这个关键字不存在,会自动补齐,并设置标志位 if(v==null) { config.set(key.name(), key.getDefaultLangText()); supplement = true; lang.put(key, key.getDefaultLangText()); } if(value instanceof String) { lang.put(key, (String) value); } } // 如果有补齐,则需要保存这个config,以便用户可以在config内查看到以定位问题 if(supplement) { config.save(); } } public String getLang(L key, String... argsPair) // 这里使用可变参数,当做数组一样处理即可 { String rawStr = lang.get(key); int argCount = argsPair.length / 2; // 计算出有多少"对"参数,末尾的孤立参数会被舍弃 for(int i=0;i