mirror of
https://gitee.com/smallc/blade-tool.git
synced 2026-07-12 03:09:45 +08:00
✨ 添加 i8n 国际化模块
This commit is contained in:
parent
929761ac34
commit
2bb0dc2073
33
blade-starter-i18n/pom.xml
Normal file
33
blade-starter-i18n/pom.xml
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.springblade</groupId>
|
||||
<artifactId>blade-tool</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>blade-starter-i18n</artifactId>
|
||||
<name>${project.artifactId}</name>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<properties>
|
||||
<module.name>org.springblade.blade.starter.i18n</module.name>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<!-- Blade -->
|
||||
<dependency>
|
||||
<groupId>org.springblade</groupId>
|
||||
<artifactId>blade-core-tool</artifactId>
|
||||
</dependency>
|
||||
<!-- Spring Boot Validation -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
/**
|
||||
* Copyright (c) 2018-2099, Chill Zhuang 庄骞 (bladejava@qq.com).
|
||||
* <p>
|
||||
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl.html
|
||||
* <p>
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springblade.core.i18n.config;
|
||||
|
||||
import jakarta.annotation.Nonnull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.core.i18n.interceptor.I18nInterceptor;
|
||||
import org.springblade.core.i18n.props.I18nProperties;
|
||||
import org.springblade.core.i18n.resolver.I18nLocaleResolver;
|
||||
import org.springblade.core.i18n.service.I18nService;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
|
||||
import org.springframework.web.servlet.LocaleResolver;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
/**
|
||||
* I18n自动配置类
|
||||
*
|
||||
* @author BladeX
|
||||
*/
|
||||
@Slf4j
|
||||
@AutoConfiguration
|
||||
@RequiredArgsConstructor
|
||||
@EnableConfigurationProperties(I18nProperties.class)
|
||||
@ConditionalOnProperty(prefix = I18nProperties.PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true)
|
||||
public class I18nAutoConfiguration {
|
||||
|
||||
/**
|
||||
* 类路径前缀常量
|
||||
*/
|
||||
private static final String CLASSPATH_PREFIX = "classpath:";
|
||||
|
||||
/**
|
||||
* 国际化配置类
|
||||
*/
|
||||
private final I18nProperties properties;
|
||||
|
||||
/**
|
||||
* 覆盖Spring配置的MessageSource
|
||||
*/
|
||||
@Bean
|
||||
@Primary
|
||||
public MessageSource messageSource() {
|
||||
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
|
||||
I18nProperties.MessageSource config = properties.getMessageSource();
|
||||
|
||||
// 设置基础名列表
|
||||
String[] baseNames = config.getBaseNames().stream()
|
||||
.map(name -> name.startsWith(CLASSPATH_PREFIX) ? name : CLASSPATH_PREFIX + name)
|
||||
.toArray(String[]::new);
|
||||
messageSource.setBasenames(baseNames);
|
||||
// 设置编码
|
||||
messageSource.setDefaultEncoding(config.getEncoding());
|
||||
// 设置缓存时间
|
||||
if (config.getCacheDuration() != null) {
|
||||
messageSource.setCacheSeconds(Func.toInt(config.getCacheDuration().getSeconds()));
|
||||
}
|
||||
// 设置是否使用代码作为默认消息
|
||||
messageSource.setUseCodeAsDefaultMessage(config.isUseCodeAsDefaultMessage());
|
||||
return messageSource;
|
||||
}
|
||||
|
||||
/**
|
||||
* 覆盖Spring配置的Locale解析器
|
||||
*/
|
||||
@Bean
|
||||
@Primary
|
||||
public LocaleResolver localeResolver() {
|
||||
return new I18nLocaleResolver(properties);
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置I18n服务
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public I18nService i18nService(MessageSource messageSource, LocaleResolver localeResolver) {
|
||||
return new I18nService(messageSource, localeResolver, properties);
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置I18n拦截器
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public I18nInterceptor i18nInterceptor() {
|
||||
return new I18nInterceptor(properties);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册I18n拦截器
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
|
||||
public WebMvcConfigurer i18nWebMvcConfigurer(I18nInterceptor i18nInterceptor) {
|
||||
return new WebMvcConfigurer() {
|
||||
@Override
|
||||
public void addInterceptors(@Nonnull InterceptorRegistry registry) {
|
||||
registry.addInterceptor(i18nInterceptor)
|
||||
.addPathPatterns("/**")
|
||||
.order(0);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
/**
|
||||
* Copyright (c) 2018-2099, Chill Zhuang 庄骞 (bladejava@qq.com).
|
||||
* <p>
|
||||
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl.html
|
||||
* <p>
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springblade.core.i18n.interceptor;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.core.i18n.props.I18nProperties;
|
||||
import org.springblade.core.i18n.utils.LocaleParseUtil;
|
||||
import org.springframework.context.i18n.LocaleContextHolder;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* I18n拦截器
|
||||
*
|
||||
* @author BladeX
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class I18nInterceptor implements HandlerInterceptor {
|
||||
|
||||
private static final String LOCALE_ATTRIBUTE = "locale";
|
||||
private static final String LANG_ATTRIBUTE = "lang";
|
||||
private static final String CONTENT_LANGUAGE_HEADER = "Content-Language";
|
||||
|
||||
private final I18nProperties properties;
|
||||
private final Locale defaultLocale;
|
||||
private final Set<String> supportedLocales;
|
||||
|
||||
public I18nInterceptor(I18nProperties properties) {
|
||||
this.properties = properties;
|
||||
this.defaultLocale = LocaleParseUtil.parseLocale(properties.getDefaultLocale());
|
||||
this.supportedLocales = properties.getSupportLocales().stream()
|
||||
.map(String::toLowerCase)
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求处理前设置Locale
|
||||
*
|
||||
* @param request 请求
|
||||
* @param response 响应
|
||||
* @param handler 处理器
|
||||
* @return 是否继续处理
|
||||
*/
|
||||
@Override
|
||||
public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler) {
|
||||
try {
|
||||
// 解析Locale
|
||||
Locale locale = LocaleParseUtil.resolveFromRequest(
|
||||
request,
|
||||
properties.getHeaderName(),
|
||||
properties.getParamName(),
|
||||
supportedLocales,
|
||||
defaultLocale
|
||||
);
|
||||
// 设置到LocaleContextHolder
|
||||
LocaleContextHolder.setLocale(locale);
|
||||
// 设置到请求属性
|
||||
request.setAttribute(LOCALE_ATTRIBUTE, locale);
|
||||
request.setAttribute(LANG_ATTRIBUTE, locale.toString());
|
||||
// 添加响应头,告知客户端当前使用的语言
|
||||
response.setHeader(CONTENT_LANGUAGE_HEADER, locale.toLanguageTag());
|
||||
} catch (Exception e) {
|
||||
// 设置默认Locale
|
||||
LocaleContextHolder.setLocale(this.defaultLocale);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求完成后清理Locale上下文
|
||||
*
|
||||
* @param request 请求
|
||||
* @param response 响应
|
||||
* @param handler 处理器
|
||||
* @param ex 异常
|
||||
*/
|
||||
@Override
|
||||
public void afterCompletion(@NonNull HttpServletRequest request,
|
||||
@NonNull HttpServletResponse response,
|
||||
@NonNull Object handler,
|
||||
Exception ex) {
|
||||
// 清理LocaleContextHolder
|
||||
LocaleContextHolder.resetLocaleContext();
|
||||
// 清理请求属性
|
||||
request.removeAttribute(LOCALE_ATTRIBUTE);
|
||||
request.removeAttribute(LANG_ATTRIBUTE);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
/**
|
||||
* Copyright (c) 2018-2099, Chill Zhuang 庄骞 (bladejava@qq.com).
|
||||
* <p>
|
||||
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl.html
|
||||
* <p>
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springblade.core.i18n.props;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* I18n配置属性
|
||||
*
|
||||
* @author BladeX
|
||||
*/
|
||||
@Data
|
||||
@ConfigurationProperties(prefix = I18nProperties.PREFIX)
|
||||
public class I18nProperties {
|
||||
public static final String PREFIX = "blade.i18n";
|
||||
|
||||
/**
|
||||
* 是否启用i18n
|
||||
*/
|
||||
private boolean enabled = true;
|
||||
|
||||
/**
|
||||
* 默认locale
|
||||
*/
|
||||
private String defaultLocale = "zh_CN";
|
||||
|
||||
/**
|
||||
* 支持的locales
|
||||
*/
|
||||
private List<String> supportLocales = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* HTTP头名称
|
||||
*/
|
||||
private String headerName = "Accept-Language";
|
||||
|
||||
/**
|
||||
* 请求参数名称
|
||||
*/
|
||||
private String paramName = "lang";
|
||||
|
||||
/**
|
||||
* 消息源配置
|
||||
*/
|
||||
private MessageSource messageSource = new MessageSource();
|
||||
|
||||
/**
|
||||
* 消息源配置
|
||||
*/
|
||||
@Data
|
||||
public static class MessageSource {
|
||||
/**
|
||||
* 国际化基础名列表
|
||||
* 示例:
|
||||
* - i18n/errors
|
||||
* - i18n/messages
|
||||
*/
|
||||
private List<String> baseNames = List.of("i18n/errors", "i18n/messages");
|
||||
|
||||
/**
|
||||
* 编码
|
||||
*/
|
||||
private String encoding = "UTF-8";
|
||||
|
||||
/**
|
||||
* 缓存过期时间
|
||||
*/
|
||||
private Duration cacheDuration = Duration.ofMinutes(30);
|
||||
|
||||
/**
|
||||
* 是否使用代码作为默认消息
|
||||
*/
|
||||
private boolean useCodeAsDefaultMessage = true;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
/**
|
||||
* Copyright (c) 2018-2099, Chill Zhuang 庄骞 (bladejava@qq.com).
|
||||
* <p>
|
||||
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl.html
|
||||
* <p>
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springblade.core.i18n.resolver;
|
||||
|
||||
import jakarta.annotation.Nonnull;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.core.i18n.props.I18nProperties;
|
||||
import org.springblade.core.i18n.utils.LocaleParseUtil;
|
||||
import org.springframework.web.servlet.LocaleResolver;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* I18nLocale解析器
|
||||
*
|
||||
* @author BladeX
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class I18nLocaleResolver implements LocaleResolver {
|
||||
|
||||
private final I18nProperties properties;
|
||||
private final Locale defaultLocale;
|
||||
private final Set<String> supportedLocales;
|
||||
|
||||
public I18nLocaleResolver(I18nProperties properties) {
|
||||
this.properties = properties;
|
||||
this.defaultLocale = LocaleParseUtil.parseLocale(properties.getDefaultLocale());
|
||||
this.supportedLocales = properties.getSupportLocales().stream()
|
||||
.map(String::toLowerCase)
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Locale resolveLocale(@Nonnull HttpServletRequest request) {
|
||||
return LocaleParseUtil.resolveFromRequest(
|
||||
request,
|
||||
properties.getHeaderName(),
|
||||
properties.getParamName(),
|
||||
supportedLocales,
|
||||
defaultLocale
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLocale(@Nonnull HttpServletRequest request, HttpServletResponse response, Locale locale) {
|
||||
if (response != null && locale != null) {
|
||||
// 检查是否支持该locale
|
||||
boolean isSupported = supportedLocales.isEmpty() ||
|
||||
supportedLocales.contains(locale.toString().toLowerCase()) ||
|
||||
supportedLocales.contains(locale.getLanguage().toLowerCase());
|
||||
|
||||
if (isSupported) {
|
||||
// 可选:设置响应头提示客户端当前的 Locale
|
||||
response.setHeader(properties.getHeaderName(), locale.toLanguageTag());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,243 @@
|
|||
/**
|
||||
* Copyright (c) 2018-2099, Chill Zhuang 庄骞 (bladejava@qq.com).
|
||||
* <p>
|
||||
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl.html
|
||||
* <p>
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springblade.core.i18n.service;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.core.i18n.props.I18nProperties;
|
||||
import org.springblade.core.i18n.utils.LocaleParseUtil;
|
||||
import org.springblade.core.tool.utils.StringPool;
|
||||
import org.springblade.core.tool.utils.StringUtil;
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.context.MessageSourceResolvable;
|
||||
import org.springframework.context.NoSuchMessageException;
|
||||
import org.springframework.context.i18n.LocaleContextHolder;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.servlet.LocaleResolver;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* I18n服务实现类
|
||||
*
|
||||
* @author BladeX
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class I18nService {
|
||||
|
||||
private final MessageSource messageSource;
|
||||
private final LocaleResolver localeResolver;
|
||||
private final I18nProperties properties;
|
||||
|
||||
/**
|
||||
* 获取消息 - 核心方法
|
||||
*
|
||||
* @param code 消息代码
|
||||
* @return 国际化消息
|
||||
*/
|
||||
public String getMessage(String code) {
|
||||
return getMessage(code, null, null, getCurrentLocale());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取消息
|
||||
*
|
||||
* @param code 消息代码
|
||||
* @param args 消息参数
|
||||
* @return 国际化消息
|
||||
*/
|
||||
public String getMessage(String code, Object[] args) {
|
||||
return getMessage(code, args, null, getCurrentLocale());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取消息,带默认值
|
||||
*
|
||||
* @param code 消息代码
|
||||
* @param args 消息参数
|
||||
* @param defaultMessage 默认消息
|
||||
* @return 国际化消息
|
||||
*/
|
||||
public String getMessage(String code, Object[] args, String defaultMessage) {
|
||||
return getMessage(code, args, defaultMessage, getCurrentLocale());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取消息 - 完整版本
|
||||
*
|
||||
* @param code 消息代码
|
||||
* @param args 消息参数
|
||||
* @param defaultMessage 默认消息
|
||||
* @param locale 区域设置
|
||||
* @return 国际化消息
|
||||
*/
|
||||
public String getMessage(String code, Object[] args, String defaultMessage, Locale locale) {
|
||||
if (StringUtil.isBlank(code)) {
|
||||
return defaultMessage != null ? defaultMessage : StringPool.EMPTY;
|
||||
}
|
||||
|
||||
final Locale finalLocale = Optional.ofNullable(locale).orElseGet(this::getCurrentLocale);
|
||||
|
||||
try {
|
||||
return messageSource.getMessage(code, args, finalLocale);
|
||||
} catch (NoSuchMessageException e) {
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("No message found for code '{}' with locale '{}'", code, finalLocale);
|
||||
}
|
||||
if (defaultMessage != null) {
|
||||
return defaultMessage;
|
||||
}
|
||||
return properties.getMessageSource().isUseCodeAsDefaultMessage() ? code : StringPool.EMPTY;
|
||||
} catch (Exception e) {
|
||||
log.error("Error retrieving message for code '{}': {}", code, e.getMessage());
|
||||
return defaultMessage != null ? defaultMessage : code;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用MessageSourceResolvable获取消息
|
||||
*
|
||||
* @param resolvable MessageSourceResolvable对象
|
||||
* @param locale 区域设置
|
||||
* @return 国际化消息
|
||||
*/
|
||||
public String getMessage(MessageSourceResolvable resolvable, Locale locale) {
|
||||
final Locale finalLocale = Optional.ofNullable(locale).orElseGet(this::getCurrentLocale);
|
||||
try {
|
||||
return messageSource.getMessage(resolvable, finalLocale);
|
||||
} catch (NoSuchMessageException e) {
|
||||
log.debug("No message found for resolvable with locale '{}'", finalLocale);
|
||||
return resolvable.getDefaultMessage() != null ? resolvable.getDefaultMessage() : "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量获取消息
|
||||
*
|
||||
* @param codes 消息代码数组
|
||||
* @return 消息映射
|
||||
*/
|
||||
public Map<String, String> getMessages(String... codes) {
|
||||
return getMessages(getCurrentLocale(), codes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量获取消息,指定Locale
|
||||
*
|
||||
* @param locale 区域设置
|
||||
* @param codes 消息代码数组
|
||||
* @return 消息映射
|
||||
*/
|
||||
public Map<String, String> getMessages(Locale locale, String... codes) {
|
||||
if (codes == null || codes.length == 0) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
final Locale finalLocale = Optional.ofNullable(locale).orElseGet(this::getCurrentLocale);
|
||||
return Stream.of(codes)
|
||||
.distinct()
|
||||
.collect(Collectors.toMap(
|
||||
code -> code,
|
||||
code -> getMessage(code, null, null, finalLocale),
|
||||
(v1, v2) -> v1,
|
||||
LinkedHashMap::new
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* 向后兼容的getBatch方法
|
||||
*/
|
||||
public Map<String, String> getBatch(String... codes) {
|
||||
return getMessages(codes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前Locale
|
||||
*
|
||||
* @return 当前区域设置
|
||||
*/
|
||||
public Locale getCurrentLocale() {
|
||||
return LocaleContextHolder.getLocale();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前Locale,从HttpServletRequest解析
|
||||
*
|
||||
* @param request HTTP请求对象
|
||||
* @return 当前区域设置
|
||||
*/
|
||||
public Locale getCurrentLocale(HttpServletRequest request) {
|
||||
if (request == null) {
|
||||
return getCurrentLocale();
|
||||
}
|
||||
return localeResolver.resolveLocale(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查消息是否存在
|
||||
*
|
||||
* @param code 消息代码
|
||||
* @return 如果消息存在返回true,否则返回false
|
||||
*/
|
||||
public boolean hasMessage(String code) {
|
||||
return hasMessage(code, getCurrentLocale());
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查消息是否存在,指定Locale
|
||||
*
|
||||
* @param code 消息代码
|
||||
* @param locale 区域设置
|
||||
* @return 如果消息存在返回true,否则返回false
|
||||
*/
|
||||
public boolean hasMessage(String code, Locale locale) {
|
||||
if (!StringUtils.hasText(code)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final Locale finalLocale = Optional.ofNullable(locale).orElseGet(this::getCurrentLocale);
|
||||
|
||||
try {
|
||||
messageSource.getMessage(code, null, finalLocale);
|
||||
return true;
|
||||
} catch (NoSuchMessageException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取支持的Locales
|
||||
*
|
||||
* @return 支持的区域设置列表
|
||||
*/
|
||||
public List<Locale> getSupportLocales() {
|
||||
List<String> supportedLocaleStrings = properties.getSupportLocales();
|
||||
if (supportedLocaleStrings == null || supportedLocaleStrings.isEmpty()) {
|
||||
return Collections.singletonList(LocaleParseUtil.parseLocale(properties.getDefaultLocale()));
|
||||
}
|
||||
|
||||
return supportedLocaleStrings.stream()
|
||||
.map(LocaleParseUtil::parseLocale)
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,402 @@
|
|||
/**
|
||||
* Copyright (c) 2018-2099, Chill Zhuang 庄骞 (bladejava@qq.com).
|
||||
* <p>
|
||||
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl.html
|
||||
* <p>
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springblade.core.i18n.utils;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.core.i18n.service.I18nService;
|
||||
import org.springblade.core.tool.utils.SpringUtil;
|
||||
import org.springblade.core.tool.utils.StringPool;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* I18n静态工具类
|
||||
*
|
||||
* @author BladeX
|
||||
*/
|
||||
@Slf4j
|
||||
@UtilityClass
|
||||
public class I18nUtil {
|
||||
|
||||
/**
|
||||
* I18nService服务类
|
||||
*/
|
||||
private static volatile I18nService i18nService;
|
||||
|
||||
/**
|
||||
* 获取I18nService实例
|
||||
*
|
||||
* @return I18nService实例
|
||||
*/
|
||||
private static I18nService getI18nService() {
|
||||
if (i18nService == null) {
|
||||
synchronized (I18nUtil.class) {
|
||||
if (i18nService == null) {
|
||||
i18nService = SpringUtil.getBean(I18nService.class);
|
||||
if (i18nService == null) {
|
||||
throw new IllegalStateException("I18nService not available. Please ensure I18n is properly configured.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return i18nService;
|
||||
}
|
||||
|
||||
// ==================== 基础获取方法 ====================
|
||||
|
||||
/**
|
||||
* 获取消息(最常用)
|
||||
*
|
||||
* @param code 消息代码
|
||||
* @return 国际化消息
|
||||
*/
|
||||
public static String get(String code) {
|
||||
return Optional.ofNullable(code)
|
||||
.map(c -> getI18nService().getMessage(c))
|
||||
.orElse(StringPool.EMPTY);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取消息,带参数
|
||||
*
|
||||
* @param code 消息代码
|
||||
* @param args 消息参数数组
|
||||
* @return 国际化消息
|
||||
*/
|
||||
public static String get(String code, Object[] args) {
|
||||
return Optional.ofNullable(code)
|
||||
.map(c -> getI18nService().getMessage(c, args))
|
||||
.orElse(StringPool.EMPTY);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取消息,带参数(List形式)
|
||||
*
|
||||
* @param code 消息代码
|
||||
* @param args 消息参数列表
|
||||
* @return 国际化消息
|
||||
*/
|
||||
public static String get(String code, List<Object> args) {
|
||||
return Optional.ofNullable(code)
|
||||
.map(c -> getI18nService().getMessage(c, args != null ? args.toArray() : null))
|
||||
.orElse(StringPool.EMPTY);
|
||||
}
|
||||
|
||||
// ==================== 指定Locale的获取方法 ====================
|
||||
|
||||
/**
|
||||
* 获取消息,指定Locale
|
||||
*
|
||||
* @param code 消息代码
|
||||
* @param locale 区域设置
|
||||
* @return 国际化消息
|
||||
*/
|
||||
public static String get(String code, Locale locale) {
|
||||
return Optional.ofNullable(code)
|
||||
.map(c -> getI18nService().getMessage(c, null, null, locale))
|
||||
.orElse(StringPool.EMPTY);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取消息,带参数和Locale
|
||||
*
|
||||
* @param code 消息代码
|
||||
* @param args 消息参数数组
|
||||
* @param locale 区域设置
|
||||
* @return 国际化消息
|
||||
*/
|
||||
public static String get(String code, Object[] args, Locale locale) {
|
||||
return Optional.ofNullable(code)
|
||||
.map(c -> getI18nService().getMessage(c, args, null, locale))
|
||||
.orElse(StringPool.EMPTY);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取消息,带参数和Locale(List形式)
|
||||
*
|
||||
* @param code 消息代码
|
||||
* @param args 消息参数列表
|
||||
* @param locale 区域设置
|
||||
* @return 国际化消息
|
||||
*/
|
||||
public static String get(String code, List<Object> args, Locale locale) {
|
||||
return Optional.ofNullable(code)
|
||||
.map(c -> getI18nService().getMessage(c, args != null ? args.toArray() : null, null, locale))
|
||||
.orElse(StringPool.EMPTY);
|
||||
}
|
||||
|
||||
// ==================== 带默认值的获取方法 ====================
|
||||
|
||||
/**
|
||||
* 获取消息,如果未找到则返回默认值
|
||||
*
|
||||
* @param code 消息代码
|
||||
* @param defaultValue 默认值
|
||||
* @return 国际化消息或默认值
|
||||
*/
|
||||
public static String getOrDefault(String code, String defaultValue) {
|
||||
return Optional.ofNullable(code)
|
||||
.map(c -> {
|
||||
String message = getI18nService().getMessage(c);
|
||||
return (message != null && !message.equals(c)) ? message : defaultValue;
|
||||
})
|
||||
.orElse(defaultValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取消息,如果未找到则返回默认值,带参数
|
||||
*
|
||||
* @param code 消息代码
|
||||
* @param args 消息参数数组
|
||||
* @param defaultValue 默认值
|
||||
* @return 国际化消息或默认值
|
||||
*/
|
||||
public static String getOrDefault(String code, Object[] args, String defaultValue) {
|
||||
return Optional.ofNullable(code)
|
||||
.map(c -> {
|
||||
String message = getI18nService().getMessage(c, args);
|
||||
return (message != null && !message.equals(c)) ? message : defaultValue;
|
||||
})
|
||||
.orElse(defaultValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取消息,如果未找到则返回默认值,带参数(List形式)
|
||||
*
|
||||
* @param code 消息代码
|
||||
* @param args 消息参数列表
|
||||
* @param defaultValue 默认值
|
||||
* @return 国际化消息或默认值
|
||||
*/
|
||||
public static String getOrDefault(String code, List<Object> args, String defaultValue) {
|
||||
return Optional.ofNullable(code)
|
||||
.map(c -> {
|
||||
String message = getI18nService().getMessage(c, args != null ? args.toArray() : null);
|
||||
return (message != null && !message.equals(c)) ? message : defaultValue;
|
||||
})
|
||||
.orElse(defaultValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取消息,如果未找到则返回默认值,指定Locale
|
||||
*
|
||||
* @param code 消息代码
|
||||
* @param locale 区域设置
|
||||
* @param defaultValue 默认值
|
||||
* @return 国际化消息或默认值
|
||||
*/
|
||||
public static String getOrDefault(String code, Locale locale, String defaultValue) {
|
||||
return Optional.ofNullable(code)
|
||||
.map(c -> {
|
||||
String message = getI18nService().getMessage(c, null, null, locale);
|
||||
return (message != null && !message.equals(c)) ? message : defaultValue;
|
||||
})
|
||||
.orElse(defaultValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取消息,如果未找到则返回默认值,带参数和Locale
|
||||
*
|
||||
* @param code 消息代码
|
||||
* @param args 消息参数数组
|
||||
* @param locale 区域设置
|
||||
* @param defaultValue 默认值
|
||||
* @return 国际化消息或默认值
|
||||
*/
|
||||
public static String getOrDefault(String code, Object[] args, Locale locale, String defaultValue) {
|
||||
return Optional.ofNullable(code)
|
||||
.map(c -> {
|
||||
String message = getI18nService().getMessage(c, args, null, locale);
|
||||
return (message != null && !message.equals(c)) ? message : defaultValue;
|
||||
})
|
||||
.orElse(defaultValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取消息,如果未找到则返回默认值,带参数和Locale(List形式)
|
||||
*
|
||||
* @param code 消息代码
|
||||
* @param args 消息参数列表
|
||||
* @param locale 区域设置
|
||||
* @param defaultValue 默认值
|
||||
* @return 国际化消息或默认值
|
||||
*/
|
||||
public static String getOrDefault(String code, List<Object> args, Locale locale, String defaultValue) {
|
||||
return Optional.ofNullable(code)
|
||||
.map(c -> {
|
||||
String message = getI18nService().getMessage(c, args != null ? args.toArray() : null, null, locale);
|
||||
return (message != null && !message.equals(c)) ? message : defaultValue;
|
||||
})
|
||||
.orElse(defaultValue);
|
||||
}
|
||||
|
||||
// ==================== 检查方法 ====================
|
||||
|
||||
/**
|
||||
* 检查消息是否存在
|
||||
*
|
||||
* @param code 消息代码
|
||||
* @return 如果消息存在返回true,否则返回false
|
||||
*/
|
||||
public static boolean exists(String code) {
|
||||
if (code == null || code.trim().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
return getI18nService().hasMessage(code);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查消息是否存在,指定Locale
|
||||
*
|
||||
* @param code 消息代码
|
||||
* @param locale 区域设置
|
||||
* @return 如果消息存在返回true,否则返回false
|
||||
*/
|
||||
public static boolean exists(String code, Locale locale) {
|
||||
if (code == null || code.trim().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
return getI18nService().hasMessage(code, locale);
|
||||
}
|
||||
|
||||
// ==================== 批量获取方法 ====================
|
||||
|
||||
/**
|
||||
* 批量获取消息
|
||||
*
|
||||
* @param codes 消息代码数组
|
||||
* @return 消息代码与消息内容的映射
|
||||
*/
|
||||
public static Map<String, String> getBatch(String[] codes) {
|
||||
return getI18nService().getMessages(codes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量获取消息,指定Locale
|
||||
*
|
||||
* @param codes 消息代码数组
|
||||
* @param locale 区域设置
|
||||
* @return 消息代码与消息内容的映射
|
||||
*/
|
||||
public static Map<String, String> getBatch(String[] codes, Locale locale) {
|
||||
return getI18nService().getMessages(locale, codes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量获取消息(List形式)
|
||||
*
|
||||
* @param codes 消息代码列表
|
||||
* @return 消息代码与消息内容的映射
|
||||
*/
|
||||
public static Map<String, String> getBatch(List<String> codes) {
|
||||
return getI18nService().getMessages(codes != null ? codes.toArray(new String[0]) : null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量获取消息,指定Locale(List形式)
|
||||
*
|
||||
* @param codes 消息代码列表
|
||||
* @param locale 区域设置
|
||||
* @return 消息代码与消息内容的映射
|
||||
*/
|
||||
public static Map<String, String> getBatch(List<String> codes, Locale locale) {
|
||||
return getI18nService().getMessages(locale, codes != null ? codes.toArray(new String[0]) : null);
|
||||
}
|
||||
|
||||
// ==================== 工具方法 ====================
|
||||
|
||||
/**
|
||||
* 获取当前Locale
|
||||
*
|
||||
* @return 当前区域设置
|
||||
*/
|
||||
public static Locale getCurrentLocale() {
|
||||
return getI18nService().getCurrentLocale();
|
||||
}
|
||||
|
||||
// ==================== 简化方法 ====================
|
||||
|
||||
/**
|
||||
* 简化的消息获取方法
|
||||
*
|
||||
* @param code 消息代码
|
||||
* @return 国际化消息
|
||||
*/
|
||||
public static String $(String code) {
|
||||
return get(code);
|
||||
}
|
||||
|
||||
/**
|
||||
* 简化的消息获取方法,带参数
|
||||
*
|
||||
* @param code 消息代码
|
||||
* @param args 消息参数数组
|
||||
* @return 国际化消息
|
||||
*/
|
||||
public static String $(String code, Object[] args) {
|
||||
return get(code, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* 简化的消息获取方法,带参数(List形式)
|
||||
*
|
||||
* @param code 消息代码
|
||||
* @param args 消息参数列表
|
||||
* @return 国际化消息
|
||||
*/
|
||||
public static String $(String code, List<Object> args) {
|
||||
return get(code, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* 简化的消息获取方法,指定Locale
|
||||
*
|
||||
* @param code 消息代码
|
||||
* @param locale 区域设置
|
||||
* @return 国际化消息
|
||||
*/
|
||||
public static String $(String code, Locale locale) {
|
||||
return get(code, locale);
|
||||
}
|
||||
|
||||
/**
|
||||
* 简化的消息获取方法,带参数和Locale
|
||||
*
|
||||
* @param code 消息代码
|
||||
* @param args 消息参数数组
|
||||
* @param locale 区域设置
|
||||
* @return 国际化消息
|
||||
*/
|
||||
public static String $(String code, Object[] args, Locale locale) {
|
||||
return get(code, args, locale);
|
||||
}
|
||||
|
||||
/**
|
||||
* 简化的消息获取方法,带参数和Locale(List形式)
|
||||
*
|
||||
* @param code 消息代码
|
||||
* @param args 消息参数列表
|
||||
* @param locale 区域设置
|
||||
* @return 国际化消息
|
||||
*/
|
||||
public static String $(String code, List<Object> args, Locale locale) {
|
||||
return get(code, args, locale);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,289 @@
|
|||
/**
|
||||
* Copyright (c) 2018-2099, Chill Zhuang 庄骞 (bladejava@qq.com).
|
||||
* <p>
|
||||
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl.html
|
||||
* <p>
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springblade.core.i18n.utils;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.experimental.UtilityClass;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Locale解析工具类
|
||||
*
|
||||
* @author BladeX
|
||||
*/
|
||||
@Slf4j
|
||||
@UtilityClass
|
||||
public class LocaleParseUtil {
|
||||
|
||||
/**
|
||||
* Accept-Language header pattern
|
||||
* 用于匹配和验证locale格式(如:zh-CN, en-US)
|
||||
*/
|
||||
private static final Pattern LOCALE_PATTERN = Pattern.compile("^[a-z]{2}(-[A-Z]{2})?$");
|
||||
|
||||
/**
|
||||
* 权重分隔符
|
||||
*/
|
||||
private static final String QUALITY_VALUE_SEPARATOR = ";";
|
||||
|
||||
/**
|
||||
* Locale分隔符
|
||||
*/
|
||||
private static final String LOCALE_SEPARATOR = ",";
|
||||
|
||||
/**
|
||||
* 下划线分隔符
|
||||
*/
|
||||
private static final String UNDERSCORE = "_";
|
||||
|
||||
/**
|
||||
* 中划线分隔符
|
||||
*/
|
||||
private static final String DASH = "-";
|
||||
|
||||
/**
|
||||
* 解析Locale字符串
|
||||
* 支持多种格式:
|
||||
* 1. 简单格式:zh_CN, zh-CN, en, en-US
|
||||
* 2. Accept-Language格式:zh-CN,zh;q=0.9,en;q=0.8
|
||||
* 3. 多个locale逗号分隔:zh-CN,en-US,ja-JP
|
||||
*
|
||||
* @param localeStr locale字符串
|
||||
* @return 解析后的Locale对象,如果无法解析则返回默认Locale
|
||||
*/
|
||||
public static Locale parseLocale(String localeStr) {
|
||||
if (!StringUtils.hasText(localeStr)) {
|
||||
return Locale.getDefault();
|
||||
}
|
||||
|
||||
try {
|
||||
// 提取第一个有效的locale
|
||||
String firstLocale = extractFirstLocale(localeStr);
|
||||
|
||||
// 标准化locale格式(将下划线替换为中划线)
|
||||
String normalizedLocale = normalizeLocaleString(firstLocale);
|
||||
|
||||
// 验证locale格式
|
||||
if (isValidLocaleFormat(normalizedLocale)) {
|
||||
return Locale.forLanguageTag(normalizedLocale);
|
||||
}
|
||||
|
||||
// 如果格式不完全匹配,尝试直接解析
|
||||
return Locale.forLanguageTag(normalizedLocale);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.debug("Failed to parse locale string '{}', using default locale. Error: {}",
|
||||
localeStr, e.getMessage());
|
||||
return Locale.getDefault();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从Accept-Language或类似格式中提取第一个locale
|
||||
* 处理格式如:zh-CN,zh;q=0.9,en;q=0.8
|
||||
*
|
||||
* @param localeStr 原始locale字符串
|
||||
* @return 第一个locale字符串
|
||||
*/
|
||||
private static String extractFirstLocale(String localeStr) {
|
||||
// 去除首尾空格
|
||||
String trimmed = localeStr.trim();
|
||||
|
||||
// 如果包含逗号,说明可能是多个locale
|
||||
if (trimmed.contains(LOCALE_SEPARATOR)) {
|
||||
// 获取第一个逗号前的内容
|
||||
trimmed = trimmed.substring(0, trimmed.indexOf(LOCALE_SEPARATOR)).trim();
|
||||
}
|
||||
|
||||
// 如果包含分号(权重值),去除权重部分
|
||||
if (trimmed.contains(QUALITY_VALUE_SEPARATOR)) {
|
||||
trimmed = trimmed.substring(0, trimmed.indexOf(QUALITY_VALUE_SEPARATOR)).trim();
|
||||
}
|
||||
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 标准化locale字符串格式
|
||||
* 将下划线替换为中划线,符合BCP 47标准
|
||||
*
|
||||
* @param localeStr locale字符串
|
||||
* @return 标准化后的locale字符串
|
||||
*/
|
||||
private static String normalizeLocaleString(String localeStr) {
|
||||
if (!StringUtils.hasText(localeStr)) {
|
||||
return localeStr;
|
||||
}
|
||||
|
||||
// 将下划线替换为中划线,统一格式
|
||||
return localeStr.replace(UNDERSCORE, DASH).trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证locale格式是否有效
|
||||
* 支持格式:en, zh-CN, en-US等
|
||||
*
|
||||
* @param localeStr 标准化后的locale字符串
|
||||
* @return 是否为有效格式
|
||||
*/
|
||||
private static boolean isValidLocaleFormat(String localeStr) {
|
||||
if (!StringUtils.hasText(localeStr)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 对于简单的语言代码(如:zh, en),直接认为有效
|
||||
if (localeStr.length() == 2 && localeStr.matches("^[a-z]{2}$")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 对于完整格式(如:zh-CN),进行格式验证
|
||||
// 转换为小写-大写格式进行验证
|
||||
String[] parts = localeStr.split(DASH);
|
||||
if (parts.length == 2) {
|
||||
String normalized = parts[0].toLowerCase() + DASH + parts[1].toUpperCase();
|
||||
return LOCALE_PATTERN.matcher(normalized).matches();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全地解析Locale,永不返回null
|
||||
*
|
||||
* @param localeStr locale字符串
|
||||
* @param defaultLocale 默认Locale
|
||||
* @return 解析后的Locale对象,解析失败则返回defaultLocale
|
||||
*/
|
||||
public static Locale parseLocaleSafely(String localeStr, Locale defaultLocale) {
|
||||
if (defaultLocale == null) {
|
||||
defaultLocale = Locale.getDefault();
|
||||
}
|
||||
|
||||
if (!StringUtils.hasText(localeStr)) {
|
||||
return defaultLocale;
|
||||
}
|
||||
|
||||
try {
|
||||
Locale parsed = parseLocale(localeStr);
|
||||
return parsed != null ? parsed : defaultLocale;
|
||||
} catch (Exception e) {
|
||||
log.debug("Safe parse failed for locale '{}', returning default", localeStr);
|
||||
return defaultLocale;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从HttpServletRequest中解析Locale
|
||||
* 优先级:Header > Parameter > Default
|
||||
*
|
||||
* @param request HTTP请求对象
|
||||
* @param headerName Header参数名
|
||||
* @param paramName Query参数名
|
||||
* @param supportedLocales 支持的Locale列表
|
||||
* @param defaultLocale 默认Locale
|
||||
* @return 解析后的Locale对象
|
||||
*/
|
||||
public static Locale resolveFromRequest(HttpServletRequest request,
|
||||
String headerName,
|
||||
String paramName,
|
||||
Set<String> supportedLocales,
|
||||
Locale defaultLocale) {
|
||||
if (request == null) {
|
||||
return defaultLocale != null ? defaultLocale : Locale.getDefault();
|
||||
}
|
||||
|
||||
// 优先从Header中解析
|
||||
Locale locale = resolveFromHeader(request, headerName);
|
||||
|
||||
// 如果Header中没有,从Parameter中解析
|
||||
if (locale == null) {
|
||||
locale = resolveFromParameter(request, paramName);
|
||||
}
|
||||
|
||||
// 检查是否在支持列表中
|
||||
if (isSupported(locale, supportedLocales)) {
|
||||
return locale;
|
||||
}
|
||||
|
||||
return defaultLocale != null ? defaultLocale : Locale.getDefault();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从HTTP Header中解析Locale
|
||||
*
|
||||
* @param request HTTP请求对象
|
||||
* @param headerName Header参数名
|
||||
* @return 解析后的Locale对象,如果无法解析则返回null
|
||||
*/
|
||||
public static Locale resolveFromHeader(HttpServletRequest request, String headerName) {
|
||||
if (request == null || !StringUtils.hasText(headerName)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Optional.ofNullable(request.getHeader(headerName))
|
||||
.filter(StringUtils::hasText)
|
||||
.map(LocaleParseUtil::parseLocale)
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从请求参数中解析Locale
|
||||
*
|
||||
* @param request HTTP请求对象
|
||||
* @param paramName 参数名
|
||||
* @return 解析后的Locale对象,如果无法解析则返回null
|
||||
*/
|
||||
public static Locale resolveFromParameter(HttpServletRequest request, String paramName) {
|
||||
if (request == null || !StringUtils.hasText(paramName)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Optional.ofNullable(request.getParameter(paramName))
|
||||
.filter(StringUtils::hasText)
|
||||
.map(LocaleParseUtil::parseLocale)
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查Locale是否在支持列表中
|
||||
*
|
||||
* @param locale 待检查的Locale
|
||||
* @param supportedLocales 支持的Locale列表(小写)
|
||||
* @return 是否支持
|
||||
*/
|
||||
public static boolean isSupported(Locale locale, Set<String> supportedLocales) {
|
||||
if (locale == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 如果支持列表为空,表示支持所有locale
|
||||
if (supportedLocales == null || supportedLocales.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
String localeStr = locale.toString().toLowerCase();
|
||||
String language = locale.getLanguage().toLowerCase();
|
||||
|
||||
// 检查完整locale或仅语言代码是否在支持列表中
|
||||
return supportedLocales.contains(localeStr) || supportedLocales.contains(language);
|
||||
}
|
||||
}
|
||||
24
blade-starter-i18n/src/main/resources/i18n/errors.properties
Normal file
24
blade-starter-i18n/src/main/resources/i18n/errors.properties
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# 验证错误
|
||||
error.validation.default=参数校验失败。
|
||||
user.name.required=请输入姓名。
|
||||
user.name.size=姓名长度需在{min}到{max}个字符之间。
|
||||
user.email.invalid=请输入有效的邮箱地址。
|
||||
user.description.size=描述不能超过{max}个字符。
|
||||
|
||||
# 业务错误
|
||||
error.biz.denied=不允许的操作:{0}。
|
||||
error.auth.unauthorized=请先登录。
|
||||
error.permission.denied=您没有访问{0}的权限。
|
||||
error.resource.notfound={0} ID {1} 不存在。
|
||||
error.user.delete.admin=不能删除管理员用户。
|
||||
error.unknown=未知错误类型:{0}
|
||||
|
||||
# 服务器错误
|
||||
error.server.internal=服务器内部错误,请稍后重试。
|
||||
error.server.busy=服务器繁忙,请稍后重试。
|
||||
error.server.maintenance=系统维护中。
|
||||
|
||||
# 数据错误
|
||||
error.data.duplicate=数据已存在。
|
||||
error.data.invalid=数据格式无效。
|
||||
error.data.notfound=数据不存在。
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
# Validation errors
|
||||
error.validation.default=Validation failed.
|
||||
user.name.required=Name is required.
|
||||
user.name.size=Name length must be between {min} and {max} characters.
|
||||
user.email.invalid=Please provide a valid email address.
|
||||
user.description.size=Description cannot exceed {max} characters.
|
||||
|
||||
# Business errors
|
||||
error.biz.denied=Operation {0} is not allowed.
|
||||
error.auth.unauthorized=Authentication required.
|
||||
error.permission.denied=You don't have permission to access {0}.
|
||||
error.resource.notfound={0} with ID {1} not found.
|
||||
error.user.delete.admin=Cannot delete admin user.
|
||||
error.unknown=Unknown error type: {0}
|
||||
|
||||
# Server errors
|
||||
error.server.internal=Internal server error. Please try again later.
|
||||
error.server.busy=Server is busy. Please try again later.
|
||||
error.server.maintenance=System is under maintenance.
|
||||
|
||||
# Data errors
|
||||
error.data.duplicate=Duplicate data found.
|
||||
error.data.invalid=Invalid data format.
|
||||
error.data.notfound=Data not found.
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
# 验证错误
|
||||
error.validation.default=参数校验失败。
|
||||
user.name.required=请输入姓名。
|
||||
user.name.size=姓名长度需在{min}到{max}个字符之间。
|
||||
user.email.invalid=请输入有效的邮箱地址。
|
||||
user.description.size=描述不能超过{max}个字符。
|
||||
|
||||
# 业务错误
|
||||
error.biz.denied=不允许的操作:{0}。
|
||||
error.auth.unauthorized=请先登录。
|
||||
error.permission.denied=您没有访问{0}的权限。
|
||||
error.resource.notfound={0} ID {1} 不存在。
|
||||
error.user.delete.admin=不能删除管理员用户。
|
||||
error.unknown=未知错误类型:{0}
|
||||
|
||||
# 服务器错误
|
||||
error.server.internal=服务器内部错误,请稍后重试。
|
||||
error.server.busy=服务器繁忙,请稍后重试。
|
||||
error.server.maintenance=系统维护中。
|
||||
|
||||
# 数据错误
|
||||
error.data.duplicate=数据已存在。
|
||||
error.data.invalid=数据格式无效。
|
||||
error.data.notfound=数据不存在。
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
# 驗證錯誤
|
||||
error.validation.default=參數校驗失敗。
|
||||
user.name.required=請輸入姓名。
|
||||
user.name.size=姓名長度需在{min}到{max}個字元之間。
|
||||
user.email.invalid=請輸入有效的電子郵件地址。
|
||||
user.description.size=描述不能超過{max}個字元。
|
||||
|
||||
# 業務錯誤
|
||||
error.biz.denied=不允許的操作:{0}。
|
||||
error.auth.unauthorized=請先登入。
|
||||
error.permission.denied=您沒有存取{0}的權限。
|
||||
error.resource.notfound={0} ID {1} 不存在。
|
||||
error.user.delete.admin=不能刪除管理員使用者。
|
||||
error.unknown=未知錯誤類型:{0}
|
||||
|
||||
# 伺服器錯誤
|
||||
error.server.internal=伺服器內部錯誤,請稍後重試。
|
||||
error.server.busy=伺服器忙碌中,請稍後重試。
|
||||
error.server.maintenance=系統維護中。
|
||||
|
||||
# 資料錯誤
|
||||
error.data.duplicate=資料已存在。
|
||||
error.data.invalid=資料格式無效。
|
||||
error.data.notfound=資料不存在。
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
# 问候消息
|
||||
greeting.message=你好,{0}!
|
||||
greeting.welcome=欢迎使用我们的应用
|
||||
|
||||
# 用户消息
|
||||
user.create.success=用户 {0} 创建成功。
|
||||
user.update.success=ID为 {1} 的用户 {0} 更新成功。
|
||||
user.delete.success=ID为 {0} 的用户删除成功。
|
||||
user.notfound=用户未找到
|
||||
|
||||
# 成功消息
|
||||
operation.success=操作成功完成
|
||||
save.success=数据保存成功
|
||||
delete.success=数据删除成功
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
# Greeting messages
|
||||
greeting.message=Hello, {0}!
|
||||
greeting.welcome=Welcome to our application
|
||||
|
||||
# User messages
|
||||
user.create.success=User {0} created successfully.
|
||||
user.update.success=User {0} with ID {1} updated successfully.
|
||||
user.delete.success=User with ID {0} deleted successfully.
|
||||
user.notfound=User not found
|
||||
|
||||
# Success messages
|
||||
operation.success=Operation completed successfully
|
||||
save.success=Data saved successfully
|
||||
delete.success=Data deleted successfully
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
# 问候消息
|
||||
greeting.message=你好,{0}!
|
||||
greeting.welcome=欢迎使用我们的应用
|
||||
|
||||
# 用户消息
|
||||
user.create.success=用户 {0} 创建成功。
|
||||
user.update.success=ID为 {1} 的用户 {0} 更新成功。
|
||||
user.delete.success=ID为 {0} 的用户删除成功。
|
||||
user.notfound=用户未找到
|
||||
|
||||
# 成功消息
|
||||
operation.success=操作成功完成
|
||||
save.success=数据保存成功
|
||||
delete.success=数据删除成功
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
# 問候訊息
|
||||
greeting.message=你好,{0}!
|
||||
greeting.welcome=歡迎使用我們的應用
|
||||
|
||||
# 使用者訊息
|
||||
user.create.success=使用者 {0} 建立成功。
|
||||
user.update.success=ID為 {1} 的使用者 {0} 更新成功。
|
||||
user.delete.success=ID為 {0} 的使用者刪除成功。
|
||||
user.notfound=使用者未找到
|
||||
|
||||
# 成功訊息
|
||||
operation.success=操作成功完成
|
||||
save.success=資料儲存成功
|
||||
delete.success=資料刪除成功
|
||||
6
pom.xml
6
pom.xml
|
|
@ -86,6 +86,7 @@
|
|||
<module>blade-starter-datascope</module>
|
||||
<module>blade-starter-develop</module>
|
||||
<module>blade-starter-excel</module>
|
||||
<module>blade-starter-i18n</module>
|
||||
<module>blade-starter-loadbalancer</module>
|
||||
<module>blade-starter-log</module>
|
||||
<module>blade-starter-mybatis</module>
|
||||
|
|
@ -223,6 +224,11 @@
|
|||
<artifactId>blade-starter-transaction</artifactId>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springblade</groupId>
|
||||
<artifactId>blade-starter-i18n</artifactId>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springblade</groupId>
|
||||
<artifactId>blade-starter-loadbalancer</artifactId>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user