PTC

Java Internationalization Guide: Translate .properties Files with AI

Set up ResourceBundle, structure .properties files, translate into 40+ languages with AI, then have PTC (Private Translation Cloud) review the running Java app via screenshots or browser extension. By the end you will have a multilingual JAR ready to ship, with every target language verified before release. For the standalone Java service overview, see translate Java apps with AI.

ResourceBundle loads the right .properties file at runtime

Java's i18n system is built around two things. .properties files that store your translated strings, and the ResourceBundle class that loads the right file at runtime based on the user's locale.

When your app runs, ResourceBundle checks the user's locale and loads the matching file automatically. If a translation is missing, it falls back to the default file silently. Nothing breaks, but missing translations do not appear as errors either. Calling a string in code:

ResourceBundle bundle = ResourceBundle.getBundle("messages", Locale.FRENCH);
String greeting = bundle.getString("welcome.message");

That is the whole mechanism. The rest of localization work happens in the .properties files themselves, which is why structuring them correctly matters.

Set up your resource bundle with messages_{locale}.properties

A resource bundle is a set of .properties files that share a common base name. The base name is the part of the filename before the locale suffix. It is what ResourceBundle.getBundle() uses to find the right file at runtime.

src/main/resources/
  messages.properties        # default (usually English)
  messages_fr.properties     # French
  messages_de.properties     # German
  messages_es.properties     # Spanish
  messages_ja.properties     # Japanese
  messages_zh_CN.properties  # Simplified Chinese (note underscore, not hyphen)

Larger applications often use multiple resource bundles to keep things organized:

src/main/resources/
  messages.properties
  errors.properties
  emails.properties

Java expects a specific naming pattern. basename_language.properties, or basename_language_COUNTRY.properties for regional variants:

messages_fr.properties      # French
messages_fr_CA.properties   # French (Canada)
messages_pt_BR.properties   # Portuguese (Brazil)

Language codes follow ISO 639-1. Country codes follow ISO 3166-1. Using the wrong format means ResourceBundle will not find the file at runtime.

Load and use the bundle in code, with MessageFormat for placeholder substitution:

import java.util.Locale;
import java.util.ResourceBundle;
import java.text.MessageFormat;

public class App {
    public static void main(String[] args) {
        Locale locale = Locale.of("es");
        ResourceBundle messages = ResourceBundle.getBundle("messages", locale);

        String welcome = MessageFormat.format(
            messages.getString("app.welcome"),
            "My App"
        );
        System.out.println(welcome);
        // -> "Bienvenido a My App"
    }
}

For simple strings without placeholders, messages.getString("key") is enough.

Six conventions that make your .properties files translation-ready

Each line is a key-value pair separated by =. How you write your source file directly affects the quality of your translations. Whether you translate manually or with an AI tool like PTC.

1. Use clear, descriptive keys that name where the string appears

Keys should make it obvious where and how a string is used. This matters when you are managing hundreds of strings across multiple files.

# Incorrect
btn1 = Submit
msg2 = Error

# Correct
form.submit.button = Submit
error.login.invalid_credentials = Invalid username or password

Never change a key after translation has started. Changing a key orphans the existing translation.

2. Use numbered placeholders, not string concatenation in code

Write the full sentence in your .properties file and use numbered placeholders for variable content instead of concatenating strings in code.

// Incorrect (in code)
"Hello, " + username + "! You have " + count + " new messages."
# Correct (in .properties)
dashboard.greeting = Hello, {0}! You have {1} new messages.

Many languages change word order and agreement rules, so splitting sentences into fragments makes correct translation impossible.

3. Handle pluralization with ChoiceFormat, ICU, or suffix keys

For pluralization in standard Java, ChoiceFormat works directly inside .properties:

messages.count = {0,choice,0#no messages|1#one message|1<{0} messages}

Java processes this at runtime and returns the right form based on the value passed in. ChoiceFormat is simple but limited to numeric-range matching. It does not natively handle complex plural rules.

For language-aware plurals (Polish's one/few/many/other, Arabic's six forms), use ICU4J's MessageFormat:

String pattern = "{0, plural, one {# note} other {# notes}}";
String result = new com.ibm.icu.text.MessageFormat(pattern, locale).format(new Object[]{count});

Or encode plurals as separate keys with conventional suffixes so PTC can generate the right plural categories per language:

notes.count.zero=No notes yet
notes.count.one={0} note
notes.count.other={0} notes

PTC generates the right plural categories per target language. Polish gets one / few / many / other. Japanese gets only other.

4. Escape =, :, #, and \ with a backslash

Characters like =, :, #, and \ have special meaning in .properties files:

  • = or : separates keys from values.
  • # or ! starts a comment.
  • \ introduces escape sequences (like \n for newline).

Escape with a backslash where needed:

support.link = Visit us at https\://support.example.com

5. Save .properties files as UTF-8

Always save .properties files in UTF-8. Without it, non-ASCII characters become corrupted and translations become unreadable. Historically Java .properties files were ISO-8859-1, requiring \uXXXX escapes for non-ASCII characters. Java 9+ reads them as UTF-8 by default, so check your runtime version before relying on raw UTF-8.

6. Keep all user-facing text out of code

If a string is visible to users, it belongs in a .properties file. Hardcoded strings will not get translated. Your app will end up showing a mix of languages.

Format dates, times, numbers, and currency with locale-aware helpers

Not everything that needs localizing lives in a .properties file. Dates, times, numbers, and currency values are formatted in code at runtime, and getting them right matters as much as your translated strings.

Dates with DateTimeFormatter:

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;

LocalDate today = LocalDate.now();
DateTimeFormatter formatter = DateTimeFormatter
    .ofLocalizedDate(FormatStyle.LONG)
    .withLocale(Locale.of("fr"));
System.out.println(today.format(formatter));
// -> "27 mai 2026"

Currency with NumberFormat:

import java.text.NumberFormat;
import java.util.Currency;

NumberFormat formatter = NumberFormat.getCurrencyInstance(Locale.of("de", "DE"));
formatter.setCurrency(Currency.getInstance("EUR"));
System.out.println(formatter.format(1999.99));
// -> "1.999,99 EUR"

Always use locale-aware formatters. Never hardcode "$", "," thousand separators, or "MM/DD/YYYY" patterns.

Wire ResourceBundle into Spring Boot with MessageSource

Spring Boot wraps ResourceBundle in a MessageSource bean that integrates with the framework's i18n features. Validation messages, Thymeleaf templates, web request locale resolution.

Configure in application.properties:

spring.messages.basename=messages
spring.messages.encoding=UTF-8
spring.messages.fallback-to-system-locale=false

Place messages.properties, messages_es.properties, etc. under src/main/resources/.

Use in a controller:

import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;

@RestController
public class GreetingController {
    private final MessageSource messageSource;

    public GreetingController(MessageSource messageSource) {
        this.messageSource = messageSource;
    }

    @GetMapping("/greeting")
    public String greeting(@RequestParam String name) {
        return messageSource.getMessage(
            "app.greeting",
            new Object[]{name},
            LocaleContextHolder.getLocale()
        );
    }
}

Configure the locale resolver to read from the Accept-Language header or a URL parameter:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;

@Configuration
public class I18nConfig implements WebMvcConfigurer {
    @Bean
    public LocaleResolver localeResolver() {
        AcceptHeaderLocaleResolver resolver = new AcceptHeaderLocaleResolver();
        resolver.setDefaultLocale(Locale.ENGLISH);
        return resolver;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        LocaleChangeInterceptor interceptor = new LocaleChangeInterceptor();
        interceptor.setParamName("lang");
        registry.addInterceptor(interceptor);
    }
}

Now GET /greeting?name=World&lang=es returns the Spanish version.

Translate Java .properties files with PTC in 5 steps

  1. Start a PTC project and pick your source locale (English / messages.properties).
  2. Upload your .properties file(s) and set output paths. PTC parses the key-value structure, recognises MessageFormat placeholders ({0}, {1}) and ChoiceFormat patterns, and reads any # comments as translator context.
  3. Add a short description of your Java application and audience. PTC uses this to translate with the right tone and terminology.
  4. Pick target languages and confirm. The free trial covers 20,000 words into 2 languages, no credit card.
  5. Download translated .properties files from the Resource files tab. One per language, with the right suffix (messages_es.properties, messages_fr.properties). Structurally identical to the source: same keys, same placeholders, translated values.

Drop them into src/main/resources/, rebuild, and ResourceBundle.getBundle("messages", locale) picks up the new languages automatically. The whole setup takes about 5 minutes.

Automate Java translation on every release with Git or the PTC API

Translating once is straightforward. Keeping translations up to date as your application evolves is harder. Every new string, every copy update, every removed key needs to flow through to every language. PTC offers two ways to automate it.

Git Integration. Connect your GitHub, GitLab, or Bitbucket repository to PTC. PTC monitors your source .properties file for changes. When a string is added or updated, PTC translates it and delivers the updated files back via a pull request.

CI/CD Integration. If you prefer to keep everything inside your existing build process, PTC's API lets you upload your source file and retrieve translations as part of your CI job:

# .github/workflows/translate.yml
name: PTC translate
on:
  push:
    branches: [main]
    paths:
      - 'src/main/resources/messages.properties'
jobs:
  translate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Trigger PTC translation
        run: |
          curl -X POST https://api.ptc.wpml.org/v1/projects/${{ secrets.PTC_PROJECT_ID }}/sync \
            -H "Authorization: Bearer ${{ secrets.PTC_API_KEY }}"

PTC syncs the new source strings, translates only what changed, and opens a PR with the updated messages_es.properties, messages_fr.properties, and so on. For Maven and Gradle projects, the same flow works. PTC does not care about your build tool. Both approaches mean adding a new language later is a configuration change, not a new manual process.

Visual translation review of your translated Java app - ship without manual QA per language

A translated .properties file is necessary but not sufficient. Whether your Java app is a Spring Boot web service serving HTML, a desktop Swing app, or a server-side CLI tool, the rendered output may have issues no string-level review can catch:

  • A German label that overflows a Swing button.
  • A French validation message with the wrong grammatical form.
  • A hardcoded English string outside messageSource.getMessage() that shows untranslated.

PTC's Visual AI Review covers both flavors of Java app:

  • For Spring Boot or any web-based Java app: install the PTC browser extension and record a walkthrough of your app's critical pages. PTC replays it in every target language after every translation update.
  • For desktop (Swing, JavaFX), server (CLI), or any non-browser Java app: upload screenshots of the running Java app in each target language. PTC's vision AI inspects every screen.

Issues PTC can fix in the .properties files (verb/noun, layout overflow, wrong sense) get fixed automatically. Issues in your Java code (missing messageSource.getMessage() call, hardcoded string, sentence concatenation that should use MessageFormat) come back as ready-to-paste prompts for Cursor or Claude Code.

The result: a verified, multilingual JAR per release. Not just translated property files.

Translate release notes, READMEs, and customer emails

Your release notes, READMEs on your internal Maven repository or GitHub, customer-facing emails, support documentation, and internal wiki pages live outside .properties. PTC's Paste to Translate handles that copy in the same project. Paste the source text in the PTC dashboard, choose target languages, get back translations that use the same glossary and brand voice as your in-app strings.

Translate enterprise data, support tickets, and customer content with the PTC API

Enterprise data, support tickets, knowledge-base entries, and customer-submitted content need translation as it arrives. The PTC REST API translates this content on demand with Bearer-token authentication, using the same glossary and brand voice as your .properties translations.

Translate AND review your Java app

Start your free 30-day trial - 20,000 words into 2 languages, no credit card. Upload your .properties files, get translated versions in minutes, then upload screenshots (or install the browser extension for Spring Boot) and let PTC verify the running app.

Related: