add:国际化

This commit is contained in:
2025-09-24 16:07:05 +08:00
parent aba35e4c31
commit bc70318571
12 changed files with 418 additions and 1 deletions

View File

@@ -17,7 +17,6 @@ package org.nl.common.exception;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.nl.common.logging.annotation.Log;
import org.springframework.http.HttpStatus;
import static org.springframework.http.HttpStatus.BAD_REQUEST;

View File

@@ -0,0 +1,20 @@
package org.nl.common.utils;
import org.apache.commons.lang3.StringUtils;
import org.nl.config.SpringContextHolder;
import org.nl.config.language.I18nManagerService;
public class I18nUtil {
private static I18nManagerService i18nManagerService = SpringContextHolder.getBean(I18nManagerService.class);
public static String msg(String key,String...args) {
if(StringUtils.isBlank(key)){
return "";
}
String msg = i18nManagerService.getCurrentMessage(key);
for(String arg: args){
msg += arg;
}
return msg;
}
}

View File

@@ -0,0 +1,178 @@
package org.nl.config.language;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ScheduledExecutorService;
@Service
public class I18nManagerService implements InitializingBean, DisposableBean {
private final I18nProperties properties;
private final Map<String, JSONObject> langCache = new HashMap<>();
private final Map<String, Long> lastModifiedTimes = new HashMap<>();
private final Map<String, String> filePaths = new HashMap<>();
private ScheduledExecutorService scheduler;
// 默认语言(从配置中获取)
private String defaultLang;
public I18nManagerService(I18nProperties properties) {
this.properties = properties;
}
@Override
public void afterPropertiesSet() throws Exception {
// 初始化默认语言
if (!properties.getSupportedLanguages().isEmpty()) {
defaultLang = properties.getSupportedLanguages().get(0);
}
// 加载所有语言文件
for (String lang : properties.getSupportedLanguages()) {
loadLangFile(lang);
}
// startRefreshScheduler();
}
// 新增:不带语言参数的方法,从上下文获取当前语言
public JSONObject getCurrentLangConfig() {
String lang = LangContextHolder.getLangOrDefault(defaultLang);
return getLangConfig(lang);
}
// 新增:不带语言参数的方法,从上下文获取当前语言
public String getCurrentMessage(String key) {
String lang = LangContextHolder.getLangOrDefault(defaultLang);
return getMessage(lang, key);
}
// 以下为原有方法(保持不变)
private void loadLangFile(String lang) throws IOException {
String fileName = lang + ".js";
String fullPath = properties.getLocation() + fileName;
Resource resource = getResource(fullPath);
if (!resource.exists() && properties.isFallbackToClasspath()) {
fullPath = "language/i18n/lang_" + fileName;
resource = new ClassPathResource(fullPath);
}
if (!resource.exists()) {
throw new IOException("Language file not found: " + fullPath);
}
String content = new String(Files.readAllBytes(Paths.get(resource.getURI())),
StandardCharsets.UTF_8);
JSONObject config = parseJsConfig(content);
langCache.put(lang, config);
lastModifiedTimes.put(lang, getLastModifiedTime(resource));
filePaths.put(lang, fullPath);
}
private Resource getResource(String path) {
if (path.startsWith("file:")) {
return new FileSystemResource(path.substring("file:".length()));
} else if (path.startsWith("classpath:")) {
return new ClassPathResource(path.substring("classpath:".length()));
} else {
return new FileSystemResource(path);
}
}
private long getLastModifiedTime(Resource resource) throws IOException {
if (resource instanceof FileSystemResource) {
return ((FileSystemResource) resource).getFile().lastModified();
} else if (resource instanceof ClassPathResource) {
return System.currentTimeMillis();
}
return 0;
}
private JSONObject parseJsConfig(String jsContent) {
String jsonStr = jsContent.replace("var config = ","").trim();
return JSON.parseObject(jsonStr);
}
public JSONObject getLangConfig(String lang) {
if (!langCache.containsKey(lang)) {
lang = defaultLang;
}
return langCache.get(lang);
}
public String getMessage(String lang, String key) {
JSONObject config = getLangConfig(lang);
if (config == null) {
return null;
}
String[] keyParts = key.split("\\.");
JSONObject current = config;
for (int i = 0; i < keyParts.length; i++) {
if (i == keyParts.length - 1) {
return StringUtils.isEmpty(current.getString(keyParts[i])) ? key : current.getString(keyParts[i]);
}
current = current.getJSONObject(keyParts[i]);
if (current == null) {
return key;
}
}
return key;
}
// private void startRefreshScheduler() {
// scheduler = Executors.newSingleThreadScheduledExecutor();
// scheduler.scheduleAtFixedRate(() -> {
// try {
// checkAndRefreshFiles();
// } catch (Exception e) {
// System.err.println("Error checking for language file updates: " + e.getMessage());
// }
// }, 0, properties.getRefreshInterval(), TimeUnit.SECONDS);
// }
// private void checkAndRefreshFiles() throws IOException {
// for (String lang : properties.getSupportedLanguages()) {
// String fullPath = filePaths.get(lang);
// if (StringUtils.isEmpty(fullPath)) {
// continue;
// }
// Resource resource = getResource(fullPath);
// if (!resource.exists()) {
// continue;
// }
// long currentLastModified = getLastModifiedTime(resource);
// if (currentLastModified > lastModifiedTimes.getOrDefault(lang, 0L)) {
// loadLangFile(lang);
// System.out.println("Language file updated: " + lang + " (" + fullPath + ")");
// }
// }
// }
@Override
public void destroy() {
if (scheduler != null) {
scheduler.shutdown();
}
langCache.clear();
lastModifiedTimes.clear();
filePaths.clear();
}
}

View File

@@ -0,0 +1,53 @@
package org.nl.config.language;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import java.util.Arrays;
import java.util.List;
@Configuration
@ConfigurationProperties(prefix = "i18n")
public class I18nProperties {
// 语言文件存放路径可以是外部路径或classpath
private String location = "D:\\i18n\\";
// 热更新检查间隔(秒)
private long refreshInterval = 60;
// 支持的语言列表
private List<String> supportedLanguages = Arrays.asList("en", "zh", "ja");
// 当外部文件不存在时是否回退到classpath中的默认文件
private boolean fallbackToClasspath = true;
// getter和setter方法
public String getLocation() {
return location;
}
public void setLocation(String location) {
this.location = location;
}
public long getRefreshInterval() {
return refreshInterval;
}
public void setRefreshInterval(long refreshInterval) {
this.refreshInterval = refreshInterval;
}
public List<String> getSupportedLanguages() {
return supportedLanguages;
}
public void setSupportedLanguages(List<String> supportedLanguages) {
this.supportedLanguages = supportedLanguages;
}
public boolean isFallbackToClasspath() {
return fallbackToClasspath;
}
public void setFallbackToClasspath(boolean fallbackToClasspath) {
this.fallbackToClasspath = fallbackToClasspath;
}
}

View File

@@ -0,0 +1,29 @@
package org.nl.config.language;
import org.springframework.util.StringUtils;
public class LangContextHolder {
// 线程本地变量,存储当前线程的语言标识
private static final ThreadLocal<String> LANG_HOLDER = new ThreadLocal<>();
// 设置当前线程的语言
public static void setLang(String lang) {
LANG_HOLDER.set(lang);
}
// 获取当前线程的语言
public static String getLang() {
return LANG_HOLDER.get();
}
// 清除当前线程的语言设置(防止内存泄漏)
public static void clear() {
LANG_HOLDER.remove();
}
// 获取当前语言,如果未设置则返回默认语言
public static String getLangOrDefault(String defaultLang) {
String lang = getLang();
return StringUtils.hasText(lang) ? lang : defaultLang;
}
}

View File

@@ -0,0 +1,51 @@
package org.nl.config.language;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class LangInterceptor implements HandlerInterceptor {
// 支持的语言列表(实际项目中可从配置获取)
private final I18nProperties properties;
public LangInterceptor(I18nProperties properties) {
this.properties = properties;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 1. 从请求参数获取语言(如 ?lang=zh
String lang = request.getParameter("lang");
// 2. 如果参数不存在可从Header获取如 Accept-Language
if (lang == null || lang.isEmpty()) {
lang = request.getHeader("Accept-Language");
// 简单处理,只取前两位(如 zh-CN -> zh
if (lang != null && lang.length() >= 2) {
lang = lang.substring(0, 2);
}
}
// 3. 验证语言是否在支持的列表中
if (lang != null && properties.getSupportedLanguages().contains(lang)) {
LangContextHolder.setLang(lang);
} else {
// 不支持的语言使用默认语言
LangContextHolder.setLang(properties.getSupportedLanguages().get(0));
}
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) {
// 清除线程变量,防止内存泄漏
LangContextHolder.clear();
}
}

View File

@@ -0,0 +1,21 @@
package org.nl.config.language;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
private final LangInterceptor langInterceptor;
public WebConfig(LangInterceptor langInterceptor) {
this.langInterceptor = langInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 对所有请求生效
registry.addInterceptor(langInterceptor).addPathPatterns("/**");
}
}

View File

@@ -36,6 +36,20 @@ login:
# 是否限制单用户登录
single-login: false
i18n:
# 语言文件存放路径支持外部路径和classpath
location: D:/i18n/lang_
# 热更新检查间隔(秒)
refresh-interval: 30
# 支持的语言列表
supported-languages:
- zh
- en
- in
- ja
# 当外部文件不存在时是否回退到classpath中的默认文件
fallback-to-classpath: true
#密码加密传输,前端公钥加密,后端私钥解密
rsa:
private_key: MIIBUwIBADANBgkqhkiG9w0BAQEFAASCAT0wggE5AgEAAkEA0vfvyTdGJkdbHkB8mp0f3FE0GYP3AYPaJF7jUd1M0XxFSE2ceK3k2kw20YvQ09NJKk+OMjWQl9WitG9pB6tSCQIDAQABAkA2SimBrWC2/wvauBuYqjCFwLvYiRYqZKThUS3MZlebXJiLB+Ue/gUifAAKIg1avttUZsHBHrop4qfJCwAI0+YRAiEA+W3NK/RaXtnRqmoUUkb59zsZUBLpvZgQPfj1MhyHDz0CIQDYhsAhPJ3mgS64NbUZmGWuuNKp5coY2GIj/zYDMJp6vQIgUueLFXv/eZ1ekgz2Oi67MNCk5jeTF2BurZqNLR3MSmUCIFT3Q6uHMtsB9Eha4u7hS31tj1UWE+D+ADzp59MGnoftAiBeHT7gDMuqeJHPL4b+kC+gzV4FGTfhR9q3tTbklZkD2A==

View File

@@ -0,0 +1,13 @@
var config = {
"lang": "English222",
"platform": {
"title": "NOBLELIFT Platform",
"tip1": "The user name cannot be empty",
"tip2": "The password cannot be empty",
"tip3": "当前语言111111英语"
},
"common": {
"home": "Dashboard",
"Layout_setting": "Layout Setting"
}
}

View File

@@ -0,0 +1,13 @@
var config = {
"lang": "English222",
"platform": {
"title": "NOBLELIFT Platform",
"tip1": "The user name cannot be empty",
"tip2": "The password cannot be empty",
"tip3": "当前语言1111印度尼西3333333亚语"
},
"common": {
"home": "Dashboard",
"Layout_setting": "Layout Setting"
}
}

View File

@@ -0,0 +1,13 @@
var config = {
"lang": "English222",
"platform": {
"title": "NOBLELIFT Platform",
"tip1": "The user name cannot be empty",
"tip2": "The password cannot be empty",
"tip3": "The verification code cannot be empty"
},
"common": {
"home": "Dashboard",
"Layout_setting": "Layout Setting"
}
}

View File

@@ -0,0 +1,13 @@
var config = {
"lang": "English222",
"platform": {
"title": "NOBLELIFT Platform",
"tip1": "The user name cannot be empty",
"tip2": "The password cannot be empty",
"tip3": "当前语言中文"
},
"common": {
"home": "Dashboard",
"Layout_setting": "Layout Setting"
}
}