Skip to content

Latest commit

 

History

History
912 lines (679 loc) · 24.7 KB

File metadata and controls

912 lines (679 loc) · 24.7 KB

@I18n - Type-Safe Internationalization

Overview

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.

Key Differences from Traditional i18n

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

Features

  • Generated methods instead of string keys: call messages.greeting(...) rather than bundle.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 .properties files; no special runtime registry.

Quick Start

1. Basic Messages

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!"
    }
}

2. Multi-Locale Support

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!"

3. Advanced Formatting

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"

Configuration Options

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 {}

Base Name

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.properties

File Organization:

src/main/resources/
├── messages.properties
├── messages_en.properties
├── messages_zh_CN.properties
└── i18n/
    ├── app.properties
    ├── app_en.properties
    └── app_fr.properties

Default Locale

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)

Generated Class Name

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., MessagesMessageResource)

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$$")        // → I18nAppMessages

Supported Locales

Optionally 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.

ICU MessageFormat Reference

Propify uses ICU4J MessageFormat for advanced formatting. Here's a comprehensive guide:

1. Simple Parameters

greeting=Hello, {0}!
welcome=Welcome, {0} {1}!
messages.greeting("Alice");              // "Hello, Alice!"
messages.welcome("John", "Doe");         // "Welcome, John Doe!"

2. Named Parameters (Alternative Syntax)

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"

3. Number Formatting

Basic Number

count=Count: {0,number}
messages.count(1234567);                 // "Count: 1,234,567"

Currency

price=Price: {0,number,currency}
messages.price(99.99);                   // "Price: $99.99" (US locale)
messages.price(99.99);                   // "Price: ¥99.99" (Chinese locale)

Percentage

discount=Discount: {0,number,percent}
messages.discount(0.25);                 // "Discount: 25%"

Custom Number Format

precise=Value: {0,number,#.##}
messages.precise(3.14159);               // "Value: 3.14"

4. Date/Time Formatting

Short Date

date.short=Date: {0,date,short}
messages.dateShort(new Date());          // "Date: 12/16/25"

Long Date

date.long=Date: {0,date,long}
messages.dateLong(new Date());           // "Date: December 16, 2025"

Full Date

date.full=Date: {0,date,full}
messages.dateFull(new Date());           // "Date: Tuesday, December 16, 2025"

Time Formatting

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"

Combined Date and Time

datetime=Date: {0,date,full} at {0,time,short}
messages.datetime(new Date());           // "Date: Tuesday, December 16, 2025 at 2:30 PM"

5. Plural Forms (ICU Plural)

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 matches
  • zero - Zero (language-specific)
  • one - Singular
  • two - 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{# сообщения}}

6. Choice Format

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"

7. Select Format (String-Based Choice)

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."

8. Complex Combinations

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."

Generated Code Structure

Access Patterns

// 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"));

Type-Safe Methods

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 items

Generated methods:

public interface LocaleMessages {
    String welcome();
    String greeting(String arg0);
    String userStatus(String arg0, int arg1);
    String complex(Date arg0, String arg1, double arg2);
}

Advanced Examples

1. Multi-Language Application

# 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());
    }
}

2. E-Commerce Messages

# 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"

3. Validation Messages

# 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"
}

4. Notification Messages

# 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"

Best Practices

1. Organize Message Files by Feature

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 {}

2. Use Descriptive Message Keys

# 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}

3. Keep Messages Consistent Across Locales

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

4. Use ICU Plural Forms Instead of Choice

# 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}

5. Provide Context in Comments

# 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} completed

6. Handle Missing Translations Gracefully

Always 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.

Troubleshooting

Message File Not Found

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

Invalid MessageFormat Pattern

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}

Missing Locale File

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

Parameter Type Mismatch

Problem: Method parameter doesn't match format

Solutions:

  • Use Date for {0,date,*} and {0,time,*}
  • Use number types (int, double) for {0,number,*}
  • Use String for simple {0} placeholders

Comparison with Alternatives

vs. Standard ResourceBundle

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)

vs. Spring MessageSource

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

vs. gettext

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

Limitations

  • Properties Format Only: Only supports .properties files (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

Runtime Dependencies

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.

Contributing

Found a bug or want to contribute? Please see the main CONTRIBUTING.md for guidelines.

License

MIT License - see LICENSE