Creating Grails Application with Internationalization and Localization
A Grails application can serve users in several countries without duplicating controllers, views, or domain logic. Internationalization, often shortened to i18n, prepares the codebase for multiple languages and regional conventions. Localization, or l10n, then applies the language, date, number, currency, and cultural rules selected for a particular audience.
This guide to Creating Grails Application with Internationalization and Localization uses Grails conventions, Groovy examples, and practical configuration patterns. It also considers an Australian product, where en_AU, Australian dollars, metric measurements, multilingual customers, and obligations under local legislation can all influence the design.
Define a locale strategy before coding
A locale represents more than a translation. It combines a language with regional preferences, such as en_AU for Australian English, en_GB for British English, or fr_FR for French used in France. A language-only locale such as fr can be useful as a fallback, while a country-specific locale provides more precise formatting.
Start by listing the markets the application will support. An online service based in Sydney may initially use Australian English and Australian dollars, then expand to New Zealand, Singapore, or French-speaking customers. Decide whether the default locale comes from the browser, an account preference, a URL segment, a cookie, or a server-side profile. A predictable priority order prevents a stored preference from being unexpectedly overridden by a browser setting.
The locale should be treated as request context rather than business data. An invoice's tax rules and currency should usually be determined by the transaction and customer account, while the language used to display that invoice can follow the current user's preference. Keeping those concerns separate avoids errors when an Australian customer travels overseas or uses a browser configured for another language.
Organise message bundles and application text
Grails applications commonly store translated messages in grails-app/i18n/messages.properties. The base file acts as the fallback bundle, and locale-specific files add or replace values. A useful structure might include:
grails-app/i18n/
├── messages.properties
├── messages_en_AU.properties
├── messages_en_GB.properties
├── messages_fr_FR.properties
└── messages_zh_CN.properties
Use stable keys instead of placing literal text directly in GSP pages or controllers. For example:
# messages.properties
account.welcome=Welcome, {0}
order.total=Order total
validation.required={0} is required
# messages_en_AU.properties
account.welcome=Welcome, {0}
order.total=Order total
The key should describe the meaning or context, rather than a particular translation. checkout.deliveryAddress is more maintainable than english.deliveryLabel, especially when translators need to rearrange a sentence. Keep placeholders documented and avoid assembling sentences from many independent fragments because word order differs between languages.
For controller and service messages, inject or use Grails' message source rather than reading files manually. In a controller, a message can be resolved with a locale and arguments:
def messageSource
String greeting(Locale locale, String name) {
messageSource.getMessage(
'account.welcome',
[name] as Object[],
locale
)
}
A missing translation should fall back sensibly, but it should still be visible during development and testing. Logging unresolved keys and reviewing the default bundle as part of a release checklist helps prevent untranslated labels from reaching customers.
Resolve and persist the user's language
Grails provides locale support through Spring's internationalization infrastructure. The exact configuration differs between Grails versions, but the application should expose one clear mechanism for changing locale. A controller action can accept a locale parameter, validate it against an allowlist, and store the selection in a session or cookie.
For example, a simple action might look like this:
class LocaleController {
static allowedMethods = [change: 'POST']
def change(String language, String redirectUri) {
def supported = ['en_AU', 'en_GB', 'fr_FR', 'zh_CN']
if (language in supported) {
session.locale = Locale.forLanguageTag(language.replace('_', '-'))
}
redirect(uri: redirectUri ?: '/')
}
}
In a production application, use a configured locale resolver or interceptor so every request receives the same behaviour. Validate the redirect destination as well, since accepting arbitrary redirect URLs can create an open redirect vulnerability. A fixed route or a same-origin path is safer than trusting a complete URL submitted by the browser.
A language selector should be accessible from every important page and should display language names in a way users can understand. Flags are poor substitutes for language names because languages cross national borders. If a visitor selects Chinese, for example, the application may need a specific script choice such as Simplified or Traditional Chinese rather than relying on a country flag.
Store a preference against an authenticated account when the user has deliberately selected one. For anonymous visitors, a secure, appropriately scoped cookie is usually more practical than a server session alone. Explain the cookie's purpose in the privacy documentation and apply the requirements relevant to the Privacy Act 1988 when the preference is linked to an identifiable person.
Localise GSP views, forms, and navigation
GSP tags and message lookups should carry the visible wording in one place. A page can render a translated heading and a field label like this:
<h1><g:message code="account.heading" /></h1>
<label for="email">
<g:message code="account.email" />
</label>
<g:textField name="email" value="${user.email}" />
Validation messages should use the same message source as page text. Grails constraints can define a meaningful property name, while locale-specific bundles provide the actual wording:
class Customer {
String email
static constraints = {
email email: true, blank: false
}
}
A validation key such as customer.email.blank can be translated for each supported locale. Avoid assuming that a sentence has a fixed grammatical order. Some languages place the field name after the explanation, and some require different plural forms. Give translators complete messages with named context wherever the framework or message format allows it.
Dates, times, quantities, and money should be formatted with locale-aware tools rather than string concatenation. An Australian customer generally expects 31/12/2025, decimal points for fractions, commas for thousands, and $ for Australian dollars. However, storing dates in UTC and using a time zone for display remains important because Sydney and Melbourne observe daylight saving while Brisbane does not.
A product catalogue may need more than translated labels. Measurements might be displayed in kilograms, kilometres, or litres for the Australian market, while a United States customer may expect miles or pounds. Product descriptions, shipping estimates, tax labels, and return information should all be reviewed for regional meaning. Australian Consumer Law can affect how guarantees, refunds, and consumer rights are described, so those messages should be reviewed by an appropriate legal or compliance specialist.
Design persistence, APIs, and security for multiple languages
Internationalization often exposes assumptions in the data model. If a product, category, or article needs translated content, decide whether to use separate translation records, a JSON structure, or language-specific columns. Separate records with a locale field are easier to query, validate, and extend when content editors need translation status, publication dates, or reviewer information.
Use Unicode throughout the application and database. Grails, Java, and modern relational databases support Unicode well, but connection settings, import files, search indexes, and external integrations still need checking. Names containing accented characters, Chinese characters, or right-to-left scripts should be accepted where the business rules allow them. A validation rule based on ASCII-only assumptions can reject legitimate customers.
APIs should carry locale information in a documented way, such as an Accept-Language header or an explicit locale parameter. Do not let a client silently change the currency or tax jurisdiction merely by changing a language header. Language affects presentation; financial and legal rules need trusted account, address, catalogue, or transaction data.
Security messages deserve careful treatment. Account recovery, password reset, and access-denied responses should not reveal whether a particular email address exists. Translate the same security-safe response for every locale. Keep audit events in a consistent operational language if support staff need to search them, while showing customer-facing explanations in the user's chosen language.
Before deployment, measure the performance effect of loading large bundles, translated catalogue content, and locale-aware formatting. Profiling can reveal expensive database calls or repeated message lookups; the Grails performance profiling guide provides a practical reference for investigating those bottlenecks.
Test regional behaviour and translation quality
Automated tests should cover locale selection, fallback behaviour, message arguments, validation errors, and formatting. A controller test can submit a locale change, follow the redirect, and verify that the next response uses the expected language. Integration tests should confirm that the locale survives the chosen session, cookie, or account-preference mechanism.
Test Australian conventions explicitly rather than assuming that English means one universal format. Check dates around daylight-saving transitions, currency values containing cents, negative amounts, long translated labels, and addresses from different states and territories. A layout that works in a short English button label may overflow when translated into German or become unusable when a larger Chinese or Arabic font is applied.
A release process benefits from separate technical and linguistic checks.
- Verify every supported locale has a fallback path.
- Check placeholders, plural forms, and HTML escaping.
- Test keyboard navigation and screen-reader labels.
- Review dates, currency, units, and time zones.
Translation review should use realistic screens instead of isolated key-value files. Context helps translators distinguish “Order” as a purchase from “order” as an instruction. Screenshots, character limits, product terminology, and notes about Australian usage can prevent technically valid translations from sounding unnatural.
For a growing application, add checks that compare message keys across bundles. A missing key should fail a build or create a visible warning rather than appearing only after a customer reaches a rarely used page. Track translations as part of the release version, and avoid changing a key's meaning while an older mobile client or cached page may still depend on it.
Choose practical translation workflows
A small Grails project can manage properties files in Git, with developers and translators working from a documented key list. Larger teams may use a translation management platform, but the exported files should still be reviewed in the application. Automated imports need safeguards against malformed placeholders, accidental HTML, duplicate keys, and translations that replace security-sensitive variables.
Content editors should know which text is translatable and which values are generated. Brand names, product codes, legal entity names, and user-generated content may need special handling. A translated interface does not automatically translate customer comments, uploaded documents, or search data, so the scope of the feature should be clear.
A useful workflow separates language coverage from regional formatting.
- Maintain a base language for fallback content.
- Review Australian English spelling and terminology.
- Record ownership for legal and transactional messages.
- Re-test bundles whenever keys or placeholders change.
Deployment checks should cover cache invalidation and packaging. If message files are changed without rebuilding or refreshing the application, some nodes may serve older translations. In a cloud environment with multiple instances, deploy the same bundle version everywhere and monitor logs for missing keys after release.
Operational support also needs a language plan. Record the selected locale with relevant support context, but avoid exposing unnecessary personal information in logs. Give support staff a way to reproduce a customer's language and region without changing the underlying financial or legal calculations.
An internationalised Grails application is easier to extend when locale decisions are made early, message keys remain stable, and presentation rules stay separate from domain rules. Begin with a small supported set such as en_AU and one additional market, add automated checks, and expand only when translations, formatting, accessibility, and compliance can be maintained together. Build the first feature around real Australian addresses, dates, prices, and customer journeys, then carry that discipline into every new locale.