Solon I18n:三种解析器,零样板代码

如果你在 Spring Boot 里做过国际化,流程大概是这样的:配 MessageSource 的 Bean、写 ReloadableResourceBundleMessageSource、声明 LocaleResolver、注册 LocaleChangeInterceptor...五六步之后才能看到第一行翻译文本。

Solon 换了个方式。整个 i18n 模块围绕三个解析器、一个注解、一个工具类展开------没有 XML、没有拦截器注册、没有样板配置。

资源文件的约定

把文件放到 resources/i18n/ 下:

复制代码
resources/i18n/messages.properties            # 默认语言(如中文)
resources/i18n/messages_en_US.properties      # 美式英语
resources/i18n/messages_ja_JP.properties      # 日文

格式是标准的 Java properties:

properties 复制代码
# messages_en_US.properties
login.title=Sign In
login.welcome=Welcome, {0}!
app.name=Solon Application

不需要 Bean 声明,不需要指定路径。Solon 自动发现。

三种解析器,一键切换

Solon 内置三个地区解析器。默认是 请求头解析器,但可以一行代码换掉。

1. Header 解析器(默认)

读取 Content-Language 请求头,降级到 Accept-Language:

yaml 复制代码
# app.yml --- 无需任何配置

自定义头部名称:

java 复制代码
@Configuration
public class I18nConfig {
    @Bean
    public LocaleResolver localeInit() {
        LocaleResolverHeader resolver = new LocaleResolverHeader();
        resolver.setHeaderName("lang");
        return resolver;
    }
}

从 cookie SOLON.LOCALE 读取语言:

java 复制代码
@Configuration
public class I18nConfig {
    @Bean
    public LocaleResolver localeInit() {
        return new LocaleResolverCookie();
    }
}

3. Session 解析器

从 session 属性读取语言:

java 复制代码
@Configuration
public class I18nConfig {
    @Bean
    public LocaleResolver localeInit() {
        return new LocaleResolverSession();
    }
}

4. 自定义解析器

如果三种都不够用,直接实现 LocaleResolver:

java 复制代码
@Component
public class QueryParamResolver implements LocaleResolver {
    @Override
    public Locale getLocale(Context ctx) {
        String lang = ctx.param("lang");
        if (lang != null) {
            return LocaleUtil.toLocale(lang);
        }
        return Locale.getDefault();
    }

    @Override
    public void setLocale(Context ctx, Locale locale) {
        ctx.setLocale(locale);
    }
}

三种读消息的方式

1. I18nUtil --- 快捷工具

随处可用的静态方法:

java 复制代码
// 从当前请求上下文(自动解析 locale)
String title = I18nUtil.getMessage("login.title");

// 指定 Locale
String welcome = I18nUtil.getMessage(locale, "login.welcome", new Object[]{"Alice"});

// 指定 Context
I18nUtil.getMessage(ctx, "app.name");

2. I18nService --- 命名包访问

从特定语言包读取消息:

java 复制代码
I18nService loginI18n = new I18nService("i18n.login");

@Mapping("/greet")
public String greet(Locale locale) {
    return loginI18n.get(locale, "login.welcome", new Object[]{"Alice"});
}

3. I18nBundle --- 编程式操作

获取原始 bundle 做更复杂的操作:

java 复制代码
I18nBundle bundle = I18nUtil.getBundle("i18n.messages", locale);
String value = bundle.get("login.title");
String formatted = bundle.getAndFormat("login.welcome", "Alice");

@I18n --- 模板集成

给 Controller 加上 @I18n,所有模板自动获得 i18n 变量:

java 复制代码
@I18n("i18n.login")
@Controller
public class LoginController {
    @Mapping("/login")
    public ModelAndView login() {
        return new ModelAndView("login.ftl");
    }
}

不指定包名则默认使用 i18n.messages。

各模板引擎语法

Freemarker:

ftl 复制代码
<h1>${i18n["login.title"]}</h1>
<p>${i18n.getAndFormat("login.welcome", user.name)}</p>

Thymeleaf:

html 复制代码
<h1 th:text="${i18n.get('login.title')}">登录</h1>

Beetl:

beetl 复制代码
<h1>${i18n["login.title"]}</h1>

分布式扩展

如果翻译内容来自配置中心或 CMS,实现 I18nBundleFactory:

java 复制代码
@Component
public class RemoteBundleFactory implements I18nBundleFactory {
    @Override
    public I18nBundle create(String bundleName, Locale locale) {
        Props props = getRemoteProps(bundleName, locale);
        return new I18nBundleImpl(props, locale);
    }
}

手动注册方式:

java 复制代码
I18nUtil.setBundleFactory(new RemoteBundleFactory());

内置的 I18nBundleFactoryLocal 从 resources/i18n/ 读取;工厂接口让你在不改业务代码的前提下换用 Redis、Nacos 或其他数据源。

诚实说三条边界

不含的:

  • 没有 MessageSource 层级体系(Solon 用扁平 bundle)
  • 没有开发模式热加载(重启生效,或自定义 I18nBundleFactory 实现热加载)
  • 没有内置的语言切换端点(切换逻辑留给 Resolver------如果要通过 URL 参数切换,写个自定义 Resolver 即可)

含的:

  • 60 秒内跑起国际化
  • 三种生产可用的解析器策略
  • 五种模板引擎的一等集成
  • 支持分布式配置的工厂模式
  • 零 XML、零拦截器、零仪式感

总结

Solon 的国际化模块延续了框架的哲学:约定优先,但需要时随时替换。一个依赖、一个注解用于模板、一个工具类用于编程访问、三个即插即用的解析器------覆盖了绝大多数真实场景。

相关推荐
范同学~15 小时前
项目添加水印
java·前端·javascript
快乐的韭菜根根15 小时前
Slurm 指定 GPU 卡:`File=/dev/nvidiaX` 映射的坑与修复
java·服务器·网络
酣大智15 小时前
hibernate学习笔记
java·hibernate·session
careathers16 小时前
【数据结构】基于比较的排序算法 - 插入排序
java·数据结构·排序算法
街头便衣16 小时前
OpenRouter周榜复算:164万亿token,输出2.55%,DeepSeek账单占1.4%
android·java·开发语言·人工智能·语言模型·开源
mister_guo16 小时前
Spring Boot 配置文件读取的几种方式:用法、原理与常见误区
java·spring boot
余槐i16 小时前
虚拟线程上线后吞吐反而掉了:Spring Boot 3 里 synchronized 锁住载体线程的定位与修复
java·spring boot·性能优化·虚拟线程·jdk 21
要开心吖ZSH16 小时前
DDD(领域驱动设计)篇三:实战对照——同一个订单业务,两种架构怎么写
java·架构·领域模型·ddd·领域驱动设计·设计思想
wangchunyu1141 天前
JAVA CompletableFuture异步编程实战
java·开发语言·python
paopaokaka_luck1 天前
流浪动物救助与收养系统(DeepSeek AI对话、 ECharts多维分析、救助流程衔接、领养与回访衔接、 捐赠确认与公示、志愿者协作)
java·前端·javascript·人工智能·mysql·echarts·推荐算法