Need help with your JSON?
Try our JSON Formatter tool to automatically identify and fix syntax errors in your JSON. JSON Formatter tool
Teaching JSON Internationalization Through Formatters
Internationalization (i18n) is the process of designing and developing a software application so that it can be easily adapted ("localized") to various languages and regions without engineering changes to the core code. A common approach is to externalize all user-facing text into translation files, often using the JSON format.
While simple key-value pairs work well for static strings, real-world applications often need to display dynamic content that varies by language and region, such as dates, numbers, currencies, and plurals. This is where formatters become essential. Teaching developers how to use formatters alongside JSON is key to building truly international applications.
Beyond Simple Key-Value Pairs
Consider a simple welcome message:
{ "welcomeMessage": "Welcome!" }
This is straightforward. But what about messages that include dynamic data?
// English "greetingWithUser": "Hello, {{userName}}!" // Spanish "greetingWithUser": "¡Hola, {{userName}}!"
Here, we introduce a placeholder ({{userName}}). The localization library or code will replace this placeholder with the actual user's name. This is a basic form of formatting – inserting a variable into a string template.
Common Formatting Needs in JSON i18n
Many types of data require specific formatting rules that differ significantly between locales:
- Dates and Times: Format (DD/MM/YYYY vs. MM/DD/YYYY), names of months/days, 12-hour vs. 24-hour clock.
- Numbers and Currencies: Decimal separators (
.
vs.,
), thousands separators, currency symbols ($ vs. €), currency placement (before/after value), number of decimal places. - Relative Times: "just now", "2 minutes ago", "in 3 months". The phrasing and grammar depend on the duration and target language.
- Plurals: How words change based on quantity (e.g., "1 item", "2 items", "0 items", and even more complex rules in languages like Arabic or Polish).
- Complex Messages: Messages where the structure changes significantly based on multiple variables or conditions (e.g., gender, item counts, etc.).
Storing these directly as simple translated strings for every possible variation is impractical and leads to an explosion of translation keys.
How JSON Stores Data Requiring Formatting
Instead of storing the final formatted string, the JSON typically stores a template string with placeholders that indicate not just *where* a value goes, but *how* it should be formatted. The actual formatting logic is handled by the application code using specific formatting tools.
A common standard for this is the ICU MessageFormat, widely supported by i18n libraries.
Examples of JSON with Formatting Placeholders:
Date Formatting:
en.json:
{ "lastLogin": "Last login: {loginDate, date, medium}" }
es.json:
{ "lastLogin": "Último inicio de sesión: {loginDate, date, medium}" }
Here, {loginDate, date, medium}
tells the formatter to take the value provided for loginDate
and format it as a date using the "medium" style according to the current locale's rules.
Number & Currency Formatting:
en.json:
{ "accountBalance": "Your balance is {balance, number, currency}." }
de.json:
{ "accountBalance": "Ihr Guthaben beträgt {balance, number, currency}." }
{balance, number, currency}
instructs the formatter to format the balance
value as a number representing currency, respecting locale-specific rules for separators, currency symbol, etc.
Plural Formatting:
en.json:
{ "itemCount": "{itemCount, plural, one {# item} other {# items}}" }
ru.json (Russian has multiple plural forms):
{ "itemCount": "{itemCount, plural, one {# товар} few {# товара} many {# товаров} other {# товара}}" }
The {itemCount, plural, ...}
construct is powerful. It takes the itemCount
and uses locale-specific plural rules to pick the correct phrase ("one", "few", "many", "other"). #
is a special placeholder for the number itself, already formatted.
Using Formatters in Code
The JSON file simply provides the template string. The actual formatting is done in the application code using built-in browser/Node.js APIs (like the Intl
object) or dedicated i18n libraries.
Libraries like react-intl
or formatjs
parse the ICU MessageFormat strings from your JSON and use the browser's or a polyfill's Intl
object to perform the locale-aware formatting.
Conceptual Code Example (using a hypothetical `translate` function):
// Assuming you have a dictionary loaded for the current locale // const messages = { /* content of en.json or es.json etc. */ }; // Assuming a translate function that uses a formatter library interface TranslationMessages { [key: string]: string; // ICU MessageFormat string } // This function would typically wrap a call to Intl.MessageFormat function translate(key: string, values?: { [key: string]: any }): string { // In a real app, this would: // 1. Look up the message string using the 'key' from the loaded locale messages // 2. Use a formatter library (e.g., new Intl.MessageFormat(messageString, currentLocale)) // 3. Format the message string using the provided 'values' object // 4. Return the formatted string console.log(`Translate called for key: ${key}, values: `, values); // --- Simplified placeholder logic for demonstration --- let template = `[MISSING TRANSLATION KEY: ${key}]`; // In a real app, you'd get this from your loaded JSON // For this static example, let's use hardcoded templates: const hardcodedTemplates: TranslationMessages = { "greetingWithUser": "Hello, {userName}!", "lastLogin": "Last login: {loginDate, date, medium}.", "accountBalance": "Your balance is {balance, number, currency}.", "itemCount": "{itemCount, plural, one {# item} other {# items}}", "itemCount_ru": "{itemCount, plural, one {# товар} few {# товара} many {# товаров} other {# товара}}" // Russian example }; const currentLocale = "en"; // Assume English for this example if (currentLocale === "ru" && key === "itemCount") { template = hardcodedTemplates["itemCount_ru"]; } else if (hardcodedTemplates[key]) { template = hardcodedTemplates[key]; } // A real formatter library would parse 'template' and 'values' // and produce the locale-aware output. // This is a VERY basic placeholder replacement, NOT a real formatter: let result = template; if (values) { for (const valKey in values) { // This simple replace doesn't handle plural/date/number types correctly // It only handles simple string replacement like {userName} const placeholderRegex = new RegExp(`{${valKey}}`, 'g'); if(placeholderRegex.test(result)){ // Check if simple placeholder exists result = result.replace(placeholderRegex, String(values[valKey])); } else { // This part would be handled by the actual formatter library // For date, number, plural formats, the placeholder looks different {key, type, style} // A real library would handle the complex parsing and formatting based on type/style. // Example: For "{loginDate, date, medium}", it finds loginDate, sees 'date, medium', // and calls Intl.DateTimeFormat(locale, { dateStyle: 'medium' }).format(values['loginDate']) // We can't replicate Intl here statically. // Let's just log a note about complex formatting for teaching purposes. // console.log(`Note: For key '${key}', value '${valKey}' might need complex formatting (date, number, plural).`); } } } // For keys requiring complex formatting in the real world, // the basic placeholder logic above is insufficient. // A library handles: // - "{loginDate, date, medium}" -> using Intl.DateTimeFormat // - "{balance, number, currency}" -> using Intl.NumberFormat // - "{itemCount, plural, ...}" -> using Intl.PluralRules and complex message parsing // The JSON structure provides the *instructions* ({type, style/rules}), // and the code with the formatter *executes* those instructions based on the locale. return result; // Returns the template string with simple placeholders replaced. } // --- How it would be used (conceptually) --- const userName = "Alice"; // Imagine user logs in, we get a Date object const loginDateTime = new Date(); // e.g., Fri Dec 17 2021 10:00:00 GMT+0000 // Imagine fetching balance from API const userBalance = 123456.789; // Imagine fetching item count const currentItemCount = 2; const singleItemCount = 1; const zeroItemCount = 0; const russianItemCountMany = 5; // Example for Russian 'many' plural form // --- How it renders (conceptually) --- // Using the conceptual translate function: // In a real app with a formatter: // English Locale: // translate('greetingWithUser', { userName: userName }) => "Hello, Alice!" // translate('lastLogin', { loginDate: loginDateTime }) => "Last login: Dec 17, 2021, 10:00 AM." (or similar medium format) // translate('accountBalance', { balance: userBalance }) => "Your balance is $123,456.79." // translate('itemCount', { itemCount: currentItemCount }) => "2 items" // translate('itemCount', { itemCount: singleItemCount }) => "1 item" // translate('itemCount', { itemCount: zeroItemCount }) => "0 items" // German Locale (de): // translate('accountBalance', { balance: userBalance }) => "Ihr Guthaben beträgt 123.456,79 €." (Note: decimal/thousands separator, currency symbol & placement) // Russian Locale (ru) - assumes different dictionary loaded and locale set // translate('itemCount', { itemCount: currentItemCount }) => "2 товара" (plural 'few') // translate('itemCount', { itemCount: singleItemCount }) => "1 товар" (plural 'one') // translate('itemCount', { itemCount: zeroItemCount }) => "0 товаров" (plural 'many' often used for 0) // translate('itemCount', { itemCount: russianItemCountMany }) => "5 товаров" (plural 'many') // Since this is a static page, we just show the concept:
As you can see, the JSON provides the rules ({value, type, style}), and the library/API interprets these rules based on the active locale and the provided value to produce the final, correctly formatted string.
Benefits of Using Formatters
- Reduced Translation Effort: Translators only need to provide the template string and the plural/select rules, not every single permutation of formatted data.
- Locale Correctness: Relies on established standards (like ICU) and built-in browser APIs (
Intl
) or robust libraries, ensuring correct date, number, and plural formatting for each locale. - Code Simplicity: Application code passes raw data (numbers, dates, counts) to the translation function; the complexity of formatting is abstracted away.
- Flexibility: Easily add support for new locales; the formatting rules are already defined by the standard or library.
Teaching Points for Developers
When introducing this concept, emphasize:
- Separation of Concerns: JSON stores *what* needs to be displayed and the *rules* for dynamic parts; code handles the *how* (the formatting logic).
- The Importance of Locale: Formatting isn't just about inserting values; it's about doing so according to the user's specific language and region settings.
- Using Standard APIs/Libraries: Encourage the use of the
Intl
object or well-maintained i18n libraries that implement standards like ICU MessageFormat, rather than building custom formatting logic. - Testing: Stress the importance of testing formatted messages with different data values (especially edge cases for plurals like 0, 1, 2, 5, 10, 20, 21, 100, 101 etc.) and in different locales.
Conclusion
Teaching developers to leverage formatters is crucial for building scalable and correctly localized applications. By using standards-based templates in JSON translation files and employing powerful formatting tools in the code, developers can handle the complexities of locale-aware data presentation effectively. This moves beyond simple text replacement, enabling applications to feel truly native to users around the world.
Need help with your JSON?
Try our JSON Formatter tool to automatically identify and fix syntax errors in your JSON. JSON Formatter tool