server i18n

This commit is contained in:
mokun 2020-03-04 11:32:17 +08:00
parent a1ba010375
commit 4423a44c55
13 changed files with 776 additions and 0 deletions

View File

@ -86,6 +86,16 @@
<groupId>org.apache.commons</groupId> <groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId> <artifactId>commons-lang3</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.1</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>1.8</version>
</dependency>
<dependency> <dependency>
<groupId>commons-codec</groupId> <groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId> <artifactId>commons-codec</artifactId>

View File

@ -0,0 +1,11 @@
package io.metersphere.commons.constants;
public class I18nConstants {
public static final String LANG_COOKIE_NAME = "MS_USER_LANG";
public static final String LOCAL = "local";
public static final String CLUSTER = "cluster";
}

View File

@ -0,0 +1,190 @@
package io.metersphere.commons.constants;
/**
* Author: chunxing
* Date: 2018/6/26 下午3:44
* Description:
*/
public interface ParamConstants {
String getValue();
enum KeyCloak implements ParamConstants {
USERNAME("keycloak.username"),
PASSWORD("keycloak.password"),
REALM("keycloak.realm"),
AUTH_SERVER_URL("keycloak.auth-server-url"),
ADDRESS("keycloak-server-address");
private String value;
KeyCloak(String value) {
this.value = value;
}
@Override
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
enum Type implements ParamConstants {
PASSWORD("password"),
TEXT("text"),
JSON("json");
private String value;
Type(String value) {
this.value = value;
}
@Override
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
enum Classify implements ParamConstants {
KEYCLOAK("keycloak"),
MAIL("smtp"),
UI("ui"),
REGISTRY("registry");
private String value;
Classify(String value) {
this.value = value;
}
@Override
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
enum UI implements ParamConstants {
LOGO("ui.logo"),
SYSTEM_NAME("ui.system.name"),
THEME_PRIMARY("ui.theme.primary"),
THEME_ACCENT("ui.theme.accent"),
FAVICON("ui.favicon"),
LOGIN_TITLE("ui.login.title"),
LOGIN_IMG("ui.login.img"),
SUPPORT_NAME("ui.support.name"),
SUPPORT_URL("ui.support.url"),
TITLE("ui.title");
private String value;
UI(String value) {
this.value = value;
}
@Override
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
enum MAIL {
SERVER("smtp.server", 1),
PORT("smtp.port", 2),
ACCOUNT("smtp.account", 3),
PASSWORD("smtp.password", 4),
SSL("smtp.ssl", 5),
TLS("smtp.tls", 6);
private String key;
private Integer value;
MAIL(String key, Integer value) {
this.key = key;
this.value = value;
}
public String getKey() {
return key;
}
public Integer getValue() {
return value;
}
}
enum Log implements ParamConstants {
KEEP_MONTHS("log.keep.months");
private String value;
Log(String value) {
this.value = value;
}
@Override
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
enum Registry implements ParamConstants {
URL("registry.url"),
REPO("registry.repo"),
USERNAME("registry.username"),
PASSWORD("registry.password");
private String value;
Registry(String value) {
this.value = value;
}
@Override
public String getValue() {
return value;
}
}
enum I18n implements ParamConstants {
LANGUAGE("i18n.language");
private String value;
I18n(String value) {
this.value = value;
}
@Override
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
}

View File

@ -0,0 +1,21 @@
package io.metersphere.config;
import io.metersphere.i18n.I18nManager;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.ArrayList;
import java.util.List;
@Configuration
public class I18nConfig {
@Bean
@ConditionalOnMissingBean
public I18nManager i18nManager() {
List<String> dirs = new ArrayList<>();
dirs.add("i18n/");
return new I18nManager(dirs);
}
}

View File

@ -0,0 +1,53 @@
package io.metersphere.controller;
import io.metersphere.commons.constants.I18nConstants;
import io.metersphere.commons.exception.MSException;
import io.metersphere.commons.utils.LogUtil;
import io.metersphere.i18n.Lang;
import io.metersphere.service.UserService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
/**
* Created by liqiang on 2019/4/1.
*/
@RestController
public class I18nController {
private static final int FOR_EVER = 3600 * 24 * 30 * 12 * 10; //10 years in second
@Value("${run.mode:release}")
private String runMode;
@Resource
private UserService userService;
@GetMapping("lang/change/{lang}")
public void changeLang(@PathVariable String lang, HttpServletResponse response) {
Lang targetLang = Lang.getLangWithoutDefault(lang);
if (targetLang == null) {
response.setStatus(HttpServletResponse.SC_NOT_ACCEPTABLE);
LogUtil.error("Invalid parameter: " + lang);
MSException.throwException("ERROR_LANG_INVALID");
}
userService.setLanguage(targetLang.getDesc());
Cookie cookie = new Cookie(I18nConstants.LANG_COOKIE_NAME, targetLang.getDesc());
cookie.setPath("/");
cookie.setMaxAge(FOR_EVER);
response.addCookie(cookie);
//重新登录
if ("release".equals(runMode)) {
Cookie f2cCookie = new Cookie("MS_COOKIE_ID", "deleteMe");
f2cCookie.setPath("/");
f2cCookie.setMaxAge(0);
response.addCookie(f2cCookie);
}
}
}

View File

@ -21,6 +21,8 @@ public class UserDTO {
private Long updateTime; private Long updateTime;
private String language;
private String lastWorkspaceId; private String lastWorkspaceId;
private String lastOrganizationId; private String lastOrganizationId;
@ -103,6 +105,14 @@ public class UserDTO {
this.userRoles = userRoles; this.userRoles = userRoles;
} }
public String getLanguage() {
return language;
}
public void setLanguage(String language) {
this.language = language;
}
public String getLastWorkspaceId() { public String getLastWorkspaceId() {
return lastWorkspaceId; return lastWorkspaceId;
} }

View File

@ -0,0 +1,86 @@
package io.metersphere.i18n;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import io.metersphere.commons.utils.IOUtils;
import io.metersphere.commons.utils.LogUtil;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.util.ResourceUtils;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class I18nManager implements ApplicationRunner {
private static Map<String, Map<String, String>> i18nMap = new HashMap<>();
private List<String> dirs;
public I18nManager(List<String> dirs) {
this.dirs = dirs;
}
public static Map<String, Map<String, String>> getI18nMap() {
return i18nMap;
}
private static Resource[] getResources(String dir, String suffix) throws IOException {
Resource[] result = new Resource[0];
PathMatchingResourcePatternResolver patternResolver = new PathMatchingResourcePatternResolver();
if (!patternResolver.getResource(ResourceUtils.CLASSPATH_URL_PREFIX + dir).exists()) {
return result;
}
Resource[] resources = patternResolver.getResources(ResourceUtils.CLASSPATH_URL_PREFIX + dir + "*");
for (Resource resource : resources) {
if (StringUtils.endsWithIgnoreCase(resource.getFilename(), suffix)) {
result = ArrayUtils.add(result, resource);
}
}
return result;
}
private void init() {
try {
for (Lang lang : Lang.values()) {
Resource[] resources = new Resource[0];
String i18nKey = lang.getDesc().toLowerCase();
for (String dir : dirs) {
resources = ArrayUtils.addAll(resources, getResources(dir, i18nKey + ".json"));
}
for (Resource resource : resources) {
if (resource.exists()) {
try (InputStream inputStream = resource.getInputStream()) {
String fileContent = IOUtils.toString(inputStream, Charset.defaultCharset());
Map<String, String> langMap = JSON.parseObject(fileContent, new TypeReference<HashMap<String, String>>() {
});
i18nMap.computeIfAbsent(i18nKey, k -> new HashMap<>());
i18nMap.get(i18nKey).putAll(langMap);
} catch (Exception e) {
e.printStackTrace();
LogUtil.error("failed to load resource: " + resource.getURI());
}
}
}
}
} catch (Exception e) {
LogUtil.error("failed to load i18n.", e);
}
}
/**
* 国际化配置初始化
*/
@Override
public void run(ApplicationArguments args) {
init();
}
}

View File

@ -0,0 +1,48 @@
package io.metersphere.i18n;
import org.apache.commons.lang3.StringUtils;
public enum Lang {
zh_CN("zh-CN"), zh_TW("zh-TW"), en_US("en-US");
private String desc;
Lang(String desc) {
this.desc = desc;
}
public String getDesc() {
return this.desc;
}
public static Lang getLang(String lang) {
Lang result = getLangWithoutDefault(lang);
if (result == null) {
result = zh_CN;
}
return result;
}
public static Lang getLangWithoutDefault(String lang) {
if (StringUtils.isBlank(lang)) {
return null;
}
for (Lang lang1 : values()) {
if (StringUtils.equalsIgnoreCase(lang1.getDesc(), lang)) {
return lang1;
}
}
if (StringUtils.startsWithIgnoreCase(lang, "zh-CN")) {
return zh_CN;
}
if (StringUtils.startsWithIgnoreCase(lang, "zh-HK") || StringUtils.startsWithIgnoreCase(lang, "zh-TW")) {
return zh_TW;
}
if (StringUtils.startsWithIgnoreCase(lang, "en")) {
return en_US;
}
return null;
}
}

View File

@ -0,0 +1,274 @@
package io.metersphere.i18n;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.JavaBeanSerializer;
import com.alibaba.fastjson.serializer.ObjectSerializer;
import com.alibaba.fastjson.serializer.SerializeConfig;
import io.metersphere.commons.constants.I18nConstants;
import io.metersphere.commons.exception.MSException;
import io.metersphere.commons.utils.BeanUtils;
import io.metersphere.service.CommonBeanFactory;
import io.metersphere.commons.utils.LogUtil;
import io.metersphere.service.SystemParameterService;
import io.metersphere.user.SessionUtils;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.collections4.map.PassiveExpiringMap;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.commons.text.StringSubstitutor;
import org.springframework.http.HttpHeaders;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.lang.reflect.Array;
import java.util.*;
import java.util.concurrent.TimeUnit;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
public class Translator {
public static final String PREFIX = "$[{";
public static final String SUFFIX = "}]";
private static final String JSON_SYMBOL = "\":";
private static final HashSet<String> IGNORE_KEYS = new HashSet<>(Arrays.asList("id", "password", "passwd"));
private static Map<String, String> langCache4Thread = Collections.synchronizedMap(new PassiveExpiringMap(1, TimeUnit.MINUTES));
public static String getLangDes() {
return getLang().getDesc();
}
public static Lang getLang() {
HttpServletRequest request = getRequest();
return getLang(request);
}
public static Object gets(Object keys) {
return gets(getLang(), keys);
}
public static Object gets(Lang lang, Object keys) {
Map<String, String> context = I18nManager.getI18nMap().get(lang.getDesc().toLowerCase());
return translateObject(keys, context);
}
// 单Key翻译
public static String get(String key) {
return get(getLang(), key);
}
public static String get(Lang lang, String key) {
if (StringUtils.isBlank(key)) {
return StringUtils.EMPTY;
}
return translateKey(key, I18nManager.getI18nMap().get(lang.getDesc().toLowerCase()));
}
public static String toI18nKey(String key) {
return String.format("%s%s%s", PREFIX, key, SUFFIX);
}
private static HttpServletRequest getRequest() {
try {
return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
} catch (NullPointerException npe) {
return null;
}
}
private static Lang getLang(HttpServletRequest request) {
String preferLang = Lang.zh_CN.getDesc();
try {
if (request != null) {
Object sessionLang = request.getSession(true).getAttribute(I18nConstants.LANG_COOKIE_NAME);
if (sessionLang != null && StringUtils.isNotBlank(sessionLang.toString())) {
return Lang.getLang(sessionLang.toString());
}
preferLang = getSystemParameterLanguage(preferLang);
if (StringUtils.isNotBlank(request.getHeader(HttpHeaders.ACCEPT_LANGUAGE))) {
String preferLangWithComma = StringUtils.substringBefore(request.getHeader(HttpHeaders.ACCEPT_LANGUAGE), ";");
String acceptLanguage = StringUtils.replace(StringUtils.substringBefore(preferLangWithComma, ","), "-", "_");
if (Lang.getLangWithoutDefault(acceptLanguage) != null) {
preferLang = acceptLanguage;
}
}
if (request.getCookies() != null && request.getCookies().length > 0) {
for (Cookie cookie : request.getCookies()) {
if (StringUtils.equalsIgnoreCase(cookie.getName(), I18nConstants.LANG_COOKIE_NAME)) {
preferLang = cookie.getValue();
}
}
}
if (SessionUtils.getUser() != null && StringUtils.isNotBlank(SessionUtils.getUser().getLanguage())) {
preferLang = SessionUtils.getUser().getLanguage();
}
request.getSession(true).setAttribute(I18nConstants.LANG_COOKIE_NAME, preferLang);
} else {
preferLang = getSystemParameterLanguage(preferLang);
}
} catch (Exception e) {
LogUtil.error("Fail to getLang.", e);
}
return Lang.getLang(preferLang);
}
private static String getSystemParameterLanguage(String defaultLang) {
String result = defaultLang;
try {
String cachedLang = langCache4Thread.get(I18nConstants.LANG_COOKIE_NAME);
if (StringUtils.isNotBlank(cachedLang)) {
return cachedLang;
}
String systemLanguage = Objects.requireNonNull(CommonBeanFactory.getBean(SystemParameterService.class)).getSystemLanguage();
if (StringUtils.isNotBlank(systemLanguage)) {
result = systemLanguage;
}
langCache4Thread.put(I18nConstants.LANG_COOKIE_NAME, result);
} catch (Exception e) {
LogUtil.error(e);
}
return result;
}
private static Object translateObject(Object javaObject, final Map<String, String> context) {
if (MapUtils.isEmpty(context)) {
return javaObject;
}
if (javaObject == null) {
return null;
}
try {
if (javaObject instanceof String) {
String rawString = javaObject.toString();
if (StringUtils.contains(rawString, JSON_SYMBOL)) {
try {
Object jsonObject = JSON.parse(rawString);
Object a = translateObject(jsonObject, context);
return JSON.toJSONString(a);
} catch (Exception e) {
LogUtil.warn("Failed to translate object " + rawString + ". Error: " + ExceptionUtils.getStackTrace(e));
return translateRawString(null, rawString, context);
}
} else {
return translateRawString(null, rawString, context);
}
}
if (javaObject instanceof Map) {
Map<Object, Object> map = (Map<Object, Object>) javaObject;
for (Map.Entry<Object, Object> entry : map.entrySet()) {
if (entry.getValue() != null) {
if (entry.getValue() instanceof String) {
if (StringUtils.contains(entry.getValue().toString(), JSON_SYMBOL)) {
map.put(entry.getKey(), translateObject(entry.getValue(), context));
} else {
map.put(entry.getKey(), translateRawString(entry.getKey().toString(), entry.getValue().toString(), context));
}
} else {
translateObject(entry.getValue(), context);
}
}
}
}
if (javaObject instanceof Collection) {
Collection<Object> collection = (Collection<Object>) javaObject;
for (Object item : collection) {
translateObject(item, context);
}
}
if (javaObject.getClass().isArray()) {
for (int i = 0; i < Array.getLength(javaObject); ++i) {
Object item = Array.get(javaObject, i);
Array.set(javaObject, i, translateObject(item, context));
}
}
ObjectSerializer serializer = SerializeConfig.globalInstance.getObjectWriter(javaObject.getClass());
if (serializer instanceof JavaBeanSerializer) {
JavaBeanSerializer javaBeanSerializer = (JavaBeanSerializer) serializer;
try {
Map<String, Object> values = javaBeanSerializer.getFieldValuesMap(javaObject);
for (Map.Entry<String, Object> entry : values.entrySet()) {
if (entry.getValue() != null) {
if (entry.getValue() instanceof String) {
if (StringUtils.contains(entry.getValue().toString(), JSON_SYMBOL)) {
BeanUtils.setFieldValueByName(javaObject, entry.getKey(), translateObject(entry.getValue(), context), String.class);
} else {
BeanUtils.setFieldValueByName(javaObject, entry.getKey(), translateRawString(entry.getKey(), entry.getValue().toString(), context), String.class);
}
} else {
translateObject(entry.getValue(), context);
}
}
}
} catch (Exception e) {
MSException.throwException(e);
}
}
return javaObject;
} catch (StackOverflowError stackOverflowError) {
try {
return JSON.parseObject(translateRawString(null, JSON.toJSONString(javaObject), context).toString(), javaObject.getClass());
} catch (Exception e) {
LogUtil.error("Failed to translate object " + javaObject.toString(), e);
return javaObject;
}
}
}
private static Object translateRawString(String key, String rawString, Map<String, String> context) {
if (StringUtils.isBlank(rawString)) {
return rawString;
}
for (String ignoreKey : IGNORE_KEYS) {
if (StringUtils.containsIgnoreCase(key, ignoreKey)) {
return rawString;
}
}
if (StringUtils.contains(rawString, PREFIX)) {
rawString = new StringSubstitutor(context, PREFIX, SUFFIX).replace(rawString);
if (StringUtils.contains(rawString, PREFIX)) {
String[] unTrans = StringUtils.substringsBetween(rawString, PREFIX, SUFFIX);
if (unTrans != null) {
for (String unTran : unTrans) {
rawString = StringUtils.replace(rawString, PREFIX + unTran + SUFFIX, unTran);
}
}
}
}
if (key != null) {
String desc = context.get(rawString);
if (StringUtils.isNotBlank(desc)) {
return desc;
}
}
return rawString;
}
private static String translateKey(String key, Map<String, String> context) {
if (MapUtils.isEmpty(context)) {
return key;
}
String desc = context.get(StringUtils.replace(StringUtils.replace(key, PREFIX, StringUtils.EMPTY), SUFFIX, StringUtils.EMPTY));
if (StringUtils.isNotBlank(desc)) {
return desc;
}
return key;
}
}

View File

@ -0,0 +1,26 @@
package io.metersphere.service;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
public class CommonBeanFactory implements ApplicationContextAware {
private static ApplicationContext context;
public CommonBeanFactory() {
}
public void setApplicationContext(ApplicationContext ctx) throws BeansException {
context = ctx;
}
public static Object getBean(String beanName) {
return context != null && !StringUtils.isBlank(beanName) ? context.getBean(beanName) : null;
}
public static <T> T getBean(Class<T> className) {
return context != null && className != null ? context.getBean(className) : null;
}
}

View File

@ -0,0 +1,34 @@
package io.metersphere.service;
import io.metersphere.base.domain.SystemParameter;
import io.metersphere.base.domain.SystemParameterExample;
import io.metersphere.base.mapper.SystemParameterMapper;
import io.metersphere.commons.constants.ParamConstants;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import java.util.List;
import javax.annotation.Resource;
@Service
public class SystemParameterService {
@Resource
private SystemParameterMapper systemParameterMapper;
public String getSystemLanguage() {
String result = StringUtils.EMPTY;
SystemParameterExample example = new SystemParameterExample();
example.createCriteria().andParamKeyEqualTo(ParamConstants.I18n.LANGUAGE.getValue());
List<SystemParameter> list = systemParameterMapper.selectByExample(example);
if (CollectionUtils.isNotEmpty(list)) {
String value = list.get(0).getParamValue();
if (StringUtils.isNotBlank(value)) {
result = value;
}
}
return result;
}
}

View File

@ -337,4 +337,14 @@ public class UserService {
public List<User> getBesideOrgMemberList(String orgId) { public List<User> getBesideOrgMemberList(String orgId) {
return extUserRoleMapper.getBesideOrgMemberList(orgId); return extUserRoleMapper.getBesideOrgMemberList(orgId);
} }
public void setLanguage(String lang) {
if (SessionUtils.getUser() != null) {
User user = new User();
user.setId(SessionUtils.getUser().getId());
user.setLanguage(lang);
updateUser(user);
SessionUtils.getUser().setLanguage(lang);
}
}
} }

View File

@ -0,0 +1,3 @@
{
"i18n_test": "测试"
}