I18n Library provides multi-language support for OutSystems Reactive Web and Mobile apps through client actions. Translations are loaded from JSON files at runtime — no redeployment needed to add or update languages.
How it works:
App starts → InitTranslation loads default JSON → Screen opens → LoadScreenResource loads screen JSON → Translate expressions render text
Automatic re-render (new): you no longer need a TranslationRevision client variable or any “ghost dependency”. Translations refresh on screen automatically when a JSON file finishes loading or when the locale changes. See Automatic Re-render.
TranslationRevision
ur-PK
he-IL
fa-IR
%{opt1}
data-trans
/locales/
/locales/ ├── default/ ← shared keys (nav, footer, common) — loaded at app start │ ├── en-US.json │ └── ar-SA.json ├── dashboard/ ← screen-specific keys — loaded on demand │ ├── en-US.json │ └── ar-SA.json ├── profile/ │ ├── en-US.json │ └── ar-SA.json └── settings/ ├── en-US.json └── ar-SA.json
Rule: The default/ folder is loaded automatically at app start. Screen folders are loaded on-demand. You only need JSON files for locales you actually translated — missing files 404 silently and fall back to the default locale.
default/
InitTranslation
LoadScreenResource
Translate
SwitchLocale
TranslateValidationMessages
ObserveDOM
DisconnectObserver
SetConfig
GetConfig
Where: Application → OnReady (runs once on app load)
BasePath
/
TranslationMap
ResourceVersion
"v1.5"
?t=v1.5
Locales
["en-US"]
DefaultLocale
"en-US"
TranslationMapItem (Record) ├── Key : Text → OutSystems screen name (case-insensitive) └── Value : Text → JSON folder name under basePath
Step 1 — Create TranslationMap list:
// Assign → TranslationMapList ListAppend(TranslationMapList, {Key: "Dashboard", Value: "dashboard"}) ListAppend(TranslationMapList, {Key: "Profile", Value: "profile"}) ListAppend(TranslationMapList, {Key: "Settings", Value: "settings"})
Step 2 — Create Locales list:
// Assign → LocalesList ListAppend(LocalesList, "en-US") ListAppend(LocalesList, "ar-SA")
Step 3 — Call InitTranslation:
InitTranslation( BasePath: "/MyApp/locales/", TranslationMap: TranslationMapList, ResourceVersion: "v1.5", (optional) Locales: LocalesList )
InitTranslation is async — it starts fetching but doesn't block the app. If a screen calls LoadScreenResource before init completes, the library auto-queues and executes after init resolves. No error, no timing issue.
Where: Each Screen → OnReady event
ScreenName
IsLoaded
True
False
// Screen OnReady — that's all. No revision variable to increment. LoadScreenResource(ScreenName: "Dashboard")
Do not block render on it. The screen paints immediately with each Translate call's DefaultValue, then updates automatically the instant the JSON arrives.
DefaultValue
Where: Any Expression widget — set Function = Yes
Key
"dashboard.title"
Args
Translate("dashboard.title", "Dashboard", "")
No ghost dependency needed. Because Translate is an Expression (Function), OutSystems re-evaluates it on every render, and the library triggers a render when translations load or the locale changes. Do not pass CurrentLocale or TranslationRevision as trailing parameters — that pattern is obsolete. Called before init → returns DefaultValue, never crashes.
CurrentLocale
JSON:
{ "welcome": "Hello, %{opt1}. You have %{opt2} notifications." }
Expression:
Translate("common.welcome", "Hello", "Gokula,5")
Output: "Hello, Gokula. You have 5 notifications."
"Hello, Gokula. You have 5 notifications."
"Gokula"
"Gokula,5"
"Gokula,5,New"
1. Current locale (e.g., ar-SA) → found? → return 2. Parent locale (e.g., ar) → found? → return 3. Default locale (e.g., en-US) → found? → return 4. Parent of default (e.g., en) → found? → return 5. DefaultValue parameter → return 6. Empty string "" → return
The library reuses the OutSystems locale publish/subscribe pipeline — the same mechanism the platform uses to re-render on a locale change. You do not manage a client variable.
When a lazy translation file finishes loading, the library publishes a re-render for the active locale. Every Translate expression re-evaluates and picks up the newly loaded text. The screen shows DefaultValue first, then swaps to the translation with no flicker — all non-blocking.
When you call SwitchLocale, the library sets the active locale, syncs the OutSystems locale (so native date/number formatting follows), and publishes a re-render. Again, no manual assignment.
Translate(Key, Default, Args, CurrentLocale, TranslationRevision)
Translate(Key, Default, Args)
Requirement: the value must be a Translate Expression (Function = Yes). Static text set directly on a widget will not update.
Where: Language switcher button/dropdown OnClick
Locale
"ar-SA"
1. SwitchLocale(Locale: "ar-SA") ← sets locale, syncs OS, applies dir/lang, re-renders 2. LoadScreenResource(ScreenName: ...) ← only if the target screen has modules not yet loaded
There is no Assign Client.CurrentLocale step and no revision increment. SwitchLocale handles direction and re-render for you.
Assign Client.CurrentLocale
Built-in RTL locales: ar-SA, ar-EG, he-IL, ur-PK, fa-IR (extend via SetConfig). The library takes ownership of the OutSystems RTL flag, so direction holds correctly across navigation — including for locales OutSystems does not ship natively (ur-PK, he-IL, fa-IR), which previously stayed left-to-right.
ar-SA
ar-EG
/* Use CSS logical properties or [dir="rtl"] selectors */ .content { margin-inline-start: 16px; padding-inline-end: 8px; } [dir="rtl"] .sidebar { right: 0; left: auto; }
Where: After SwitchLocale. Translates the OutSystems built-in validation messages (“Required field”, “Invalid email”, …).
TranslateValidationMessages(EspaceName: "MyApp")
The original (default-locale) messages are captured on first call and used as fallbacks, so switching back restores them correctly.
Where: Screen OnReady, for text placed directly in HTML (outside Translate expressions).
<span data-trans="dashboard.title" data-trans-default="Dashboard"></span> <span data-trans="common.greeting" data-trans-default="Hello" data-trans-args="Gokula,5"></span>
data-trans-default
data-trans-args
Screen OnReady: ObserveDOM() ← translates existing nodes, watches new ones Screen OnDestroy: DisconnectObserver() ← no argument — tears down the internal observer
The observer is tracked inside the library, so you do not store its handle — DisconnectObserver() with no argument tears down whatever ObserveDOM() last created. Calling ObserveDOM() again auto-disconnects the previous one. These nodes also re-translate automatically when a lazy fetch lands.
DisconnectObserver()
ObserveDOM()
SetConfig overrides configuration at runtime (empty value = keep current). TranslationMap and RtlLocales merge into the existing values; all other keys replace.
RtlLocales
CacheStrategy
"force-cache"
"no-cache"
"reload"
"default"
EnableFallback
SetConfig( BasePath: "", ← unchanged ResourceVersion: "v2.0", ← updated CacheStrategy: "no-cache", ← updated EnableFallback: True ← unchanged )
GetConfig returns a snapshot of the current values (BasePath, ResourceVersion, Locales, DefaultLocale, CacheStrategy, EnableFallback) for debugging.
Translate("common.save", "Save", "")
// JSON: "Last updated: %{opt1}" Translate("dashboard.lastUpdated", "Last updated", FormatDateTime(Now(), "dd MMM yyyy"))
// JSON: "%{opt1} — %{opt2} on %{opt3}" Translate("dashboard.list.item", "", "John,Transfer,15 Jul")
Translate("premium.feature", If(IsPremiumUser, "Unlock", "Upgrade to access"), "")
// In a Table Records expression: Translate("status." + GetRecordList.Current.Status, GetRecordList.Current.Status, "")
JSON structure:
{ "status": { "active": "نشط", "inactive": "غير نشط", "pending": "قيد الانتظار" } }
{ "dashboard.title": "Dashboard", "dashboard.subtitle": "Your overview", "dashboard.summary.total": "Total" }
{ "dashboard": { "title": "Dashboard", "subtitle": "Your overview", "summary": { "total": "Total" } } }
{ "dashboard": { "title": "Dashboard", "summary.total": "Total" } }
Error: [I18n_Lib] Invalid JSON structure at key: "dashboard.summary.total". Use either pure flat or pure nested format.
[I18n_Lib] Invalid JSON structure at key: "dashboard.summary.total". Use either pure flat or pure nested format.
dir
lang
<html>
rtlLocales
[dir="rtl"]
This is normal. If you have 5 locales configured but only 2 JSON files for a screen, the missing ones 404 silently and the fallback locale is used instead.
Cause: the OutSystems locale service could not be resolved. Translations still load and render; the automatic re-render is disabled.Fix: ensure InitTranslation runs in Application OnReady.
[missing "en-US.some.key" translation]
Cause: the key doesn't exist in any loaded JSON file.Fix: check the key spelling, confirm the right module JSON is loaded for the screen, and add a DefaultValue to the Translate call as a safety net.
I18n_Lib — wrapper v1.5.1 · OutSystems 11+ Reactive Web. See CHANGELOG for version history.
/locales/ ├── default/ ← shared keys (nav, footer, common) │ ├── en-US.json │ └── ar-SA.json ├── dashboard/ ← screen-specific keys │ ├── en-US.json │ └── ar-SA.json ├── profile/ │ ├── en-US.json │ └── ar-SA.json └── settings/ ├── en-US.json └── ar-SA.json
Rule: The default/ folder is loaded automatically at app start. Screen folders are loaded on-demand.
"v1.3"
?t=v1.3
TranslationMapItem (Record) ├── Key : Text → OutSystems screen name (exact match) └── Value : Text → JSON folder name under basePath
// Assign → TranslationMapList ListAppend(TranslationMapList, {Key: "Dashboard", Value: "dashboard"}) ListAppend(TranslationMapList, {Key: "Profile", Value: "profile"}) ListAppend(TranslationMapList, {Key: "Settings", Value: "settings"}) ListAppend(TranslationMapList, {Key: "Transfers", Value: "transfers"})
InitTranslation( BasePath: "/MyApp/locales/", TranslationMap: TranslationMapList, ResourceVersion: "v1.3", (optional) Locales: LocalesList )
default/en-US.json
default/ar-SA.json
loadForScreen()
t()
// Screen OnReady: LoadScreenResource(ScreenName: "Dashboard") // Then assign: TranslationRevision = TranslationRevision + 1
If ScreenName doesn't exist in TranslationMap, no screen-specific JSON is loaded — but default/ translations are still available. This is not an error.
TranslatedText
Translate( "dashboard.title", "Dashboard", "" )
Important: Include CurrentLocale and TranslationRevision as trailing parameters. They aren't used by the function logic — they are ghost dependencies that force OutSystems to re-evaluate the expression when locale changes or translations load.
""
1. SwitchLocale(Locale: "ar-SA") 2. Assign → Client.CurrentLocale = "ar-SA" 3. LoadScreenResource(ScreenName: GetCurrentScreen())
Where: Anywhere at runtime (optional — for advanced use)
SetConfig( BasePath: "", ← unchanged ResourceVersion: "v2.0", ← updated Locales: "", ← unchanged DefaultLocale: "", ← unchanged CacheStrategy: "no-cache", ← updated EnableFallback: True ← unchanged )
Where: Debug screens, developer tools
GetConfig() // Use outputs for debug display or conditional logic
// Reusable across screens: LoadScreenResource(ScreenName: GetCurrentScreenName())
{ "dashboard.title": "Dashboard", "dashboard.subtitle": "Your overview", "dashboard.summary.total": "Total", "dashboard.alerts.empty": "No active alerts." }
{ "dashboard": { "title": "Dashboard", "subtitle": "Your overview", "summary": { "total": "Total" }, "alerts": { "empty": "No active alerts." } } }
Cause: LoadScreenResource called before InitTranslation resolved.Fix: Already handled in v1.3.0+ — the library auto-queues. If you still see this error, ensure InitTranslation is in Application OnReady (not Screen OnReady).
Check:
This is normal. If you have 5 locales configured but only 2 JSON files for a screen, the missing ones 404 silently. The fallback locale (en-US) is used instead.
en-US
Cause: Key doesn't exist in any loaded JSON file.Fix: