map;
+
+ /**
+ * 默认构造
+ *
+ * @param language 所属语言
+ */
+ public LanguageMap(Language language) {
+ super(language);
+ this.map = new HashMap<>();
+ }
+
+ public void union(LanguageMap map) {
+ this.map.putAll(map.map);
+ }
+
+ @Override
+ public void add(String key, String value) {
+ map.put(key, value);
+ }
+
+ @Override
+ public boolean has(String key) {
+ return map.containsKey(key);
+ }
+
+ /**
+ * 获取文本,支持二次映射,没有找到映射时返回入参键
+ *
+ * test.msg=Hello world
+ * mapping=@test.msg
+ * no_mapping=\@test.msg
+ *
+ * Multilingual.text("mapping"); // Hello world
+ * Multilingual.text("no_mapping"); // @test.msg
+ *
+ *
+ * @param key 键
+ * @return 文本值
+ */
+ @Override
+ public String text(String key) {
+ if (map.containsKey(key)) {
+ String result = map.get(key);
+ if (result.startsWith("@")) {
+ // 递归映射
+ return text(result.substring(1));
+ } else {
+ if (result.startsWith("\\@")) {
+ return result.substring(1);
+ } else {
+ return result;
+ }
+ }
+ } else {
+ if (TimiJava.isNotEmpty(key)) {
+ // 推测
+ String guessKey = key;
+ LinkedHashMap keySRL = Text.similarityRatioList(map.keySet(), key);
+ if (keySRL.keySet().iterator().hasNext()) {
+ String k = keySRL.keySet().iterator().next();
+ Number ratio = keySRL.get(k);
+ if (.7 < ratio.doubleValue()) {
+ guessKey = k;
+ }
+ }
+ System.err.printf("not found language mapping for key: [%s], it is [%s]?%n", key, guessKey);
+ if (isDebugging) {
+ throw new RuntimeException("not found language mapping key: " + key);
+ }
+ }
+ }
+ // 找不到映射,直接返回键
+ return key;
+ }
+
+ /**
+ * 获取文本
+ *
+ * @param key 键
+ * @param def 默认值(没有找到映射值时)
+ * @return 获取结果
+ */
+ @Override
+ public String text(String key, String def) {
+ String result = text(key);
+ return key.equals(result) ? def : result;
+ }
+
+ /**
+ * 插入参数获取文本
+ *
+ * @param key 键
+ * @param args 参数
+ * @return 结果
+ */
+ @Override
+ public String textArgs(String key, Object... args) {
+ String result = text(key);
+ if (map.containsKey(result)) {
+ // 没有映射值
+ return result + Arrays.toString(args);
+ }
+ FORMAT.applyPattern(result);
+ return FORMAT.format(args);
+ }
+}
diff --git a/src/main/java/com/imyeyu/lang/mapper/PropertiesLanguageMap.java b/src/main/java/com/imyeyu/lang/mapper/PropertiesLanguageMap.java
new file mode 100644
index 0000000..18ae14a
--- /dev/null
+++ b/src/main/java/com/imyeyu/lang/mapper/PropertiesLanguageMap.java
@@ -0,0 +1,27 @@
+package com.imyeyu.lang.mapper;
+
+import com.imyeyu.java.bean.Language;
+
+import java.util.HashMap;
+import java.util.Properties;
+
+/**
+ * @author 夜雨
+ * @version 2024-04-09 00:50
+ */
+public class PropertiesLanguageMap extends LanguageMap {
+
+ /**
+ * 默认构造
+ *
+ * @param language 所属语言
+ */
+ public PropertiesLanguageMap(Language language) {
+ super(language);
+ }
+
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ public void load(Properties properties) {
+ map.putAll(new HashMap(properties));
+ }
+}
diff --git a/src/main/java/com/imyeyu/lang/mapper/ResourcesLanguageMap.java b/src/main/java/com/imyeyu/lang/mapper/ResourcesLanguageMap.java
new file mode 100644
index 0000000..0eae46d
--- /dev/null
+++ b/src/main/java/com/imyeyu/lang/mapper/ResourcesLanguageMap.java
@@ -0,0 +1,39 @@
+package com.imyeyu.lang.mapper;
+
+import com.imyeyu.io.IO;
+import com.imyeyu.java.bean.Language;
+
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.Properties;
+
+/**
+ * @author 夜雨
+ * @version 2024-04-09 00:55
+ */
+public class ResourcesLanguageMap extends PropertiesLanguageMap {
+ /**
+ * 默认构造
+ *
+ * @param language 所属语言
+ */
+ public ResourcesLanguageMap(Language language) {
+ super(language);
+ }
+
+ public void load(String path) {
+ try {
+ InputStream is = IO.resourceToInputStream(getClass(), path.formatted(language.toString()));
+ if (is != null) {
+ Properties properties = new Properties();
+ properties.load(new InputStreamReader(is));
+ load(properties);
+ } else {
+ throw new FileNotFoundException("not found language properties file: " + path.formatted(language.toString()));
+ }
+ } catch (Exception e) {
+ throw new RuntimeException("load language properties file error", e);
+ }
+ }
+}
diff --git a/src/main/java/com/imyeyu/lang/multi/FileMultilingual.java b/src/main/java/com/imyeyu/lang/multi/FileMultilingual.java
new file mode 100644
index 0000000..3384666
--- /dev/null
+++ b/src/main/java/com/imyeyu/lang/multi/FileMultilingual.java
@@ -0,0 +1,29 @@
+package com.imyeyu.lang.multi;
+
+import com.imyeyu.java.bean.Language;
+import com.imyeyu.lang.mapper.AbstractLanguageMapper;
+import com.imyeyu.lang.mapper.FileLanguageMap;
+import com.imyeyu.lang.mapper.LanguageMap;
+
+/**
+ * @author 夜雨
+ * @version 2024-04-09 01:06
+ */
+public class FileMultilingual extends Multilingual {
+
+ public void addAll(String path) {
+ Language[] values = Language.values();
+ for (int i = 0; i < values.length; i++) {
+ FileLanguageMap mapper = new FileLanguageMap(values[i]);
+ mapper.load(path);
+ if (multilingualMap.containsKey(values[i])) {
+ AbstractLanguageMapper existMapper = multilingualMap.get(values[i]);
+ if (existMapper instanceof LanguageMap map) {
+ map.union(mapper);
+ }
+ } else {
+ add(values[i], mapper);
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/imyeyu/lang/multi/Multilingual.java b/src/main/java/com/imyeyu/lang/multi/Multilingual.java
new file mode 100644
index 0000000..9eb4ff1
--- /dev/null
+++ b/src/main/java/com/imyeyu/lang/multi/Multilingual.java
@@ -0,0 +1,218 @@
+package com.imyeyu.lang.multi;
+
+import com.imyeyu.java.bean.CallbackArg;
+import com.imyeyu.java.bean.Language;
+import com.imyeyu.java.bean.LanguageMapping;
+import com.imyeyu.java.ref.Ref;
+import com.imyeyu.lang.mapper.AbstractLanguageMapper;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * 多语言系统,支持多文件多语言加载
+ *
+ * // lang 文件
+ * say.msg=hello {0}
+ *
+ * // 加载
+ * Multilingual lang = Multilingual.getInstance();
+ * lang.load(Language.zh_CN, "lang/timijava/%s.lang");
+ *
+ * // 加载所有支持的映射文件
+ * lang.loadAll("lang/timifx/%s.lang");
+ *
+ * // 设置当前激活语言
+ * lang.setActivated("zh_CN");
+ *
+ * // 引用当前激活语言
+ * Multilingual.text("say.msg");
+ * Multilingual.text("say.msg", "timi-java");
+ *
+ * // 动态引用
+ * Multilingual.map("en_US").text("say.msg");
+ *
+ *
+ * @author 夜雨
+ * @version 2021-10-25 15:16
+ */
+public class Multilingual implements LanguageMapping {
+
+ protected final Map multilingualMap;
+ protected final List> updateActiveListeners;
+
+ /** 当前激活语言 */
+ protected Language activated;
+
+ protected boolean isDebugging = false;
+
+ public Multilingual() {
+ multilingualMap = new HashMap<>();
+ updateActiveListeners = new ArrayList<>();
+
+ activated = Ref.toType(Language.class, Locale.getDefault().toString());
+
+ if (activated == null) {
+ activated = Language.zh_CN;
+ }
+ }
+
+ public void add(Language language, AbstractLanguageMapper mapper) {
+ mapper.setDebugging(isDebugging);
+ multilingualMap.put(language, mapper);
+ }
+
+ /**
+ * 添加更新激活语言回调
+ *
+ * @param callback 回调
+ */
+ public void addUpdateActiveListeners(CallbackArg callback) {
+ updateActiveListeners.add(callback);
+ }
+
+ /**
+ * 移除更新激活语言回调
+ *
+ * @param callback 回调
+ */
+ public void removeUpdateActiveListeners(CallbackArg callback) {
+ updateActiveListeners.remove(callback);
+ }
+
+ /**
+ * 设置激活语言
+ *
+ * @param languageName 激活语言
+ */
+ public void setActivated(String languageName) {
+ setActivated(Ref.toType(Language.class, languageName));
+ }
+
+ /**
+ * 设置激活语言
+ *
+ * @param activated 激活语言
+ */
+ public void setActivated(Language activated) {
+ Objects.requireNonNull(activated);
+ this.activated = activated;
+
+ String[] localSp = activated.toString().split("_");
+ Locale.setDefault(new Locale(localSp[0], localSp[1]));
+
+ for (int i = 0; i < updateActiveListeners.size(); i++) {
+ updateActiveListeners.get(i).handler(activated);
+ }
+ }
+
+ public Language getActivated() {
+ return activated;
+ }
+
+ public void setDebugging(boolean debugging) {
+ isDebugging = debugging;
+ for (Map.Entry item : multilingualMap.entrySet()) {
+ item.getValue().setDebugging(true);
+ }
+ }
+
+ /**
+ * 获取指定映射数据
+ *
+ * @param lang 语言
+ * @return 映射表
+ */
+ public AbstractLanguageMapper map(String lang) {
+ Language language = Ref.toType(Language.class, lang);
+ if (language == null) {
+ throw new RuntimeException("not support language: " + lang);
+ }
+ return map(language);
+ }
+
+ /**
+ * 获取指定映射数据
+ *
+ * @param lang 语言
+ * @return 映射表
+ */
+ public AbstractLanguageMapper map(Language lang) {
+ return multilingualMap.get(lang);
+ }
+
+ @Override
+ public void add(String key, String value) {
+ multilingualMap.get(getActivated()).add(key, value);
+ }
+
+ /**
+ * 当前激活语言是否存在语言映射
+ *
+ * @param key 键
+ * @return true 为存在
+ */
+ @Override
+ public boolean has(String key) {
+ if (multilingualMap.get(getActivated()) == null) {
+ return false;
+ }
+ return multilingualMap.get(getActivated()).has(key);
+ }
+
+ /**
+ * 从当前激活语言获取文本,支持二次映射,没有找到映射时返回入参键
+ *
+ * test.msg=Hello world
+ * mapping=@test.msg
+ * no_mapping=\@test.msg
+ *
+ * Multilingual.text("mapping"); // Hello world
+ * Multilingual.text("no_mapping"); // @test.msg
+ *
+ *
+ * @param key 键
+ * @return 文本值
+ */
+ @Override
+ public String text(String key) {
+ if (multilingualMap.get(getActivated()) == null) {
+ return key;
+ }
+ return multilingualMap.get(getActivated()).text(key);
+ }
+
+ /**
+ * 从当前激活语言获取文本
+ *
+ * @param key 键
+ * @param def 默认值(没有找到映射值时)
+ * @return 获取结果
+ */
+ @Override
+ public String text(String key, String def) {
+ if (multilingualMap.get(getActivated()) == null) {
+ return Objects.requireNonNullElse(def, key);
+ }
+ return multilingualMap.get(getActivated()).text(key, def);
+ }
+
+ /**
+ * 从当前激活语言获取插入参数文本
+ *
+ * @param key 键
+ * @param params 参数
+ * @return 结果
+ */
+ @Override
+ public String textArgs(String key, Object... params) {
+ if (multilingualMap.get(getActivated()) == null) {
+ return key;
+ }
+ return multilingualMap.get(getActivated()).textArgs(key, params);
+ }
+}
diff --git a/src/main/java/com/imyeyu/lang/multi/ResourcesMultilingual.java b/src/main/java/com/imyeyu/lang/multi/ResourcesMultilingual.java
new file mode 100644
index 0000000..b8cb341
--- /dev/null
+++ b/src/main/java/com/imyeyu/lang/multi/ResourcesMultilingual.java
@@ -0,0 +1,29 @@
+package com.imyeyu.lang.multi;
+
+import com.imyeyu.java.bean.Language;
+import com.imyeyu.lang.mapper.AbstractLanguageMapper;
+import com.imyeyu.lang.mapper.LanguageMap;
+import com.imyeyu.lang.mapper.ResourcesLanguageMap;
+
+/**
+ * @author 夜雨
+ * @version 2024-04-09 01:03
+ */
+public class ResourcesMultilingual extends Multilingual {
+
+ public void addAll(String path) {
+ Language[] values = Language.values();
+ for (int i = 0; i < values.length; i++) {
+ ResourcesLanguageMap mapper = new ResourcesLanguageMap(values[i]);
+ mapper.load(path);
+ if (multilingualMap.containsKey(values[i])) {
+ AbstractLanguageMapper existMapper = multilingualMap.get(values[i]);
+ if (existMapper instanceof LanguageMap map) {
+ map.union(mapper);
+ }
+ } else {
+ add(values[i], mapper);
+ }
+ }
+ }
+}