This commit is contained in:
kk
2026-07-07 18:01:21 +08:00
commit b259f94d4b
1088 changed files with 121778 additions and 0 deletions
@@ -0,0 +1,137 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.i18n.config;
import jakarta.annotation.Nonnull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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.i18n.interceptor.I18nInterceptor;
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.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
@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,46 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.i18n.config;
import org.springblade.core.i18n.props.I18nProperties;
import org.springblade.core.launch.props.BladePropertySource;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
/**
* I18n自动配置类
*
* @author BladeX
*/
@AutoConfiguration
@EnableConfigurationProperties(I18nProperties.class)
@ConditionalOnProperty(prefix = I18nProperties.PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true)
@BladePropertySource(value = "classpath:/blade-i18n.yml")
public class I18nConfiguration {
}
@@ -0,0 +1,205 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.i18n.enums;
import org.springblade.core.i18n.utils.I18nUtil;
import java.util.Locale;
/**
* 国际化枚举基础接口
* <p>
* 为枚举类提供完整的国际化支持能力。实现接口并提供 {@link #getCode()} 方法,
* 即可自动获得所有国际化消息获取功能。
* </p>
*
* <h3>设计特点:</h3>
* <ul>
* <li>提供完整的getMessage和getMessageOrDefault方法族</li>
* <li>支持参数化消息和多语言</li>
* <li>建议覆盖toString方法返回国际化消息</li>
* </ul>
*
* <h3>实现示例:</h3>
* <pre>{@code
* @Getter
* @AllArgsConstructor
* public enum BladeMessage implements I18nEnum {
* HELLO("hello.world"),
* WELCOME("welcome.message");
*
* private final String code;
*
* @Override
* public String toString() {
* return getMessage();
* }
* }
*
* // 使用示例
* String msg = BladeMessage.HELLO.getMessage();
* String paramMsg = BladeMessage.WELCOME.getMessage("张三");
* String enMsg = BladeMessage.HELLO.getMessage(Locale.ENGLISH);
* "结果:" + BladeMessage.HELLO // 自动显示国际化消息
* }</pre>
*
* @author BladeX
*/
public interface I18nEnum {
/**
* 获取国际化消息代码
* <p>
* 这是唯一需要实现的抽象方法,返回的代码将作为国际化资源文件的key。
* </p>
*
* @return 国际化消息代码
*/
String getCode();
// ==================== 基础getMessage方法 ====================
/**
* 获取当前语言环境下的国际化消息
*
* @return 国际化后的消息内容
*/
default String getMessage() {
return I18nUtil.get(getCode());
}
/**
* 获取带参数的国际化消息
* <p>
* 参数将替换消息模板中的占位符,支持以下格式:
* <ul>
* <li>MessageFormat格式:{0}, {1}, {2}...</li>
* <li>Spring格式:{min}, {max}, {name}...</li>
* </ul>
* </p>
*
* @param args 消息参数,将替换消息模板中的占位符
* @return 格式化后的国际化消息
*/
default String getMessage(Object... args) {
return I18nUtil.get(getCode(), args);
}
/**
* 获取指定语言环境下的国际化消息
*
* @param locale 目标语言环境
* @return 指定语言的国际化消息
*/
default String getMessage(Locale locale) {
return I18nUtil.get(getCode(), locale);
}
/**
* 获取带参数的指定语言环境下的国际化消息
*
* @param args 消息参数
* @param locale 目标语言环境
* @return 格式化后的指定语言国际化消息
*/
default String getMessage(Object[] args, Locale locale) {
return I18nUtil.get(getCode(), args, locale);
}
// ==================== getMessageOrDefault方法 ====================
/**
* 获取带默认值的国际化消息
* <p>
* 当消息未找到或获取失败时,返回提供的默认值。
* </p>
*
* @param defaultValue 当消息未找到时返回的默认值
* @return 国际化消息或默认值
*/
default String getMessageOrDefault(String defaultValue) {
return I18nUtil.getOrDefault(getCode(), defaultValue);
}
/**
* 获取带参数和默认值的国际化消息
*
* @param args 消息参数
* @param defaultValue 默认值
* @return 格式化后的国际化消息或默认值
*/
default String getMessageOrDefault(Object[] args, String defaultValue) {
return I18nUtil.getOrDefault(getCode(), args, defaultValue);
}
/**
* 获取带默认值的指定语言环境下的国际化消息
*
* @param locale 目标语言环境
* @param defaultValue 默认值
* @return 指定语言的国际化消息或默认值
*/
default String getMessageOrDefault(Locale locale, String defaultValue) {
return I18nUtil.getOrDefault(getCode(), locale, defaultValue);
}
/**
* 获取带参数和默认值的指定语言环境下的国际化消息
*
* @param args 消息参数
* @param locale 目标语言环境
* @param defaultValue 默认值
* @return 格式化后的指定语言国际化消息或默认值
*/
default String getMessageOrDefault(Object[] args, Locale locale, String defaultValue) {
return I18nUtil.getOrDefault(getCode(), args, locale, defaultValue);
}
// ==================== 存在性检查方法 ====================
/**
* 检查消息是否存在
* <p>
* 用于在获取消息前验证消息是否已定义,避免返回未翻译的代码。
* </p>
*
* @return 如果消息存在返回true,否则返回false
*/
default boolean exists() {
return I18nUtil.exists(getCode());
}
/**
* 检查指定语言环境下的消息是否存在
*
* @param locale 目标语言环境
* @return 如果消息存在返回true,否则返回false
*/
default boolean exists(Locale locale) {
return I18nUtil.exists(getCode(), locale);
}
}
@@ -0,0 +1,193 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.i18n.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 国际化错误消息枚举类
* <p>
* 包含系统中所有错误相关的国际化消息代码。
* 通过实现 {@link I18nEnum} 接口,自动获得完整的国际化消息功能。
* </p>
*
* <h3>使用示例:</h3>
* <pre>{@code
* // 直接获取消息
* String message = I18nError.USER_NAME_REQUIRED.getMessage();
*
* // 带参数获取消息
* String sizeError = I18nError.USER_NAME_SIZE.getMessage(2, 50);
*
* // 指定Locale获取消息
* String englishMessage = I18nError.ERROR_AUTH_UNAUTHORIZED.getMessage(Locale.ENGLISH);
*
* // 带参数和Locale获取消息
* String permissionError = I18nError.ERROR_PERMISSION_DENIED.getMessage(
* new Object[]{"admin"}, Locale.CHINESE);
*
* // 使用静态方法
* String errorMsg = I18nError.of("error.validation.default").getMessage();
* }</pre>
*
* @author BladeX
*/
@Getter
@AllArgsConstructor
public enum I18nError implements I18nEnum {
// ==================== 验证错误 ====================
/**
* 参数校验失败
*/
ERROR_VALIDATION_DEFAULT("error.validation.default"),
/**
* 请输入姓名
*/
USER_NAME_REQUIRED("user.name.required"),
/**
* 姓名长度需在{min}到{max}个字符之间
*/
USER_NAME_SIZE("user.name.size"),
/**
* 请输入有效的邮箱地址
*/
USER_EMAIL_INVALID("user.email.invalid"),
/**
* 描述不能超过{max}个字符
*/
USER_DESCRIPTION_SIZE("user.description.size"),
// ==================== 业务错误 ====================
/**
* 不允许的操作:{0}
*/
ERROR_BIZ_DENIED("error.biz.denied"),
/**
* 请先登录
*/
ERROR_AUTH_UNAUTHORIZED("error.auth.unauthorized"),
/**
* 您没有访问{0}的权限
*/
ERROR_PERMISSION_DENIED("error.permission.denied"),
/**
* {0} ID {1} 不存在
*/
ERROR_RESOURCE_NOTFOUND("error.resource.notfound"),
/**
* 不能删除管理员用户
*/
ERROR_USER_DELETE_ADMIN("error.user.delete.admin"),
/**
* 未知错误类型:{0}
*/
ERROR_UNKNOWN("error.unknown"),
// ==================== 服务器错误 ====================
/**
* 服务器内部错误,请稍后重试
*/
ERROR_SERVER_INTERNAL("error.server.internal"),
/**
* 服务器繁忙,请稍后重试
*/
ERROR_SERVER_BUSY("error.server.busy"),
/**
* 系统维护中
*/
ERROR_SERVER_MAINTENANCE("error.server.maintenance"),
// ==================== 数据错误 ====================
/**
* 数据已存在
*/
ERROR_DATA_DUPLICATE("error.data.duplicate"),
/**
* 数据格式无效
*/
ERROR_DATA_INVALID("error.data.invalid"),
/**
* 数据不存在
*/
ERROR_DATA_NOTFOUND("error.data.notfound");
/**
* 国际化消息代码
*/
private final String code;
// ==================== 静态工具方法 ====================
/**
* 根据消息代码创建枚举实例的静态工厂方法
* <p>
* 用于动态创建枚举实例,适用于消息代码在运行时确定的场景
* </p>
*
* @param code 消息代码
* @return 匹配的枚举实例,如果没有匹配则返回null
*/
public static I18nError of(String code) {
if (code == null || code.trim().isEmpty()) {
return null;
}
for (I18nError error : values()) {
if (error.getCode().equals(code)) {
return error;
}
}
return null;
}
/**
* 返回国际化消息
* <p>
* 重写toString方法,使枚举实例可以直接在字符串拼接或日志输出时
* 自动显示国际化后的消息内容。
* </p>
*
* @return 当前语言环境下的国际化消息
*/
@Override
public String toString() {
return getMessage();
}
}
@@ -0,0 +1,152 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.i18n.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 国际化消息枚举类
* <p>
* 包含系统中所有业务相关的国际化消息代码。
* 通过实现 {@link I18nEnum} 接口,自动获得完整的国际化消息功能。
* </p>
*
* <h3>使用示例:</h3>
* <pre>{@code
* // 直接获取消息
* String message = I18nMessage.OPERATION_SUCCESS.getMessage();
*
* // 带参数获取消息
* String createMsg = I18nMessage.USER_CREATE_SUCCESS.getMessage("张三");
*
* // 指定Locale获取消息
* String englishMsg = I18nMessage.GREETING_WELCOME.getMessage(Locale.ENGLISH);
*
* // 带参数和Locale获取消息
* String updateMsg = I18nMessage.USER_UPDATE_SUCCESS.getMessage(
* new Object[]{"李四", 123}, Locale.CHINESE);
*
* // 使用静态方法
* String msg = I18nMessage.of("greeting.welcome").getMessage();
* }</pre>
*
* @author BladeX
*/
@Getter
@AllArgsConstructor
public enum I18nMessage implements I18nEnum {
// ==================== 问候消息 ====================
/**
* 你好,{0}
*/
GREETING_MESSAGE("greeting.message"),
/**
* 欢迎使用我们的应用
*/
GREETING_WELCOME("greeting.welcome"),
// ==================== 用户消息 ====================
/**
* 用户 {0} 创建成功
*/
USER_CREATE_SUCCESS("user.create.success"),
/**
* ID为 {1} 的用户 {0} 更新成功
*/
USER_UPDATE_SUCCESS("user.update.success"),
/**
* ID为 {0} 的用户删除成功
*/
USER_DELETE_SUCCESS("user.delete.success"),
/**
* 用户未找到
*/
USER_NOTFOUND("user.notfound"),
// ==================== 成功消息 ====================
/**
* 操作成功完成
*/
OPERATION_SUCCESS("operation.success"),
/**
* 数据保存成功
*/
SAVE_SUCCESS("save.success"),
/**
* 数据删除成功
*/
DELETE_SUCCESS("delete.success");
/**
* 国际化消息代码
*/
private final String code;
// ==================== 静态工具方法 ====================
/**
* 根据消息代码创建枚举实例的静态工厂方法
* <p>
* 用于动态创建枚举实例,适用于消息代码在运行时确定的场景
* </p>
*
* @param code 消息代码
* @return 匹配的枚举实例,如果没有匹配则返回null
*/
public static I18nMessage of(String code) {
if (code == null || code.trim().isEmpty()) {
return null;
}
for (I18nMessage message : values()) {
if (message.getCode().equals(code)) {
return message;
}
}
return null;
}
/**
* 返回国际化消息
* <p>
* 重写toString方法,使枚举实例可以直接在字符串拼接或日志输出时
* 自动显示国际化后的消息内容。
* </p>
*
* @return 当前语言环境下的国际化消息
*/
@Override
public String toString() {
return getMessage();
}
}
@@ -0,0 +1,120 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
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,103 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
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,88 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
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,253 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
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,412 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
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,299 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
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);
}
}
@@ -0,0 +1,25 @@
blade:
i18n:
# 是否启用i18n
enabled: true
# 默认locale
default-locale: zh_CN
# 支持的locales
support-locales:
- zh_CN # 简体中文(中国)
- zh_TW # 繁体中文(台湾省)
- en_US # 英语(美国)
- en_GB # 英语(英国)
- fr_FR # 法语(法国)
- de_DE # 德语(德国)
- ru_RU # 俄语(俄罗斯)
- it_IT # 意语(意大利)
# 消息源配置
message-source:
# 配置
base-names:
- i18n/errors
- i18n/messages
# 编码
encoding: UTF-8
@@ -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 @@
# Validierungsfehler
error.validation.default=Parametervalidierung fehlgeschlagen.
user.name.required=Bitte geben Sie einen Namen ein.
user.name.size=Der Name muss zwischen {min} und {max} Zeichen lang sein.
user.email.invalid=Bitte geben Sie eine gültige E-Mail-Adresse ein.
user.description.size=Die Beschreibung darf {max} Zeichen nicht überschreiten.
# Geschäftsfehler
error.biz.denied=Operation nicht erlaubt: {0}.
error.auth.unauthorized=Bitte melden Sie sich zuerst an.
error.permission.denied=Sie haben keine Berechtigung für den Zugriff auf {0}.
error.resource.notfound={0} mit ID {1} existiert nicht.
error.user.delete.admin=Administrator-Benutzer kann nicht gelöscht werden.
error.unknown=Unbekannter Fehlertyp: {0}
# Serverfehler
error.server.internal=Interner Serverfehler. Bitte versuchen Sie es später erneut.
error.server.busy=Server ist ausgelastet. Bitte versuchen Sie es später erneut.
error.server.maintenance=System wird gewartet.
# Datenfehler
error.data.duplicate=Daten existieren bereits.
error.data.invalid=Ungültiges Datenformat.
error.data.notfound=Daten nicht gefunden.
@@ -0,0 +1,24 @@
# Validation Errors
error.validation.default=Parameter validation failed.
user.name.required=Please enter a name.
user.name.size=Name must be between {min} and {max} characters.
user.email.invalid=Please enter a valid email address.
user.description.size=Description cannot exceed {max} characters.
# Business Errors
error.biz.denied=Operation not permitted: {0}.
error.auth.unauthorized=Please log in first.
error.permission.denied=You do not have permission to access {0}.
error.resource.notfound={0} with ID {1} does not exist.
error.user.delete.admin=Cannot delete administrator 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=Data already exists.
error.data.invalid=Invalid data format.
error.data.notfound=Data not found.
@@ -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 @@
# Erreurs de validation
error.validation.default=Échec de la validation des paramètres.
user.name.required=Veuillez saisir un nom.
user.name.size=Le nom doit contenir entre {min} et {max} caractères.
user.email.invalid=Veuillez saisir une adresse e-mail valide.
user.description.size=La description ne peut pas dépasser {max} caractères.
# Erreurs métier
error.biz.denied=Opération non autorisée : {0}.
error.auth.unauthorized=Veuillez vous connecter d'abord.
error.permission.denied=Vous n'avez pas l'autorisation d'accéder à {0}.
error.resource.notfound={0} avec l'ID {1} n'existe pas.
error.user.delete.admin=Impossible de supprimer l'utilisateur administrateur.
error.unknown=Type d'erreur inconnu : {0}
# Erreurs serveur
error.server.internal=Erreur interne du serveur. Veuillez réessayer plus tard.
error.server.busy=Le serveur est occupé. Veuillez réessayer plus tard.
error.server.maintenance=Système en maintenance.
# Erreurs de données
error.data.duplicate=Les données existent déjà.
error.data.invalid=Format de données invalide.
error.data.notfound=Données introuvables.
@@ -0,0 +1,24 @@
# Errori di validazione
error.validation.default=Validazione dei parametri fallita.
user.name.required=Inserisci il nome.
user.name.size=La lunghezza del nome deve essere tra {min} e {max} caratteri.
user.email.invalid=Inserisci un indirizzo email valido.
user.description.size=La descrizione non può superare {max} caratteri.
# Errori di business
error.biz.denied=Operazione non consentita: {0}.
error.auth.unauthorized=Effettua il login prima di procedere.
error.permission.denied=Non hai i permessi per accedere a {0}.
error.resource.notfound={0} con ID {1} non esiste.
error.user.delete.admin=Non è possibile eliminare l'utente amministratore.
error.unknown=Tipo di errore sconosciuto: {0}
# Errori del server
error.server.internal=Errore interno del server, riprova più tardi.
error.server.busy=Il server è occupato, riprova più tardi.
error.server.maintenance=Sistema in manutenzione.
# Errori di dati
error.data.duplicate=I dati esistono già.
error.data.invalid=Formato dati non valido.
error.data.notfound=I dati non esistono.
@@ -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,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 @@
# Begrüßungsnachrichten
greeting.message=Hallo, {0}!
greeting.welcome=Willkommen in unserer Anwendung
# Benutzernachrichten
user.create.success=Benutzer {0} wurde erfolgreich erstellt.
user.update.success=Benutzer {0} mit ID {1} wurde erfolgreich aktualisiert.
user.delete.success=Benutzer mit ID {0} wurde erfolgreich gelöscht.
user.notfound=Benutzer nicht gefunden
# Erfolgsmeldungen
operation.success=Vorgang erfolgreich abgeschlossen
save.success=Daten erfolgreich gespeichert
delete.success=Daten erfolgreich gelöscht
@@ -0,0 +1,14 @@
# Greeting Messages
greeting.message=Hello, {0}!
greeting.welcome=Welcome to our application
# User Messages
user.create.success=User {0} has been successfully created.
user.update.success=User {0} with ID {1} has been successfully updated.
user.delete.success=User with ID {0} has been successfully deleted.
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 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 @@
# Messages de salutation
greeting.message=Bonjour, {0} !
greeting.welcome=Bienvenue dans notre application
# Messages utilisateur
user.create.success=L'utilisateur {0} a été créé avec succès.
user.update.success=L'utilisateur {0} avec l'ID {1} a été mis à jour avec succès.
user.delete.success=L'utilisateur avec l'ID {0} a été supprimé avec succès.
user.notfound=Utilisateur introuvable
# Messages de succès
operation.success=Opération terminée avec succès
save.success=Données enregistrées avec succès
delete.success=Données supprimées avec succès
@@ -0,0 +1,14 @@
# Messaggi di saluto
greeting.message=Ciao, {0}!
greeting.welcome=Benvenuto nella nostra applicazione
# Messaggi utente
user.create.success=L'utente {0} è stato creato con successo.
user.update.success=L'utente {0} con ID {1} è stato aggiornato con successo.
user.delete.success=L'utente con ID {0} è stato eliminato con successo.
user.notfound=Utente non trovato
# Messaggi di successo
operation.success=Operazione completata con successo
save.success=Dati salvati con successo
delete.success=Dati eliminati con successo
@@ -0,0 +1,14 @@
# Приветственные сообщения
greeting.message=Здравствуйте, {0}!
greeting.welcome=Добро пожаловать в наше приложение
# Сообщения пользователя
user.create.success=Пользователь {0} успешно создан.
user.update.success=Пользователь {0} с ID {1} успешно обновлен.
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=数据删除成功
@@ -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=資料刪除成功