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