Shipping a Java app to more than one country should not feel like pulling teeth. The pieces are there. We just need to wire them in a way that does not punish the team.
\n\n\n\nHere is a straight path to ResourceBundle that avoids the usual traps and keeps your strings sane, your dates right, and your users happy.
\n\n\n\nWhy care before you ship?
\n\n\n\nEvery string baked into code will come back to haunt you. Internationalization is cheaper now than after QA finds English in places it should not be.
\n\n\n\nPlan keys, files, and formats from day one. Your future self will thank you when a new market shows up out of nowhere.
\n\n\n\nWhere does it hurt in Java today?
\n\n\n\nPlain .properties files are read as ISO 8859 1. Anything outside that set must use native2ascii escapes. That breaks eyeballs and slows translators.
\n\n\n\n# ISO 8859 1 with escapes\ngreeting=\\u3053\\u3093\\u306B\\u3061\\u306F\nbutton.save=Save\nerror.user=Usuario no v\\u00E1lido\n\n\n\nYou can keep running native2ascii in Ant or Maven, or skip the pain with a tiny Java 6 feature.
\n\n\n\nCan we keep properties in UTF 8 and move on?
\n\n\n\nYes. Java 6 added ResourceBundle.Control. With one class we can read bundles as UTF 8, no escapes, human friendly for translators.
\n\n\n\nimport java.io.*;\nimport java.net.URL;\nimport java.net.URLConnection;\nimport java.util.*;\n\npublic class UTF8Control extends ResourceBundle.Control {\n @Override\n public ResourceBundle newBundle(String baseName, Locale locale, String format,\n ClassLoader loader, boolean reload)\n throws IllegalAccessException, InstantiationException, IOException {\n\n String bundleName = toBundleName(baseName, locale);\n String resourceName = toResourceName(bundleName, "properties");\n InputStream stream = null;\n\n if (reload) {\n URL url = loader.getResource(resourceName);\n if (url != null) {\n URLConnection connection = url.openConnection();\n connection.setUseCaches(false);\n stream = connection.getInputStream();\n }\n } else {\n stream = loader.getResourceAsStream(resourceName);\n }\n\n if (stream == null) return null;\n try (InputStreamReader reader = new InputStreamReader(stream, "UTF-8")) {\n return new PropertyResourceBundle(reader);\n }\n }\n}\n\n\n\n// Usage\nLocale es = new Locale("es", "AR");\nResourceBundle msg = ResourceBundle.getBundle("i18n.messages", es, new UTF8Control());\nSystem.out.println(msg.getString("greeting"));\n\n\n\nNow i18n/messages_es_AR.properties can live as real UTF 8 text. No more escapes. Less build glue.
\n\n\n\nHow should we name bundles and keys?
\n\n\n\nMatch bundle files with code packages. Keep keys stable and boring. Avoid full sentences in keys. The value holds the sentence.
\n\n\n\n- \n
- i18n.messages.properties default for en \n
- i18n.messages_es.properties Spanish generic \n
- i18n.messages_es_AR.properties Spanish Argentina \n
# Good keys\nnav.home=Home\nuser.login.title=Sign in\nuser.login.error=Wrong user or password\n\n# Avoid keys like:\n# SIGN_IN_PAGE_TITLE\n\n\n\nHow do we format messages with data?
\n\n\n\nUse MessageFormat. It respects the Locale and gives translators a stable order of parts.
\n\n\n\n// messages.properties\nwelcome=Hello {0}, you have {1,number} new messages\n\n// Java\nResourceBundle rb = ResourceBundle.getBundle("i18n.messages", locale, new UTF8Control());\nString text = java.text.MessageFormat.format(rb.getString("welcome"), userName, 3);\nSystem.out.println(text);\n\n\n\nRemember that single quotes are special in MessageFormat. Escape them by doubling: It”s.
\n\n\n\nWhat about plurals without ifs everywhere?
\n\n\n\nUse ChoiceFormat inside MessageFormat. It keeps logic inside the text where translators can adjust it.
\n\n\n\n# messages.properties\ninbox={0,choice,0#No new mail|1#One new mail|1<{0,number} new mails}\n\n// Java\nString s = MessageFormat.format(rb.getString("inbox"), count);\n\n\n\nFor languages with tricky plurals, split into separate keys per form. Keep it clear for translators.
\n\n\n\nHow do we plug this into web views?
\n\n\n\nWith JSP and JSTL fmt, load the bundle and print keys. The tag lib respects the request locale.
\n\n\n\n<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>\n<fmt:setBundle basename="i18n.messages" />\n<fmt:message key="welcome">\n <fmt:param value="${user.name}" />\n <fmt:param value="${inboxCount}" />\n</fmt:message>\n\n\n\nWith JSF, use f:loadBundle. With Spring MVC, wire a ResourceBundleMessageSource.
\n\n\n\n<f:loadBundle basename="i18n.messages" var="msg"/>\n#{msg['user.login.title']}\n\n\n\nWhat is the fallback story?
\n\n\n\nGiven base name and locale, Java looks in this order: language plus country, then language, then default. So messages_es_AR then messages_es then messages.
\n\n\n\nKeep base files complete. Region files should be small and only override what truly changes.
\n\n\n\nHow do we format dates, numbers and money?
\n\n\n\nNever hardcode patterns unless you must. Use DateFormat and NumberFormat with the user locale. Currency picks symbols and separators for you.
\n\n\n\nLocale userLocale = locale;\nDate now = new Date();\n\nDateFormat df = DateFormat.getDateInstance(DateFormat.MEDIUM, userLocale);\nNumberFormat nf = NumberFormat.getNumberInstance(userLocale);\nNumberFormat cf = NumberFormat.getCurrencyInstance(userLocale);\n\nSystem.out.println(df.format(now));\nSystem.out.println(nf.format(12345.678));\nSystem.out.println(cf.format(49.9));\n\n\n\nAny tips for right to left languages?
\n\n\n\nFor Swing, set ComponentOrientation from the locale. In HTML, use dir=”rtl” for layout and keep strings translated in the proper order.
\n\n\n\ncomponent.applyComponentOrientation(\n ComponentOrientation.getOrientation(new Locale("ar"))\n);\n\n\n\nHow do we test this without breaking the dev box?
\n\n\n\nWrite a small helper to set the Locale for a test run. Drive screens and logs in that locale and scan for English leaks.
\n\n\n\n@Test\npublic void es_ar_smoke() {\n Locale.setDefault(new Locale("es", "AR"));\n ResourceBundle rb = ResourceBundle.getBundle("i18n.messages", Locale.getDefault(), new UTF8Control());\n assertEquals("Guardar", rb.getString("button.save"));\n}\n\n\n\nWhat about the build?
\n\n\n\nIf you use the UTF 8 control, you can skip native2ascii. Keep your editor and repo set to UTF 8 for properties. Do not filter these files in the build.
\n\n\n\n<resources>\n <resource>\n <directory>src/main/resources</directory>\n <filtering>false</filtering>\n <includes>\n <include>i18n/</include>\n </includes>\n </resource>\n</resources>\n\n\n\nIf you must stick to ISO 8859 1, add an Ant native2ascii step. It works but adds moving parts.
\n\n\n\n<target name="i18n-escape">\n <native2ascii encoding="UTF-8" src="src/main/resources/i18n"\n dest="target/classes/i18n"/>\n</target>\n\n\n\nWhat mistakes should we avoid?
\n\n\n\n- \n
- Concatenating pieces like “Hello ” plus name plus “!”. Use MessageFormat instead. \n
- Reusing a key for two different sentences. One key per meaning. \n
- Putting HTML in bundles for desktop apps or Swing mnemonics in web text. Keep contexts clean. \n
- Hardcoding units and time zones. Pass data already in the right unit and zone. \n
- Letting translators edit keys. Give them only values and notes. \n
How do we brief translators without confusion?
\n\n\n\nAdd comments next to tricky keys. Describe placeholders. Mention brand names that never change.
\n\n\n\n# Shows on the first run wizard. {0} is product name. Do not translate {0}.\nsetup.welcome=Welcome to {0}! Let us set things up.\n\n\n\nA short glossary for the project helps a lot. Keep it in the repo next to the bundles.
\n\n\n\nWhat is a simple rollout plan?
\n\n\n\nPick two target locales. Externalize all strings. Add the UTF 8 control. Wire JSTL or your view tech. Run UI smoke tests for both locales.
\n\n\n\nThen hand off the base file to translators. Pull back their UTF 8 files, commit, and ship a build to QA with locale toggles visible.
\n\n\n\nQuick win: Add a hidden query param like ?lang=es_AR in dev to flip the locale per request. It speeds up testing and demos.
Eclipse Europa and NetBeans 6 both handle UTF 8 properties well if the file encoding is set. Check your editor settings before you blame Java.
\n\n\n\nWrap up
\n\n\n\nKeep bundles in UTF 8, load them with a tiny ResourceBundle.Control, format text with MessageFormat, and let the Locale do the heavy lifting for dates and numbers.
\n\n\n\nDo this and internationalization in Java** stops being a headache. Fewer build hacks, clearer text for translators, and smoother releases for you.
\n