15
0

i18n.md 9.9 KB

Poyee 国际化(i18n)功能说明

1. 概述

Poyee 框架提供了完善的国际化(i18n)支持,使应用能够根据用户的语言偏好自动切换界面文本、错误消息和业务数据。国际化功能主要由 com.poyee.i18n 包和相关注解、工具类组成,支持多语言消息配置、动态语言切换和国际化数据处理。

2. 核心组件

2.1 i18n 注解

i18n 注解用于标记需要国际化的字段、方法或类。

@Target({ElementType.FIELD, ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface i18n {
    // 是否启用国际化
    boolean value() default true;
    // Excel导出时的sheet名称
    String sheetName() default "";
    // 国际化格式
    I18nFormat[] format() default {};
}

2.2 I18nUtils 工具类

I18nUtils 是国际化功能的核心工具类,提供了获取国际化消息的方法。

@Configuration
public class I18nUtils {
    // 定义占位符的正则表达式模式
    private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("\\{(\\d+)\\}");
    
    private MessageProperties messageProperties;
    
    // 存储消息的Map,键是消息键,值是包含语言和消息的Map
    private static Map<String, Map<String, String>> messages;
    
    // 初始化国际化工具类,并加载消息属性
    @Autowired
    public I18nUtils(MessageProperties messageProperties) {
        this.messageProperties = messageProperties;
        messages = messageProperties.init();
    }
    
    // 根据枚举和参数获取国际化消息
    public static String get(I18nMessageEnums i18n, Object... args) {
        String key = i18n.getCode();
        Map<String, String> translations = messages.get(key);
        String message;
        if (Objects.isNull(translations)) {
            // 如果没有找到对应的翻译,使用枚举中的默认消息
            message = i18n.getMessage();
        } else {
            // 根据当前的locale获取对应的翻译
            Locale locale = LocaleContextHolder.getLocale();
            message = translations.getOrDefault(locale.getLanguage(), i18n.getLang());
        }

        // 替换消息中的占位符
        return replacePlaceholders(message, args);
    }
    
    // 其他辅助方法...
}

2.3 MessageProperties 配置类

MessageProperties 负责从配置文件中加载国际化消息。

@Component
public class MessageProperties {

    @Autowired
    private ResourceLoader resourceLoader;

    public Map<String, Map<String, String>> init() {
        return loadMessagesFromYaml();
    }

    public Map<String, Map<String, String>> loadMessagesFromYaml() {
        Resource resource = resourceLoader.getResource("classpath:I18n/message.yml");
        try (InputStream input = resource.getInputStream()) {
            Yaml yaml = new Yaml();
            Map map = yaml.loadAs(input, Map.class);
            return map;
        } catch (Exception e) {
            throw new RuntimeException("Failed to load message.yml", e);
        }
    }
}

2.4 I18nMessageEnums 枚举

I18nMessageEnums 定义了系统中所有的国际化消息。

@Getter
public enum I18nMessageEnums {
    // 请求成功
    SUCCESS("success", "成功", "zh"),
    // 请求失败
    REQUEST_ERROR("request_error", "请求失败", "zh"),
    // 无权操作
    NO_PERMISSION("no_permission", "无权操作{0}", "zh"),
    // 更多消息...
    
    private String code; // 多语言标识
    private String message; // 默认信息
    private String lang; // 默认语言
    
    I18nMessageEnums(String code, String message, String lang) {
        this.code = code;
        this.message = message;
        this.lang = lang;
    }
}

2.5 i18nAspect 切面类

i18nAspect 是一个切面类,用于处理国际化相关的逻辑。

@Slf4j
@Aspect
@Component
public class i18nAspect {

    // 配置织入点,针对使用了i18n注解的方法
    @Pointcut("@annotation(com.poyee.annotation.I18n)")
    public void logPointCut() {
    }

    // 在控制器的方法执行前,处理国际化逻辑
    @Before("execution(public * com.poyee.controller.*.*(..))")
    public void doBefore(JoinPoint joinPoint) {
        Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method method = methodSignature.getMethod();
        checkI18n(method);
    }

    // 判断是否支持国际化,并根据情况设置会话属性
    private void checkI18n(Method method) {
        if (method.isAnnotationPresent(i18n.class)) {
            i18n i18n = method.getAnnotation(i18n.class);
            HttpServletRequest request = ServletUtils.getRequest();
            String language = request.getHeader("language");
            boolean isI18n = true;
            if(StringUtils.isBlank(language)) {
                language = LocaleContextHolder.getLocale().getLanguage();
                isI18n = false;
            }

            try {
                HttpSession session = ServletUtils.getSession(false);
                if (session == null) {
                    log.warn("Session is null, cannot set I18n attributes.");
                    return;
                }

                // 根据方法上的i18n注解和请求头中的语言信息,决定是否启用国际化
                if(i18n.value() && isI18n) {
                    session.setAttribute("language", language);
                    I18nFormat[] format = i18n.format();
                    if(format.length > 0) {
                        session.setAttribute("i18nFormat", i18n.format());
                        if (log.isInfoEnabled()) {
                            log.info("i18nFormat:{}", format[0].getMsg());
                        }
                    }
                    session.setAttribute("i18n", true);
                } else {
                    // 如果 I18n.value() 为 false,但 isI18n 为 true,仍不启用国际化
                    session.setAttribute("i18n", false);
                }
            } catch (Exception e) {
                log.error("Failed to set I18n attributes", e);
            }
        }
    }
}

2.6 I18nFormat 枚举

I18nFormat 定义了国际化格式的类型。

@Getter
public enum I18nFormat {

    INSERT("新增"),
    UPDATE("编辑"),
    DELETE("删除"),
    SEARCH("查询"),
    EXPORT("导出"),
    IMPORT("导入")
    ;

    private String msg;

    I18nFormat(String msg) {
        this.msg = msg;
    }
}

3. 国际化配置文件

国际化消息配置文件位于 classpath:i18n/message.yml,采用 YAML 格式,结构如下:

success:
  en: "Success"
  zh: "成功"
  
request_error:
  en: "Request failed"
  zh: "请求失败"
  
no_permission:
  en: "No permission to operate {0}"
  zh: "无权操作{0}"

# 更多消息...

4. 使用方法

4.1 在控制器中启用国际化

在控制器方法上添加 @i18n 注解,启用国际化支持:

@RestController
@RequestMapping("/api/users")
public class UserController {

    @GetMapping("/list")
    @i18n(format = {I18nFormat.SEARCH})
    public Result<List<UserDto>> listUsers() {
        // 业务逻辑...
        return Result.success(userList);
    }
    
    @PostMapping("/create")
    @i18n(format = {I18nFormat.INSERT})
    public Result<Void> createUser(@RequestBody UserCreateReq req) {
        // 业务逻辑...
        return Result.success();
    }
}

4.2 获取国际化消息

使用 I18nUtils.get() 方法获取国际化消息:

// 简单消息
String successMsg = I18nUtils.get(SUCCESS);

// 带参数的消息
String noPermissionMsg = I18nUtils.get(NO_PERMISSION, "用户管理");

// 在异常中使用
throw new ServiceException(I18nUtils.get(DATA_NOT_EXIST, "用户"));

// 在返回结果中使用
return Result.error(I18nUtils.get(PARAM_ERROR));

4.3 标记国际化字段

在实体类或DTO中,使用 @i18n 注解标记需要国际化的字段:

@Data
public class ProductDto extends BaseDto {
    private Integer id;
    private String code;
    
    @TableField("name")
    private String name;
    
    @i18n
    @TableField("name_i18n")
    private String nameI18n;
    
    // 其他字段...
}

4.4 在服务层处理国际化字段

在服务实现类中,可以根据是否启用国际化来决定是否查询国际化字段:

public class ProductServiceImpl extends BaseServiceImpl<ProductMapper, ProductReq, ProductDto> implements ProductService {

    @Override
    public Result<Page<ProductDto>> page(ProductPageReq req) {
        MPJLambdaWrapper<ProductDto> wrapper = new MPJLambdaWrapper<>();
        wrapper.selectAll(ProductDto.class);
        
        // 根据是否启用国际化,决定是否查询国际化字段
        checkI18n(wrapper, ProductDto.class);
        
        // 其他查询条件...
        
        return Result.success(baseMapper.selectPage(req.getPage(), wrapper));
    }
}

5. 最佳实践

5.1 统一使用枚举定义消息

所有国际化消息都应该在 I18nMessageEnums 中定义,避免硬编码消息字符串。

5.2 合理使用占位符

消息中的可变部分应使用占位符 {0}, {1} 等,而不是字符串拼接。

5.3 控制器方法添加国际化注解

所有需要支持国际化的控制器方法都应添加 @i18n 注解,并指定适当的 format

5.4 国际化字段命名规范

国际化字段通常以原字段名加 I18n 后缀命名,如 namenameI18n

6. 注意事项

  1. 国际化功能依赖于 Session,确保在无状态环境(如API网关)中正确传递语言信息。
  2. 国际化消息配置文件的修改需要重启应用才能生效。
  3. 默认语言为中文(zh),如需更改默认语言,需修改 I18nMessageEnums 中的定义。
  4. 在前端请求中,可通过 language 请求头指定语言,如 language: en