From 13b31b2ca2a5f8600002a042b536c9d5529842e3 Mon Sep 17 00:00:00 2001
From: Junjie <fallin.jie@qq.com>
Date: 星期一, 09 三月 2026 19:21:18 +0800
Subject: [PATCH] #

---
 src/main/java/com/zy/common/i18n/I18nMessageService.java |  406 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 406 insertions(+), 0 deletions(-)

diff --git a/src/main/java/com/zy/common/i18n/I18nMessageService.java b/src/main/java/com/zy/common/i18n/I18nMessageService.java
new file mode 100644
index 0000000..c125264
--- /dev/null
+++ b/src/main/java/com/zy/common/i18n/I18nMessageService.java
@@ -0,0 +1,406 @@
+package com.zy.common.i18n;
+
+import com.core.common.Cools;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.stereotype.Service;
+import org.springframework.context.i18n.LocaleContextHolder;
+
+import java.io.File;
+import java.io.BufferedReader;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.text.MessageFormat;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+
+@Service
+public class I18nMessageService {
+
+    private static final String MESSAGE_BUNDLE = "messages";
+    private static final String LEGACY_BUNDLE = "legacy";
+
+    private final Map<String, CacheEntry> cache = new ConcurrentHashMap<>();
+
+    @Autowired
+    private I18nProperties properties;
+
+    public Locale getCurrentLocale() {
+        Locale locale = LocaleContextHolder.getLocale();
+        return locale == null ? I18nLocaleUtils.defaultLocale(properties) : locale;
+    }
+
+    public Locale resolveLocale(String requested) {
+        return I18nLocaleUtils.resolveLocale(requested, properties);
+    }
+
+    public String getDefaultLocaleTag() {
+        return properties.getDefaultLocale();
+    }
+
+    public List<String> getSupportedLocaleTags() {
+        return properties.getSupportedLocales();
+    }
+
+    public String getMessage(String key, Object... args) {
+        return getMessage(key, getCurrentLocale(), args);
+    }
+
+    public String getMessage(String key, Locale locale, Object... args) {
+        if (Cools.isEmpty(key)) {
+            return "";
+        }
+        Locale resolvedLocale = locale == null ? I18nLocaleUtils.defaultLocale(properties) : locale;
+        String value = mergedBundle(resolvedLocale, MESSAGE_BUNDLE).get(key);
+        if (value == null && !I18nLocaleUtils.isDefaultLocale(resolvedLocale, properties)) {
+            value = mergedBundle(I18nLocaleUtils.defaultLocale(properties), MESSAGE_BUNDLE).get(key);
+        }
+        if (value == null) {
+            return key;
+        }
+        return args == null || args.length == 0 ? value : MessageFormat.format(value, args);
+    }
+
+    public boolean hasMessage(String key, Locale locale) {
+        return mergedBundle(locale == null ? I18nLocaleUtils.defaultLocale(properties) : locale, MESSAGE_BUNDLE).containsKey(key);
+    }
+
+    public Map<String, String> getMessages(Locale locale) {
+        return new LinkedHashMap<>(mergedBundle(locale, MESSAGE_BUNDLE));
+    }
+
+    public Map<String, String> getLegacyMessages(Locale locale) {
+        return new LinkedHashMap<>(mergedBundle(locale, LEGACY_BUNDLE));
+    }
+
+    public String translateLegacy(String text) {
+        return translateLegacy(text, getCurrentLocale());
+    }
+
+    public String translateLegacy(String text, Locale locale) {
+        if (Cools.isEmpty(text)) {
+            return text;
+        }
+        Locale resolvedLocale = locale == null ? I18nLocaleUtils.defaultLocale(properties) : locale;
+        if (I18nLocaleUtils.isDefaultLocale(resolvedLocale, properties)) {
+            return text;
+        }
+        Map<String, String> bundle = mergedBundle(resolvedLocale, LEGACY_BUNDLE);
+        String direct = directTranslate(text, bundle);
+        if (!text.equals(direct)) {
+            return direct;
+        }
+        String regex = regexTranslate(text, bundle);
+        if (!text.equals(regex)) {
+            return regex;
+        }
+        return fragmentTranslate(text, bundle);
+    }
+
+    public String resolveResourceText(String fallbackName, String code, Long id) {
+        Locale locale = getCurrentLocale();
+        String key = resourceKey(code, id);
+        if (hasMessage(key, locale)) {
+            return getMessage(key, locale);
+        }
+        if (!I18nLocaleUtils.isDefaultLocale(locale, properties)) {
+            String humanized = I18nLocaleUtils.humanizeCode(code);
+            if (!Cools.isEmpty(humanized)) {
+                return humanized;
+            }
+        }
+        return fallbackName;
+    }
+
+    public String resolvePermissionText(String fallbackName, String action, Long id) {
+        Locale locale = getCurrentLocale();
+        String key = permissionKey(action, id);
+        if (hasMessage(key, locale)) {
+            return getMessage(key, locale);
+        }
+        if (!I18nLocaleUtils.isDefaultLocale(locale, properties)) {
+            String humanized = I18nLocaleUtils.humanizeCode(action);
+            if (!Cools.isEmpty(humanized)) {
+                return humanized;
+            }
+        }
+        return fallbackName;
+    }
+
+    public String resourceKey(String code, Long id) {
+        return "resource." + normalizeKey(code, id, "resource");
+    }
+
+    public String permissionKey(String action, Long id) {
+        return "permission." + normalizeKey(action, id, "permission");
+    }
+
+    private Map<String, String> mergedBundle(Locale locale, String bundleName) {
+        Locale resolvedLocale = locale == null ? I18nLocaleUtils.defaultLocale(properties) : locale;
+        LinkedHashMap<String, String> merged = new LinkedHashMap<>();
+        Locale defaultLocale = I18nLocaleUtils.defaultLocale(properties);
+        merged.putAll(loadBundle(defaultLocale, bundleName));
+        if (!I18nLocaleUtils.toTag(defaultLocale).equalsIgnoreCase(I18nLocaleUtils.toTag(resolvedLocale))) {
+            merged.putAll(loadBundle(resolvedLocale, bundleName));
+        }
+        return merged;
+    }
+
+    private Map<String, String> loadBundle(Locale locale, String bundleName) {
+        String localeTag = I18nLocaleUtils.toTag(locale);
+        String cacheKey = localeTag + ":" + bundleName;
+        CacheEntry cacheEntry = cache.get(cacheKey);
+        File externalFile = externalBundle(localeTag, bundleName);
+        long refreshMillis = Math.max(1, properties.getRefreshSeconds()) * 1000L;
+        long externalLastModified = externalFile.exists() ? externalFile.lastModified() : -1L;
+        if (cacheEntry != null
+                && (System.currentTimeMillis() - cacheEntry.loadedAt) < refreshMillis
+                && cacheEntry.externalLastModified == externalLastModified) {
+            return cacheEntry.values;
+        }
+        synchronized (cache.computeIfAbsent(cacheKey, key -> new CacheEntry())) {
+            CacheEntry latest = cache.get(cacheKey);
+            if (latest != null
+                    && (System.currentTimeMillis() - latest.loadedAt) < refreshMillis
+                    && latest.externalLastModified == externalLastModified) {
+                return latest.values;
+            }
+            LinkedHashMap<String, String> values = new LinkedHashMap<>();
+            readClasspathBundle(values, localeTag, bundleName);
+            readExternalBundle(values, externalFile);
+            CacheEntry updated = new CacheEntry();
+            updated.values = values;
+            updated.loadedAt = System.currentTimeMillis();
+            updated.externalLastModified = externalLastModified;
+            cache.put(cacheKey, updated);
+            return updated.values;
+        }
+    }
+
+    private void readClasspathBundle(Map<String, String> values, String localeTag, String bundleName) {
+        ClassPathResource resource = new ClassPathResource("i18n/" + localeTag + "/" + bundleName + ".properties");
+        if (!resource.exists()) {
+            return;
+        }
+        try (InputStream inputStream = resource.getInputStream()) {
+            loadProperties(values, inputStream);
+        } catch (IOException ex) {
+            throw new IllegalStateException("Failed to load classpath i18n bundle: " + resource.getPath(), ex);
+        }
+    }
+
+    private void readExternalBundle(Map<String, String> values, File file) {
+        if (!file.exists() || !file.isFile()) {
+            return;
+        }
+        try (InputStream inputStream = new FileInputStream(file)) {
+            loadProperties(values, inputStream);
+        } catch (IOException ex) {
+            throw new IllegalStateException("Failed to load external i18n bundle: " + file.getAbsolutePath(), ex);
+        }
+    }
+
+    private void loadProperties(Map<String, String> values, InputStream inputStream) throws IOException {
+        try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
+            String line;
+            while ((line = reader.readLine()) != null) {
+                parsePropertyLine(values, line);
+            }
+        }
+    }
+
+    private void parsePropertyLine(Map<String, String> values, String line) {
+        if (line == null) {
+            return;
+        }
+        String trimmed = line.trim();
+        if (trimmed.isEmpty() || trimmed.startsWith("#") || trimmed.startsWith("!")) {
+            return;
+        }
+        int separatorIndex = findSeparator(line);
+        String rawKey = separatorIndex >= 0 ? line.substring(0, separatorIndex) : line;
+        String rawValue = separatorIndex >= 0 ? line.substring(separatorIndex + 1) : "";
+        while (!rawValue.isEmpty() && Character.isWhitespace(rawValue.charAt(0))) {
+            rawValue = rawValue.substring(1);
+        }
+        String key = unescapePropertyToken(rawKey);
+        String value = unescapePropertyToken(rawValue);
+        if (!key.isEmpty()) {
+            values.put(key, value);
+        }
+    }
+
+    private int findSeparator(String line) {
+        boolean escaping = false;
+        for (int i = 0; i < line.length(); i++) {
+            char ch = line.charAt(i);
+            if (escaping) {
+                escaping = false;
+                continue;
+            }
+            if (ch == '\\') {
+                escaping = true;
+                continue;
+            }
+            if (ch == '=' || ch == ':') {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    private String unescapePropertyToken(String text) {
+        StringBuilder builder = new StringBuilder(text.length());
+        boolean escaping = false;
+        for (int i = 0; i < text.length(); i++) {
+            char ch = text.charAt(i);
+            if (!escaping) {
+                if (ch == '\\') {
+                    escaping = true;
+                } else {
+                    builder.append(ch);
+                }
+                continue;
+            }
+            switch (ch) {
+                case 't':
+                    builder.append('\t');
+                    break;
+                case 'r':
+                    builder.append('\r');
+                    break;
+                case 'n':
+                    builder.append('\n');
+                    break;
+                case 'f':
+                    builder.append('\f');
+                    break;
+                case 'u':
+                    if (i + 4 < text.length()) {
+                        String hex = text.substring(i + 1, i + 5);
+                        try {
+                            builder.append((char) Integer.parseInt(hex, 16));
+                            i += 4;
+                            break;
+                        } catch (NumberFormatException ex) {
+                            builder.append('u');
+                            break;
+                        }
+                    }
+                    builder.append('u');
+                    break;
+                default:
+                    builder.append(ch);
+                    break;
+            }
+            escaping = false;
+        }
+        if (escaping) {
+            builder.append('\\');
+        }
+        return builder.toString();
+    }
+
+    private File externalBundle(String localeTag, String bundleName) {
+        return new File(properties.getPackPath(), localeTag + File.separator + bundleName + ".properties");
+    }
+
+    private String directTranslate(String text, Map<String, String> bundle) {
+        String trimmed = text.trim();
+        String translated = bundle.get(trimmed);
+        if (translated == null) {
+            return text;
+        }
+        return preserveOuterWhitespace(text, translated);
+    }
+
+    private String regexTranslate(String text, Map<String, String> bundle) {
+        String trimmed = text == null ? "" : text.trim();
+        for (Map.Entry<String, String> entry : bundle.entrySet()) {
+            String key = entry.getKey();
+            if (Cools.isEmpty(key) || !key.startsWith("regex:")) {
+                continue;
+            }
+            String patternText = key.substring("regex:".length());
+            if (Cools.isEmpty(patternText)) {
+                continue;
+            }
+            try {
+                Pattern pattern = Pattern.compile(patternText);
+                if (!trimmed.isEmpty()) {
+                    Matcher trimmedMatcher = pattern.matcher(trimmed);
+                    if (trimmedMatcher.matches()) {
+                        return preserveOuterWhitespace(text, trimmedMatcher.replaceAll(entry.getValue()));
+                    }
+                }
+                Matcher matcher = pattern.matcher(text);
+                if (matcher.find()) {
+                    return matcher.replaceAll(entry.getValue());
+                }
+            } catch (PatternSyntaxException ex) {
+                // Ignore invalid regex entries so a bad pack does not break all translations.
+            }
+        }
+        return text;
+    }
+
+    private String fragmentTranslate(String text, Map<String, String> bundle) {
+        List<Map.Entry<String, String>> entries = new ArrayList<>(bundle.entrySet());
+        entries.sort((left, right) -> Integer.compare(right.getKey().length(), left.getKey().length()));
+        String result = text;
+        for (Map.Entry<String, String> entry : entries) {
+            if (Cools.isEmpty(entry.getKey())
+                    || entry.getKey().length() < 2
+                    || entry.getKey().startsWith("regex:")
+                    || entry.getKey().equals(entry.getValue())) {
+                continue;
+            }
+            result = result.replace(entry.getKey(), entry.getValue());
+        }
+        return result;
+    }
+
+    private String preserveOuterWhitespace(String original, String translated) {
+        String trimmed = original == null ? "" : original.trim();
+        if (trimmed.isEmpty()) {
+            return translated;
+        }
+        int leading = original.indexOf(trimmed);
+        int trailing = original.length() - leading - trimmed.length();
+        StringBuilder builder = new StringBuilder();
+        if (leading > 0) {
+            builder.append(original, 0, leading);
+        }
+        builder.append(translated);
+        if (trailing > 0) {
+            builder.append(original.substring(original.length() - trailing));
+        }
+        return builder.toString();
+    }
+
+    private String normalizeKey(String raw, Long id, String prefix) {
+        if (Cools.isEmpty(raw)) {
+            return prefix + "." + (id == null ? "unknown" : id);
+        }
+        String normalized = raw.replaceAll("\\.html", "");
+        normalized = normalized.replaceAll("[^A-Za-z0-9]+", ".");
+        normalized = normalized.replaceAll("\\.+", ".");
+        normalized = normalized.replaceAll("^\\.|\\.$", "");
+        if (Cools.isEmpty(normalized)) {
+            return prefix + "." + (id == null ? "unknown" : id);
+        }
+        return normalized;
+    }
+
+    private static class CacheEntry {
+        private Map<String, String> values = new LinkedHashMap<>();
+        private long loadedAt;
+        private long externalLastModified = -1L;
+    }
+}

--
Gitblit v1.9.1