Internationalization (i18n)
Internationalization (commonly abbreviated as i18n, because there are 18 letters between the leading i and trailing n) is the practice of designing an application so that it can be adapted to different languages, regions, and cultural conventions without changes to the underlying code.
What i18n actually means#
Internationalization is not the act of translating strings. It is the work that happens before translation is possible: structuring the application so that text, numbers, dates, currencies, plural forms, gendered phrasing, and reading direction are all driven by data instead of being hard-coded for a single audience.
A well internationalized application is one where:
- user-visible strings are looked up by key, not written inline
- numbers, dates, times, and currencies are formatted according to a locale rather than a fixed format
- plural rules, gender, and other grammatical variation are expressed in a way the runtime can resolve per locale
- reading direction (left-to-right or right-to-left) is metadata the UI can react to
- adding a new locale does not require touching application code
Internationalization is the enabling work. Localization is what becomes possible once that work is done.
Why i18n is usually painful#
In most codebases, internationalization is bolted on after the fact:
- string keys are sprinkled across the source tree with no schema
- formatting rules live as string concatenations and hand-rolled helpers
- the "translations file" is a flat JSON blob that any key can be added to
- there is no test that proves a given key resolves correctly for a given locale, context, or set of runtime values
- behavioral differences between regions (VAT-inclusive pricing, address layouts, plan availability) end up as
if (region === 'DE')branches in the application
Shipping a new locale ends up being a risky, weeks-long project, and regressions in existing locales are easy to miss.
How Messagevisor approaches i18n#
Messagevisor treats internationalization as a structured, declarative system instead of a loose collection of files. The pieces that traditionally get scattered across an application live together in one project:
Locales describe languages, not just keys#
A locale is more than a folder of translations. It is a typed definition that carries description, direction, named format presets, and optional inheritance from a base locale.
description: Arabic (Saudi Arabia)direction: rtlinheritFormatsFrom: arThe SDK exposes getDirection(locale?) and related helpers so the application can react to locale metadata without inventing its own table.
Messages declare structure, not just strings#
Messages carry a description, a translation map, optional metadata, examples, and conditional overrides. The message file is the canonical record of what a string is for, not just what it says.
description: Total amount in the billing summarytranslations: en: "Total: {amount, number, money}" nl: "Totaal: {amount, number, money}"That structure is what makes lint rules, tests, and the catalog possible.
Formats are a shared vocabulary#
Number, date, time, relative-time, and date-time-range formatting are authored as named format presets on the locale, modeled as a subset of the standard Intl APIs.
The application calls translate("billing.total", { amount: 149.99 }) and the runtime resolves both the right string and the right currency formatting. The format definition lives with the locale, not in the application.
Pluggable message syntax#
The message syntax itself is a module. Use @messagevisor/module-icu for full ICU MessageFormat with plurals, selects, and rich variable typing, or @messagevisor/module-interpolation for lightweight {name} interpolation. Either way, the choice is made at SDK construction, not baked into the content model.
Inheritance for regional variants#
Regional locales like en-GB, en-US, nl-NL, and nl-BE rarely diverge fully from their base language. Messagevisor's inheritance lets a regional locale reuse translations, formats, and examples from a base and only author what actually differs.
description: English (United Kingdom)inheritTranslationsFrom: eninheritFormatsFrom: enformats: number: money: style: currency currency: GBPTargets carry per-application overrides#
Different surfaces (web, mobile, email, marketing) often have different i18n requirements: shorter strings, different currency display, different included messages. Targets let you define those differences explicitly and build a separate datafile per surface.
What you get from doing i18n this way#
- Adding a locale is a pull request. Author a new locale file, optionally inherit from a base, and run the build. No application code change is required.
- i18n behavior is testable. Tests assert that a specific message resolves correctly for a given locale, target, and runtime context. CI catches regressions before they reach users.
- One source of truth across surfaces. Web, mobile, backend, and email all read from datafiles built from the same definitions, so they cannot drift.
- The hard parts live in one place. Plural rules, currency display, RTL metadata, and per-region behavioral overrides all live in the project, not scattered across the application.
i18n vs l10n#
Internationalization and localization are commonly conflated. The short version:
- i18n is the structural work that makes localization possible: data-driven strings, format presets, locale metadata, runtime resolution.
- l10n is the ongoing act of adapting the system to a specific locale: translating strings, picking the right currency display, writing examples, validating that the result reads naturally to a native speaker.
Messagevisor is built to support both, and to keep them on the same Git timeline as the rest of the product.

