Solon 采用了不同的方法。整个 i18n 模块围绕三个解析器、一个注解和一个工具类构建——无需 XML、无需拦截器注册、无需样板配置。

让我来详细介绍它是如何工作的。

资源约定

将消息文件放在 resources/i18n/ 目录下:

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

Enter fullscreen mode Exit fullscreen mode

文件格式为标准的 Java 属性文件:

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

Enter fullscreen mode Exit fullscreen mode

就是这样。没有 Bean 声明,没有位置配置。Solon 会自动发现这些文件。

三种区域设置解析器,一个开关

Solon 提供了三种内置的区域设置解析器。默认是基于请求头的,但你可以通过一行代码进行切换。

1. 请求头解析器(默认)

读取 Content-Language 请求头,如果没有则回退到 Accept-Language

# app.yml — 无需配置,开箱即用

Enter fullscreen mode Exit fullscreen mode

要自定义请求头名称:

@Configuration
public class I18nConfig {
    @Bean
    public LocaleResolver localeInit() {
        LocaleResolverHeader resolver = new LocaleResolverHeader();
        resolver.setHeaderName("lang");  // 现在读取 "lang" 请求头
        return resolver;
    }
}

Enter fullscreen mode Exit fullscreen mode

2. Cookie 解析器

默认从名为 SOLON.LOCALE 的 Cookie 中读取区域设置:

@Configuration
public class I18nConfig {
    @Bean
    public LocaleResolver localeInit() {
        return new LocaleResolverCookie();
    }
}

Enter fullscreen mode Exit fullscreen mode

自定义 Cookie 名称:

resolver.setCookieName("lang");

Enter fullscreen mode Exit fullscreen mode

3. Session 解析器

从 Session 属性中读取区域设置:

@Configuration
public class I18nConfig {
    @Bean
    public LocaleResolver localeInit() {
        return new LocaleResolverSession();
    }
}

Enter fullscreen mode Exit fullscreen mode

自定义属性名称:

resolver.setAttrName("lang");

Enter fullscreen mode Exit fullscreen mode

4. 自定义解析器

如果三种解析器都不符合你的需求,可以直接实现 LocaleResolver

@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);
    }
}

Enter fullscreen mode Exit fullscreen mode

三种读取消息的方式

1. I18nUtil — 快速访问

在任何地方使用静态方法进行一次性查找:

// 从当前请求上下文(自动解析区域设置)
String title = I18nUtil.getMessage("login.title");

// 使用指定的 Locale
String welcome = I18nUtil.getMessage(locale, "login.welcome", "Alice");

// 使用 Context 对象
I18nUtil.getMessage(ctx, "app.name");

Enter fullscreen mode Exit fullscreen mode

2. I18nService — 命名 Bundle 访问

当你需要从特定 bundle 中读取消息时:

I18nService loginI18n = new I18nService("i18n.login");

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

Enter fullscreen mode Exit fullscreen mode

3. I18nBundle — 编程式 Bundle

对于高级用法,获取原始 bundle:

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

Enter fullscreen mode Exit fullscreen mode

@I18n — 模板集成

在控制器上添加 @I18n,每个模板都会获得一个 i18n 变量:

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

Enter fullscreen mode Exit fullscreen mode

如果不指定 bundle 名称,它默认使用 i18n.messages

模板语法示例

Freemarkerlogin.ftl):

<h1>${i18n["login.title"]}</h1>
<p>${i18n.getAndFormat("login.welcome", session.user.name)}</p>

Enter fullscreen mode Exit fullscreen mode

Thymeleaf

<h1 th:text="${i18n.get('login.title')}">Sign In</h1>
<p th:text="${i18n.getAndFormat('login.welcome', 'Alice')}">Welcome!</p>

Enter fullscreen mode Exit fullscreen mode

Beetl

<h1>${i18n["login.title"]}</h1>
<p>${@i18n.getAndFormat("login.welcome", "Alice")}</p>

Enter fullscreen mode Exit fullscreen mode

Enjoy

<h1>#(i18n.get("login.title"))</h1>
<p>#(i18n.getAndFormat("login.welcome", "Alice"))</p>

Enter fullscreen mode Exit fullscreen mode

分布式环境扩展

对于在配置中心或 CMS 中管理翻译的团队,可以实现 I18nBundleFactory

@Component
public class RemoteBundleFactory implements I18nBundleFactory {
    @Override
    public I18nBundle create(String bundleName, Locale locale) {
        // 从远程配置中心获取
        Props props = getRemoteProps(bundleName, locale);
        return new I18nBundleImpl(props, locale);
    }
}

Enter fullscreen mode Exit fullscreen mode

注册它:

I18nUtil.setBundleFactory(new RemoteBundleFactory());

Enter fullscreen mode Exit fullscreen mode

Solon 内置的 I18nBundleFactoryLocalresources/i18n/ 读取;工厂模式让你可以在不更改应用代码的情况下切换到 Redis、Nacos 或任何其他数据源。

最小依赖

solon-i18n 模块除了 solon-core 之外没有其他传递性依赖。如果你使用 solon-web,它已经包含在内。

<dependency>
    <groupId>org.noear</groupId>
    <artifactId>solon-i18n</artifactId>
</dependency>

Enter fullscreen mode Exit fullscreen mode

权衡

你得不到的:

  • 没有 MessageSource 式的层级结构(Solon 使用每个名称对应扁平的 bundle)
  • 没有开发模式下的自动重载(重启以获取更改,或实现带热重载的自定义 I18nBundleFactory
  • 没有内置的语言切换端点(有意留给解析器——通过查询参数切换需要自定义 LocaleResolver

你能得到的:

  • 不到 60 秒即可让 i18n 工作
  • 三种生产就绪的解析器策略
  • 跨 5 种引擎的一流模板集成
  • 用于分布式配置的可扩展工厂模式
  • 零 XML、零拦截器、零仪式

总结

Solon 的 i18n 模型遵循框架的理念:约定优于配置,但需要时可以切换。一个依赖、一个用于模板的注解、一个用于编程访问的工具,以及三个即插即用的解析器,覆盖了绝大多数真实世界的场景。

如果你一直在与 MessageSource 配置纠缠,不妨试试这个——它会让人耳目一新。