init
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
<?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>BladeX-Tool</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>blade-starter-i18n</artifactId>
|
||||
<name>${project.artifactId}</name>
|
||||
<version>${project.parent.version}</version>
|
||||
<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>
|
||||
<!-- Auto -->
|
||||
<dependency>
|
||||
<groupId>org.springblade</groupId>
|
||||
<artifactId>blade-core-auto</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
+137
@@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
+46
@@ -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();
|
||||
}
|
||||
}
|
||||
+120
@@ -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;
|
||||
}
|
||||
}
|
||||
+88
@@ -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);
|
||||
}
|
||||
}
|
||||
+299
@@ -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=資料刪除成功
|
||||
Reference in New Issue
Block a user