The @I18n annotation generates a typed API over standard Java .properties bundles. Each message key becomes a Java method, and placeholders become method parameters. If you rename a key, remove a message, or change its arguments, you get a compile-time error instead of a runtime failure.
Under the hood it uses ICU4J MessageFormat, so you can keep using ICU patterns for plural/select logic, date/time formatting, and number formatting.
| Feature | Traditional ResourceBundle | @I18n |
|---|---|---|
| Access Method | bundle.getString("greeting") |
messages.greeting() |
| Type Safety | Runtime strings | Compile-time methods |
| Parameters | Manual formatting | Strongly-typed parameters |
| Error Detection | Runtime MissingResourceException | Compile-time errors |
| IDE Support | No autocomplete | Full autocomplete |
| Format Validation | Runtime | Compile-time |
- Generated methods instead of string keys: call
messages.greeting(...)rather thanbundle.getString("greeting"). - Compile-time checks: missing keys and invalid ICU patterns fail the build.
- ICU4J MessageFormat support: plurals/selects, dates/times, numbers, and custom ICU patterns.
- Typed parameters: message arguments are expressed as method parameters.
- Standard locale handling: uses the usual ResourceBundle naming and
Locale. - Read-only access: generated resources are immutable.
- Works with existing bundles: keep your
.propertiesfiles; no special runtime registry.
Create message files in src/main/resources/:
# messages.properties (default locale)
welcome=Welcome!
greeting=Hello, {0}!
farewell=Goodbye, {0}. See you soon!Annotate a class:
@I18n(baseName = "messages")
public class Messages {}Use the generated class:
public class Application {
public static void main(String[] args) {
MessageResource messages = MessageResource.getDefault();
System.out.println(messages.welcome()); // "Welcome!"
System.out.println(messages.greeting("Alice")); // "Hello, Alice!"
System.out.println(messages.farewell("Bob")); // "Goodbye, Bob. See you soon!"
}
}Create locale-specific files:
# messages.properties (default - English)
welcome=Welcome!
greeting=Hello, {0}!
# messages_zh_CN.properties (Simplified Chinese)
welcome=欢迎!
greeting=你好,{0}!
# messages_es.properties (Spanish)
welcome=¡Bienvenido!
greeting=¡Hola, {0}!
# messages_fr.properties (French)
welcome=Bienvenue!
greeting=Bonjour, {0}!Annotate a class:
@I18n(
baseName = "messages",
defaultLocale = "en"
)
public class Messages {}Use with different locales:
// Default locale (English)
MessageResource.LocaleMessages enMessages = MessageResource.getDefault();
System.out.println(enMessages.greeting("Alice")); // "Hello, Alice!"
// Chinese
MessageResource.LocaleMessages zhMessages = MessageResource.get(Locale.CHINESE);
System.out.println(zhMessages.greeting("张三")); // "你好,张三!"
// Spanish
MessageResource.LocaleMessages esMessages = MessageResource.get(new Locale("es"));
System.out.println(esMessages.greeting("María")); // "¡Hola, María!"
// French
MessageResource.LocaleMessages frMessages = MessageResource.get(Locale.FRENCH);
System.out.println(frMessages.greeting("Pierre")); // "Bonjour, Pierre!"Create messages with ICU formatting:
# messages.properties
# Number formatting
product.price=Product price: {0,number,currency}
discount=Discount: {0,number,percent}
# Date/time formatting
current.time=Current time: {0,date,full} {0,time,short}
event.date=Event date: {0,date,long}
# Plural forms (ICU MessageFormat)
item.count={0, plural, =0{No items} =1{One item} other{# items}}
file.size={0, plural, =0{No files} =1{One file} other{# files}}
# Choice format for ranges
temperature={0,choice,-50<It''s freezing|0<It''s cold|20<It''s warm|30<It''s hot}
# Complex messages with multiple parameters
user.status=User {0} has {1, plural, =0{no messages} =1{one message} other{# messages}}Usage:
MessageResource.LocaleMessages messages = MessageResource.getDefault();
// Number formatting
System.out.println(messages.productPrice(1234.56)); // "Product price: $1,234.56"
System.out.println(messages.discount(0.15)); // "Discount: 15%"
// Date formatting
Date now = new Date();
System.out.println(messages.currentTime(now)); // "Current time: Tuesday, December 16, 2025 2:30 PM"
// Plural forms
System.out.println(messages.itemCount(0)); // "No items"
System.out.println(messages.itemCount(1)); // "One item"
System.out.println(messages.itemCount(5)); // "5 items"
// Choice format
System.out.println(messages.temperature(-5)); // "It's freezing"
System.out.println(messages.temperature(25)); // "It's warm"
// Complex messages
System.out.println(messages.userStatus("Alice", 3)); // "User Alice has 3 messages"The @I18n annotation provides several configuration options:
@I18n(
baseName = "messages", // Resource bundle base name
defaultLocale = "en", // Default locale (language code)
generatedClassName = "$$Resource", // Generated class name pattern
supportedLocales = {} // Optional: explicitly list supported locales
)
public class Messages {}Specifies the base name of the resource bundle (without locale suffix and .properties extension):
@I18n(baseName = "messages") // → messages.properties, messages_zh_CN.properties
@I18n(baseName = "i18n/app") // → i18n/app.properties, i18n/app_fr.properties
@I18n(baseName = "localization/text") // → localization/text.propertiesFile Organization:
src/main/resources/
├── messages.properties
├── messages_en.properties
├── messages_zh_CN.properties
└── i18n/
├── app.properties
├── app_en.properties
└── app_fr.properties
Specifies the default locale to use when no locale is explicitly provided:
@I18n(
baseName = "messages",
defaultLocale = "en" // English as default
)Locale Codes:
"en"- English"zh"- Chinese"es"- Spanish"fr"- French"de"- German"ja"- Japanese"ko"- Korean
Locale Variants:
"en_US"- English (United States)"en_GB"- English (United Kingdom)"zh_CN"- Chinese (Simplified)"zh_TW"- Chinese (Traditional)"fr_CA"- French (Canada)
Customize the name of the generated class:
@I18n(
baseName = "messages",
generatedClassName = "I18nMessages"
)
public class Messages {}
// Usage:
I18nMessages.LocaleMessages messages = I18nMessages.getDefault();Default Pattern: $$Resource (e.g., Messages → MessageResource)
Custom Patterns:
$$is replaced with the annotated class name- Use any valid Java identifier
// Class: AppMessages
@I18n(generatedClassName = "$$") // → AppMessages
@I18n(generatedClassName = "$$Bundle") // → AppMessagesBundle
@I18n(generatedClassName = "I18n$$") // → I18nAppMessagesOptionally specify which locales should be generated:
@I18n(
baseName = "messages",
supportedLocales = {"en", "zh_CN", "es", "fr"}
)
public class Messages {}If not specified, Propify will detect all available locale files automatically.
Propify uses ICU4J MessageFormat for advanced formatting. Here's a comprehensive guide:
greeting=Hello, {0}!
welcome=Welcome, {0} {1}!messages.greeting("Alice"); // "Hello, Alice!"
messages.welcome("John", "Doe"); // "Welcome, John Doe!"greeting=Hello, {name}!
user.info=User: {username}, Email: {email}// Parameters are positional based on property file order
messages.greeting("Alice"); // "Hello, Alice!"
messages.userInfo("john", "john@example.com"); // "User: john, Email: john@example.com"count=Count: {0,number}messages.count(1234567); // "Count: 1,234,567"price=Price: {0,number,currency}messages.price(99.99); // "Price: $99.99" (US locale)
messages.price(99.99); // "Price: ¥99.99" (Chinese locale)discount=Discount: {0,number,percent}messages.discount(0.25); // "Discount: 25%"precise=Value: {0,number,#.##}messages.precise(3.14159); // "Value: 3.14"date.short=Date: {0,date,short}messages.dateShort(new Date()); // "Date: 12/16/25"date.long=Date: {0,date,long}messages.dateLong(new Date()); // "Date: December 16, 2025"date.full=Date: {0,date,full}messages.dateFull(new Date()); // "Date: Tuesday, December 16, 2025"time.short=Time: {0,time,short}
time.long=Time: {0,time,long}messages.timeShort(new Date()); // "Time: 2:30 PM"
messages.timeLong(new Date()); // "Time: 2:30:45 PM PST"datetime=Date: {0,date,full} at {0,time,short}messages.datetime(new Date()); // "Date: Tuesday, December 16, 2025 at 2:30 PM"Handles grammatical pluralization rules for different languages:
item.count={0, plural, =0{No items} =1{One item} other{# items}}
notification={0, plural, =0{No notifications} =1{You have one notification} other{You have # notifications}}messages.itemCount(0); // "No items"
messages.itemCount(1); // "One item"
messages.itemCount(5); // "5 items"
messages.notification(0); // "No notifications"
messages.notification(1); // "You have one notification"
messages.notification(3); // "You have 3 notifications"Plural Categories:
=0,=1,=2, etc. - Exact matcheszero- Zero (language-specific)one- Singulartwo- Dual (some languages)few- Small number (some languages)many- Large number (some languages)other- Default case#- Replaced with the number
Language-Specific Plurals:
# English: one/other
messages={0, plural, one{# message} other{# messages}}
# Russian: one/few/many/other
messages={0, plural, one{# сообщение} few{# сообщения} many{# сообщений} other{# сообщения}}Select message based on numeric ranges:
temperature={0,choice,-50<Freezing|0<Cold|20<Warm|30<Hot|40<Very Hot}
score={0,choice,0<Failed|60<Passed|80<Good|90<Excellent}messages.temperature(-10); // "Freezing"
messages.temperature(15); // "Cold"
messages.temperature(25); // "Warm"
messages.score(45); // "Failed"
messages.score(75); // "Passed"
messages.score(95); // "Excellent"gender={0, select, male{He} female{She} other{They}} will arrive soon.messages.gender("male"); // "He will arrive soon."
messages.gender("female"); // "She will arrive soon."
messages.gender("other"); // "They will arrive soon."Combine multiple formats in a single message:
order.status=Order #{0} placed on {1,date,short} for {2,number,currency} is {3, select, pending{being processed} shipped{on the way} delivered{completed} other{in an unknown state}}.
user.summary={0} has {1, plural, =0{no posts} =1{one post} other{# posts}} and {2, plural, =0{no followers} =1{one follower} other{# followers}}.messages.orderStatus(12345, new Date(), 99.99, "shipped");
// "Order #12345 placed on 12/16/25 for $99.99 is on the way."
messages.userSummary("Alice", 5, 100);
// "Alice has 5 posts and 100 followers."// Get default locale messages
MessageResource.LocaleMessages defaultMessages = MessageResource.getDefault();
// Get specific locale messages
MessageResource.LocaleMessages zhMessages = MessageResource.get(Locale.CHINESE);
MessageResource.LocaleMessages enMessages = MessageResource.get(Locale.US);
// Using custom locale
MessageResource.LocaleMessages customMessages = MessageResource.get(new Locale("es", "MX"));For each message in your properties file, a corresponding type-safe method is generated:
Property file:
welcome=Welcome!
greeting=Hello, {0}!
user.status=User {0} has {1} messages
complex=On {0,date,short}, {1} bought {2,number,currency} worth of itemsGenerated methods:
public interface LocaleMessages {
String welcome();
String greeting(String arg0);
String userStatus(String arg0, int arg1);
String complex(Date arg0, String arg1, double arg2);
}# messages.properties (English)
app.title=My Application
menu.home=Home
menu.about=About
menu.contact=Contact
user.login=Login
user.logout=Logout
form.submit=Submit
form.cancel=Cancel
# messages_zh_CN.properties (Chinese)
app.title=我的应用
menu.home=首页
menu.about=关于
menu.contact=联系我们
user.login=登录
user.logout=登出
form.submit=提交
form.cancel=取消
# messages_es.properties (Spanish)
app.title=Mi Aplicación
menu.home=Inicio
menu.about=Acerca de
menu.contact=Contacto
user.login=Iniciar sesión
user.logout=Cerrar sesión
form.submit=Enviar
form.cancel=Cancelar@I18n(baseName = "messages", defaultLocale = "en")
public class Messages {}
// Usage in UI
public class UserInterface {
private MessageResource.LocaleMessages messages;
public UserInterface(Locale locale) {
this.messages = MessageResource.get(locale);
}
public void renderMenu() {
System.out.println(messages.appTitle());
System.out.println("- " + messages.menuHome());
System.out.println("- " + messages.menuAbout());
System.out.println("- " + messages.menuContact());
}
}# messages.properties
product.added={0} has been added to your cart
product.removed={0} has been removed from your cart
cart.total=Cart total: {0,number,currency}
cart.items=You have {0, plural, =0{no items} =1{one item} other{# items}} in your cart
order.placed=Order #{0} placed successfully on {1,date,long}
order.shipped=Your order has been shipped and will arrive by {0,date,short}
order.delivered=Order delivered on {0,date,short}
payment.success=Payment of {0,number,currency} processed successfully
payment.failed=Payment failed. Please try again.
promo.discount=Save {0,number,percent} on your purchase!
promo.code=Use code {0} for {1,number,percent} off@I18n(baseName = "messages")
public class Messages {}
// Usage in e-commerce app
MessageResource.LocaleMessages messages = MessageResource.getDefault();
// Cart operations
System.out.println(messages.productAdded("iPhone 15"));
// "iPhone 15 has been added to your cart"
System.out.println(messages.cartItems(3));
// "You have 3 items in your cart"
System.out.println(messages.cartTotal(299.99));
// "Cart total: $299.99"
// Order status
System.out.println(messages.orderPlaced(12345, new Date()));
// "Order #12345 placed successfully on December 16, 2025"
// Promotions
System.out.println(messages.promoDiscount(0.20));
// "Save 20% on your purchase!"
System.out.println(messages.promoCode("SAVE15", 0.15));
// "Use code SAVE15 for 15% off"# validation.properties
validation.required={0} is required
validation.email={0} must be a valid email address
validation.min={0} must be at least {1}
validation.max={0} must be at most {1}
validation.range={0} must be between {1} and {2}
validation.minLength={0} must be at least {1, plural, =1{one character} other{# characters}} long
validation.maxLength={0} must be at most {1, plural, =1{one character} other{# characters}} long
validation.pattern={0} format is invalid
validation.match={0} and {1} must match
# Custom validation messages
password.weak=Password is too weak. Must contain uppercase, lowercase, numbers, and symbols.
username.taken=Username {0} is already taken
email.exists=An account with email {0} already exists@I18n(baseName = "validation")
public class ValidationMessages {}
// Usage in validation logic
ValidationMessageResource.LocaleMessages messages = ValidationMessageResource.getDefault();
// Field validation
if (email.isEmpty()) {
throw new ValidationException(messages.validationRequired("Email"));
}
if (!email.matches(EMAIL_PATTERN)) {
throw new ValidationException(messages.validationEmail("Email"));
}
if (password.length() < 8) {
throw new ValidationException(messages.validationMinLength("Password", 8));
// "Password must be at least 8 characters long"
}
if (age < 18 || age > 100) {
throw new ValidationException(messages.validationRange("Age", 18, 100));
// "Age must be between 18 and 100"
}# notifications.properties
notification.welcome=Welcome to our platform, {0}!
notification.newMessage={0} sent you a message
notification.newMessages={0} sent you {1, plural, =1{a message} other{# messages}}
notification.friendRequest={0} wants to be your friend
notification.commented={0} commented on your post: "{1}"
notification.liked={0, plural, =1{{0} liked} other{{0} and {1, plural, =1{one other person} other{# others}} liked}} your post
notification.reminder=Reminder: {0} is scheduled for {1,date,short} at {1,time,short}
notification.deadline=Deadline approaching: {0} is due {1, choice, 0<in {1} hours|1<tomorrow|7<next week}
notification.system.maintenance=System maintenance scheduled for {0,date,long}. Expected downtime: {1} hours.
notification.system.updated=System updated to version {0}@I18n(baseName = "notifications")
public class NotificationMessages {}
// Usage in notification system
NotificationMessageResource.LocaleMessages messages = NotificationMessageResource.getDefault();
// Social notifications
System.out.println(messages.notificationNewMessages("Alice", 3));
// "Alice sent you 3 messages"
System.out.println(messages.notificationCommented("Bob", "Great post!"));
// "Bob commented on your post: \"Great post!\""
// Reminders
Date meetingTime = /* tomorrow at 2 PM */;
System.out.println(messages.notificationReminder("Team Meeting", meetingTime));
// "Reminder: Team Meeting is scheduled for 12/17/25 at 2:00 PM"
// System notifications
System.out.println(messages.notificationSystemUpdated("2.5.0"));
// "System updated to version 2.5.0"src/main/resources/
├── i18n/
│ ├── common.properties # Common messages
│ ├── common_zh_CN.properties
│ ├── validation.properties # Validation messages
│ ├── validation_zh_CN.properties
│ ├── errors.properties # Error messages
│ └── errors_zh_CN.properties
@I18n(baseName = "i18n/common")
public class CommonMessages {}
@I18n(baseName = "i18n/validation")
public class ValidationMessages {}
@I18n(baseName = "i18n/errors")
public class ErrorMessages {}# Good: Clear and descriptive
user.welcome.message=Welcome back, {0}!
form.validation.email.invalid=Please enter a valid email address
order.confirmation.sent=Order confirmation sent to {0}
# Bad: Cryptic abbreviations
usr.wlc=Welcome back, {0}!
frm.val.eml=Please enter a valid email address
ord.conf=Order confirmation sent to {0}Ensure all locales have the same message keys:
# messages.properties
welcome=Welcome!
goodbye=Goodbye!
# messages_zh_CN.properties
welcome=欢迎!
goodbye=再见!
# Missing 'goodbye' would cause compilation error# Good: ICU plural (grammatically correct)
items={0, plural, =0{No items} =1{One item} other{# items}}
# Avoid: Choice format for counts
items={0,choice,0<No items|1<One item|1<{0} items}# Shown in the user profile header
user.greeting=Hello, {0}!
# Maximum length: 50 characters
# Parameter: username (string)
form.username.placeholder=Enter your username
# Displayed after successful payment
# Parameters: amount (currency), orderId (number)
payment.success=Payment of {0,number,currency} for order #{1} completedAlways provide a default locale file:
# messages.properties (always present)
welcome=Welcome!
# messages_zh_CN.properties (optional)
welcome=欢迎!If a locale file is missing, the default locale is used automatically.
Problem: Resource bundle not found: messages
Solutions:
- Ensure file is in
src/main/resources/ - Check base name matches file name (without
.properties) - Verify file extension is
.properties
Problem: Compilation error: Invalid message format
Solutions:
- Check ICU MessageFormat syntax
- Escape single quotes: use
''instead of' - Validate plural forms:
{0, plural, ...} - Test choice format syntax:
{0,choice,0<msg1|10<msg2}
Problem: Generated code fails at runtime for specific locale
Solutions:
- Create locale-specific file:
messages_zh_CN.properties - Ensure all locales have the same keys
- Use fallback to default locale
Problem: Method parameter doesn't match format
Solutions:
- Use
Datefor{0,date,*}and{0,time,*} - Use number types (
int,double) for{0,number,*} - Use
Stringfor simple{0}placeholders
| Feature | ResourceBundle | @I18n |
|---|---|---|
| Type Safety | None (string keys) | Full (generated methods) |
| Error Detection | Runtime | Compile-time |
| IDE Support | No autocomplete | Full autocomplete |
| Parameter Safety | Manual formatting | Type-checked parameters |
| ICU Support | No | Yes (via ICU4J) |
| Feature | Spring MessageSource | @I18n |
|---|---|---|
| Framework Dependency | Requires Spring | None |
| Type Safety | String-based | Strongly-typed |
| Configuration | XML/Java config | Annotation-based |
| Performance | Runtime lookup | Compile-time optimization |
| Feature | gettext | @I18n |
|---|---|---|
| File Format | PO/POT files | Standard .properties |
| Java Integration | External tool | Native annotation processor |
| Type Safety | None | Full |
| Tooling | Separate tools needed | Integrated with build |
- Properties Format Only: Only supports
.propertiesfiles (not JSON, YAML, or other formats) - Read-Only: Generated messages are immutable
- Build-Time Only: All translations must be available at compile time
- ICU4J Dependency: Applications using generated code need ICU4J at runtime for advanced formatting
For applications using generated i18n classes with advanced formatting:
<!-- ICU4J for advanced formatting (optional, only if using plural/choice/date formats) -->
<dependency>
<groupId>com.ibm.icu</groupId>
<artifactId>icu4j</artifactId>
<version>73.2</version>
</dependency>If you only use simple string substitution, no runtime dependencies are needed.
Found a bug or want to contribute? Please see the main CONTRIBUTING.md for guidelines.
MIT License - see LICENSE