Solon takes a different approach. The entire i18n module is built around three resolvers, one annotation, and a utility class — no XML, no interceptor registration, no boilerplate configuration.
Let me walk through how it works.
The Resource Convention
Place your message files under resources/i18n/:
resources/i18n/messages.properties # default (e.g. Chinese)
resources/i18n/messages_en_US.properties # US English
resources/i18n/messages_ja_JP.properties # Japanese
Enter fullscreen mode Exit fullscreen mode
File format is standard Java properties:
# messages_en_US.properties
login.title=Sign In
login.welcome=Welcome, {0}!
app.name=Solon Application
Enter fullscreen mode Exit fullscreen mode
That's it. No bean declaration, no location configuration. Solon discovers them automatically.
Three Locale Resolvers, One Switch
Solon ships three built-in locale resolvers. The default is header-based, but you can swap it with a one-liner.
1. Header Resolver (Default)
Reads the Content-Language header, falling back to Accept-Language:
# app.yml — no config needed, works out of the box
Enter fullscreen mode Exit fullscreen mode
To customize the header name:
@Configuration
public class I18nConfig {
@Bean
public LocaleResolver localeInit() {
LocaleResolverHeader resolver = new LocaleResolverHeader();
resolver.setHeaderName("lang"); // now reads "lang" header
return resolver;
}
}
Enter fullscreen mode Exit fullscreen mode
2. Cookie Resolver
Reads locale from a cookie named SOLON.LOCALE by default:
@Configuration
public class I18nConfig {
@Bean
public LocaleResolver localeInit() {
return new LocaleResolverCookie();
}
}
Enter fullscreen mode Exit fullscreen mode
Custom cookie name:
resolver.setCookieName("lang");
Enter fullscreen mode Exit fullscreen mode
3. Session Resolver
Reads locale from a session attribute:
@Configuration
public class I18nConfig {
@Bean
public LocaleResolver localeInit() {
return new LocaleResolverSession();
}
}
Enter fullscreen mode Exit fullscreen mode
Custom attribute name:
resolver.setAttrName("lang");
Enter fullscreen mode Exit fullscreen mode
4. Custom Resolver
If none of the three fit your use case, implement LocaleResolver directly:
@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
Three Ways to Read Messages
1. I18nUtil — Quick Access
Static methods for one-off lookups anywhere:
// From current request context (locale resolved automatically)
String title = I18nUtil.getMessage("login.title");
// With a specific Locale
String welcome = I18nUtil.getMessage(locale, "login.welcome", "Alice");
// With a Context object
I18nUtil.getMessage(ctx, "app.name");
Enter fullscreen mode Exit fullscreen mode
2. I18nService — Named Bundle Access
When you need messages from a specific 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 — Programmatic Bundle
For advanced usage, get the raw 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 — Template Integration
Add @I18n to your controller and every template gets an i18n variable:
@I18n("i18n.login")
@Controller
public class LoginController {
@Mapping("/login")
public ModelAndView login() {
return new ModelAndView("login.ftl");
}
}
Enter fullscreen mode Exit fullscreen mode
Without specifying a bundle name, it defaults to i18n.messages.
Template Syntax Examples
Freemarker (login.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
Extending for Distributed Environments
For teams that manage translations in a config center or CMS, implement I18nBundleFactory:
@Component
public class RemoteBundleFactory implements I18nBundleFactory {
@Override
public I18nBundle create(String bundleName, Locale locale) {
// Fetch from remote config center
Props props = getRemoteProps(bundleName, locale);
return new I18nBundleImpl(props, locale);
}
}
Enter fullscreen mode Exit fullscreen mode
Register it:
I18nUtil.setBundleFactory(new RemoteBundleFactory());
Enter fullscreen mode Exit fullscreen mode
Solon's built-in I18nBundleFactoryLocal reads from resources/i18n/; the factory approach lets you swap in Redis, Nacos, or any other source without changing your application code.
Minimal Dependency
The solon-i18n module has zero transitive dependencies beyond solon-core. If you're using solon-web, it's already included.
<dependency>
<groupId>org.noear</groupId>
<artifactId>solon-i18n</artifactId>
</dependency>
Enter fullscreen mode Exit fullscreen mode
The Trade-offs
What you don't get:
- No
MessageSource-style hierarchy (Solon uses flat bundles per name) - No automatic reload in dev mode (restart to pick up changes, or implement a custom
I18nBundleFactorywith hot-reload) - No built-in language switcher endpoint (it's intentionally left to the resolver — switching via query param requires a custom
LocaleResolver)
What you do get:
- i18n working in under 60 seconds
- Three production-ready resolver strategies
- First-class template integration across 5 engines
- Extensible factory pattern for distributed config
- Zero XML, zero interceptors, zero ceremony
Summary
Solon's i18n model follows the framework's philosophy: convention over configuration, but swap when you need to. One dependency, one annotation for templates, one utility for programmatic access, and three drop-in resolvers cover the vast majority of real-world scenarios.
If you've been wrestling with MessageSource configurations, give this a try — it's surprisingly refreshing.
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.