Loading

Spring Boot 国际化(i18n)终极指南:集成外部翻译平台与动态更新

在上一篇 文章中,我们探讨了构建一套解耦、强大的 i18n 体系的设计思想和顶层实现。然而,文中的一些核心组件如 MsctPropertiesIMsctResourceService 以及动态更新机制都处于“待实现”状态。

本文是该系列的“实战篇”,我们将亲手完成所有缺失的拼图,提供一份可以直接在项目中使用的、生产级别的代码实现。读完本文,你将获得:

  1. 一个完整的、基于外部翻译平台的 i18n 自动配置方案。
  2. 一套内存缓存与定时任务相结合的翻译动态更新机制。
  3. 清晰、可扩展的代码结构,助你轻松应对未来的 i18n 需求。

一、回顾:我们的目标架构

  1. MsctProperties: 类型安全地读取 application.yml 中的所有 i18n 相关配置。
  2. IMsctResourceService: 模拟与“多语言翻译平台”交互的SDK核心服务,负责拉取翻译数据。
  3. TranslationCacheManager: 单例 Bean,作为内存缓存,存储所有翻译词条,并内置一个定时任务,周期性地从 IMsctResourceService 拉取更新。
  4. MsctMessageSource: Spring MessageSource 接口的实现,它的数据源不再是本地文件,而是 TranslationCacheManager
  5. I18nService: 一个便捷的门面(Facade)类,让业务代码能更简单地调用翻译功能。

二、核心实现详解

第1步:万物之源 - 配置类 MsctProperties

我们需要一个类来映射 application.yml 中的配置项,使其具有类型安全和自动提示的优点。

MsctProperties.java

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.time.Duration;

@Component
@ConfigurationProperties(prefix = "msct.i18n")
public class MsctProperties {

    /** 是否启用国际化功能 */
    private boolean enabled = true;

    /** 默认语言, e.g., zh-CN, en-US */
    private String defaultLocale = "zh-CN";

    /** SDK相关配置 */
    private Sdk sdk = new Sdk();

    // Getters and Setters ...

    public static class Sdk {
        /** 翻译平台的API地址 */
        private String apiUrl;

        /** 应用的租户ID或项目ID */
        private String tenantId;

        /** 访问API的密钥 */
        private String apiKey;
        
        /** 缓存刷新配置 */
        private CacheRefresh cacheRefresh = new CacheRefresh();
        
        // Getters and Setters ...

        public static class CacheRefresh {
            /** 是否开启定时刷新 */
            private boolean enabled = true;
            
            /** 首次启动后延迟多久开始刷新 */
            private Duration initialDelay = Duration.ofMinutes(1);

            /** 刷新任务的执行间隔 */
            private Duration fixedDelay = Duration.ofHours(1);
            
            // Getters and Setters ...
        }
    }
}

application.yml 配置示例:

msct:
  i18n:
    enabled: true
    default-locale: zh-CN
    sdk:
      api-url: https://api.your-translation-platform.com
      tenant-id: my-awesome-project
      api-key: secret-key-xxxx
      cache-refresh:
        enabled: true
        initial-delay: 1m # 1分钟
        fixed-delay: 1h   # 1小时
第2步:模拟SDK与缓存 - IMsctResourceServiceTranslationCacheManager

这是整个方案的基石。我们先定义接口,然后创建一个模拟实现和一个缓存管理器。

IMsctResourceService.java (接口定义)

import java.util.Locale;
import java.util.Map;

/**
 * 模拟与多语言翻译平台交互的服务接口
 */
public interface IMsctResourceService {
    
    /**
     * 根据租户ID拉取所有翻译资源
     * @param tenantId 租户或项目ID
     * @return 嵌套Map,结构:{ "消息Code": { Locale: "翻译文本" } }
     * e.g., { "user.welcome": { Locale.US: "Welcome", Locale.CHINA: "欢迎" } }
     */
    Map<String, Map<Locale, String>> fetchAllTranslations(String tenantId);
}

MockMsctResourceService.java (模拟实现)
在实际项目中,这个类会由SDK提供,或者由你根据平台API来实现,这里我们用一个Mock来让程序跑起来。

import org.springframework.stereotype.Service;
import java.util.Locale;
import java.util.Map;
import java.util.HashMap;

@Service
public class MockMsctResourceService implements IMsctResourceService {

    @Override
    public Map<String, Map<Locale, String>> fetchAllTranslations(String tenantId) {
        System.out.println("【I18N SDK】: 正在从模拟平台拉取租户 '" + tenantId + "' 的翻译数据...");
        
        Map<String, Map<Locale, String>> translations = new HashMap<>();

        // 词条1: 简单欢迎语
        Map<Locale, String> welcomeMessages = new HashMap<>();
        welcomeMessages.put(Locale.SIMPLIFIED_CHINESE, "欢迎您, {0}!");
        welcomeMessages.put(Locale.US, "Welcome, {0}!");
        translations.put("user.welcome", welcomeMessages);

        // 词条2: 系统错误提示
        Map<Locale, String> errorMessages = new HashMap<>();
        errorMessages.put(Locale.SIMPLIFIED_CHINESE, "系统发生未知错误,请联系管理员。");
        errorMessages.put(Locale.US, "An unexpected system error occurred. Please contact the administrator.");
        translations.put("error.system.unexpected", errorMessages);
        
        // 模拟动态更新:每次调用时增加一个词条
        if (Math.random() > 0.5) {
            Map<Locale, String> dynamicMessages = new HashMap<>();
            dynamicMessages.put(Locale.SIMPLIFIED_CHINESE, "这是一个动态更新的词条!");
            dynamicMessages.put(Locale.US, "This is a dynamically updated entry!");
            translations.put("dynamic.update.test", dynamicMessages);
            System.out.println("【I18N SDK】: 发现并拉取了动态更新的词条 'dynamic.update.test'");
        }

        return translations;
    }
}

TranslationCacheManager.java (缓存与动态更新核心)

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;

@Component
public class TranslationCacheManager {

    private final Map<String, Map<Locale, String>> cache = new ConcurrentHashMap<>();
    private final IMsctResourceService resourceService;
    private final MsctProperties properties;

    public TranslationCacheManager(IMsctResourceService resourceService, MsctProperties properties) {
        this.resourceService = resourceService;
        this.properties = properties;
    }

    /**
     * 在服务启动时立即执行一次,完成首次加载
     */
    @PostConstruct
    public void init() {
        if (properties.isEnabled()) {
            loadTranslations();
        }
    }

    /**
     * 根据配置的定时任务,周期性刷新缓存
     */
    @Scheduled(
        initialDelayString = "${msct.i18n.sdk.cache-refresh.initial-delay:1m}",
        fixedDelayString = "${msct.i18n.sdk.cache-refresh.fixed-delay:1h}"
    )
    public void refreshCache() {
        if (properties.isEnabled() && properties.getSdk().getCacheRefresh().isEnabled()) {
            System.out.println("【I18N Cache】: 定时刷新任务开始...");
            loadTranslations();
            System.out.println("【I18N Cache】: 定时刷新任务结束。");
        }
    }

    private void loadTranslations() {
        Map<String, Map<Locale, String>> latestTranslations = 
            resourceService.fetchAllTranslations(properties.getSdk().getTenantId());
        
        if (latestTranslations != null && !latestTranslations.isEmpty()) {
            // 原子性地替换整个缓存,而不是逐条更新,避免并发问题
            cache.clear();
            cache.putAll(latestTranslations);
            System.out.println("【I18N Cache】: 翻译缓存已更新, 共加载 " + cache.size() + " 个词条。");
        }
    }

    /**
     * 对外提供获取翻译文本的接口
     * @return Optional<String>
     */
    public Optional<String> getTranslation(String code, Locale locale) {
        return Optional.ofNullable(cache.get(code))
                       .map(translations -> translations.get(locale));
    }
}

别忘了在你的主启动类上添加 @EnableScheduling 来开启定时任务功能。

第3步:连接Spring的桥梁 - MsctMessageSource

这个类是 Spring MessageSource 接口的具体实现,它将调用转发到我们的 TranslationCacheManager

MsctMessageSource.java

import org.springframework.context.support.AbstractMessageSource;
import org.springframework.stereotype.Component;
import java.text.MessageFormat;
import java.util.Locale;

@Component("messageSource") // 注册为bean并命名为"messageSource"
public class MsctMessageSource extends AbstractMessageSource {

    private final TranslationCacheManager cacheManager;

    public MsctMessageSource(TranslationCacheManager cacheManager) {
        this.cacheManager = cacheManager;
    }

    @Override
    protected MessageFormat resolveCode(String code, Locale locale) {
        return cacheManager.getTranslation(code, locale)
                .map(MessageFormat::new)
                .orElse(null); // 返回null,Spring会继续查找父MessageSource(如果有的话)
    }
}
第4步:便捷的门面 - I18nServiceLocaleUtil

LocaleUtil.java

import org.springframework.util.StringUtils;
import java.util.Locale;

public final class LocaleUtil {
    public static Locale parseLocale(String localeStr) {
        return StringUtils.parseLocaleString(localeStr);
    }
}

I18nService.java

import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.stereotype.Service;

@Service
public class I18nService {

    private final MessageSource messageSource;

    public I18nService(MessageSource messageSource) {
        this.messageSource = messageSource;
    }

    /**
     * 获取当前Locale下的翻译
     * @param code 消息Code
     * @return 翻译文本
     */
    public String getMessage(String code) {
        return getMessage(code, null);
    }
    
    /**
     * 获取当前Locale下带参数的翻译
     * @param code 消息Code
     * @param args 占位符参数
     * @return 翻译文本
     */
    public String getMessage(String code, Object[] args) {
        // 第三个参数是当找不到code时的默认消息,这里返回code本身
        return messageSource.getMessage(code, args, code, LocaleContextHolder.getLocale());
    }
}
第5步:整合 - 最终的 I18nAutoConfiguration

现在,我们可以用我们创建好的所有组件,来完成自动配置类了。

import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver;
import java.util.Collections;

@Configuration
// 只有在 msct.i18n.enabled = true (或未配置) 时,整个配置才生效
@ConditionalOnProperty(name = "msct.i18n.enabled", havingValue = "true", matchIfMissing = true)
public class I18nAutoConfiguration {

    // TranslationCacheManager, MsctMessageSource, I18nService等
    // 已经通过@Component或@Service注解自动注册了,这里无需再次@Bean声明。

    @Bean
    public LocaleResolver localeResolver(MsctProperties properties) {
        // 使用Spring内置的AcceptHeaderLocaleResolver,它能自动解析Accept-Language
        AcceptHeaderLocaleResolver resolver = new AcceptHeaderLocaleResolver();
        
        // 设置支持的语言列表,可选,但建议配置
        resolver.setSupportedLocales(Collections.singletonList(LocaleUtil.parseLocale(properties.getDefaultLocale())));
        
        // 设置默认语言
        Locale defaultLocale = LocaleUtil.parseLocale(properties.getDefaultLocale());
        resolver.setDefaultLocale(defaultLocale);
        LocaleContextHolder.setDefaultLocale(defaultLocale);
        
        return resolver;
    }
}

三、测试一下

创建一个简单的Controller来验证我们的成果。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class I18nTestController {

    @Autowired
    private I18nService i18nService;

    @GetMapping("/welcome")
    public String welcome(@RequestParam String name) {
        // 使用带参数的翻译
        return i18nService.getMessage("user.welcome", new Object[]{name});
    }

    @GetMapping("/error-info")
    public String errorInfo() {
        return i18nService.getMessage("error.system.unexpected");
    }

    @GetMapping("/dynamic")
    public String dynamic() {
        // 测试动态更新,这个词条可能在服务启动时不存在
        return i18nService.getMessage("dynamic.update.test");
    }
}

使用 cURL 测试:

  1. 请求中文:

    curl -X GET "http://localhost:8080/welcome?name=张三" -H "Accept-Language: zh-CN"
    # 预期输出: 欢迎您, 张三!
    
  2. 请求英文:

    curl -X GET "http://localhost:8080/welcome?name=John" -H "Accept-Language: en-US"
    # 预期输出: Welcome, John!
    
  3. 测试动态更新:
    启动服务后,立即访问 /dynamic 可能会返回 dynamic.update.test。等待你的刷新周期(例如1分钟后)再次访问,如果你在 MockMsctResourceService 中模拟了更新逻辑,你将看到翻译好的文本。

四、总结

我们从一个设计思想出发,一步步实现了所有必要的组件,最终构建了一套完整、健壮、可动态更新的国际化方案。这套方案将翻译管理的复杂性从业务代码中彻底剥离,极大地提升了开发效率和系统的可维护性,是现代分布式应用中处理 i18n 问题的理想选择。

posted @ 2026-01-13 19:38  飞鸿影  阅读(5)  评论(0)    收藏  举报