Wrapper: v1.5.3 · Platform: OutSystems 11+ Reactive Web
Internationalization for OutSystems Reactive Web through Client Actions. Noserver logic — translations are JSON files on the server or a CDN, loaded lazilyper screen.
i18n.config.js
data-trans
Each Client Action is a thin JS node wrapping the I18n_Lib.i18n module. Thecanonical JS for each is in source/I18nWrapper-Development-Chat-Export.md.
I18n_Lib.i18n
source/I18nWrapper-Development-Chat-Export.md
Deploy a single i18n.config.js at your app root (as a module Resource, served at/{YourModule}/i18n.config.js). The library fetches it on load, constructs the engine,seeds the persisted locale, and initializes itself — no InitTranslation inApplication OnReady.
/{YourModule}/i18n.config.js
InitTranslation
// i18n.config.js → served at /YourApp/i18n.config.js return { basePath: "/YourApp/locales/", // required · must end with / locales: ["en-US", "ar-SA", "ur-PK", "fr-FR", "zh-CN"], // defaultLocale: "en-US", // resourceVer: "v1", // cache-bust ?t= per deploy // cacheStrategy: "force-cache", rtlLocales: ["ar-SA", "ar-EG", "he-IL", "ur-PK", "fa-IR"], translationMap: { "home": "home", "dashboard": "dashboard" } // lowercase screen → folder };
The file is fetched with cache: "no-cache", so editing it takes effect on the nextreload without a cache-bust token. A bare return { … } (above) or avar I18nConfig = { … } declaration both work. The active locale is seeded from theOutSystems-persisted $…$CurrentLocale before init, so a reload renders in the lastchosen language immediately.
cache: "no-cache"
return { … }
var I18nConfig = { … }
$…$CurrentLocale
Still supported if you want explicit control (or need config values computed atruntime). Skip it entirely if i18n.config.js is deployed — running both justinitializes twice.
BasePath
/locales/
/
Locales
["en-US"]
DefaultLocale
"en-US"
TranslationMap
ResourceVersion
?t=
JS node (parses OS Lists → config, then init):
init
var localesList = [], rtlList = [], screenObjs = {}; try { localesList = JSON.parse($parameters.Locales); } catch (e) {} try { rtlList = JSON.parse($parameters.RtlLocales); } catch (e) {} try { JSON.parse($parameters.TranslationMap).forEach(function (item) { if (item.key && item.key.trim() !== "") screenObjs[item.key.trim().toLowerCase()] = item.value.trim().toLowerCase(); }); } catch (e) {} var CSI18n = require("I18n_Lib.i18n"); var i18n = new TinyI18n(); // TinyI18n is a global i18n.locale = $parameters.CurrentLocale || "en-US"; CSI18n.init(i18n, { basePath: $parameters.ResourceLocation, resourceVer: $parameters.ResourceVersion || "", locales: localesList.length ? localesList : ["en-US"], defaultLocale: $parameters.DefaultLocale || "en-US", enableFallback: true, cacheStrategy: $parameters.CacheStrategy || "force-cache", rtlLocales: rtlList.length ? rtlList : ["ar-SA","ar-EG","he-IL","ur-PK","fa-IR"], translationMap: screenObjs }).then($resolve).catch($reject);
Timing is handled for you: if a Screen OnReady fires before init finishes (cachehit), LoadScreenResource / SwitchLocale / TranslateValidationMessages queuesilently and continue once init resolves.
LoadScreenResource
SwitchLocale
TranslateValidationMessages
ScreenName
IsLoaded
True
LoadScreenResource(ScreenName: "Dashboard") // or "dashboard,accounts,orders"
Do not block render on it. Paint immediately; the screen shows DefaultValuetext, then refreshes automatically when the JSON lands (pub/sub re-render). Partiallocale coverage is fine — missing files 404 silently and fall back.
DefaultValue
| Key | Text | ✅ | Dot key: "dashboard.title" || DefaultValue | Text | — | Fallback if key missing || Args | Text | — | Comma-separated → %{opt1}, %{opt2}, … |
Key
"dashboard.title"
Args
%{opt1}
%{opt2}
GetTranslation("common.welcome", "Welcome", UserName) // → "مرحباً، Ahmed" GetTranslation("order.status", "Order", OrderId + "," + Status) // %{opt1},%{opt2}
JS:
var CSI18n = require("I18n_Lib.i18n"); $parameters.TranslatedText = CSI18n.t($parameters.Key, $parameters.DefaultValue, $parameters.Args);
No ghost dependency needed. Because GetTranslation is an Expression, OutSystemsre-evaluates it on every render, and the library forces a render (via the OS flush)when translations load or the locale changes. Called before init → returnsDefaultValue, never crashes.
GetTranslation
| Locale | Text | ✅ | Target locale, e.g. "ar-SA" |
Locale
"ar-SA"
var CSI18n = require("I18n_Lib.i18n"); if ($parameters.Locale && $parameters.Locale.trim() !== "") { var locale = $parameters.Locale.trim(); CSI18n.setLocale(locale); // sets active locale, syncs OS locale, publishes → re-render document.documentElement.setAttribute("dir", CSI18n.getDirection()); document.documentElement.setAttribute("lang", locale); document.body.classList.toggle("is-rtl", CSI18n.isRTL()); }
Then, if the target screen has locale-specific modules not yet loaded:
LoadScreenResource(ScreenName: "Dashboard")
That's it — no RevisionCounter reassignment, no Assign Client.CurrentLocale.setLocale publishes on the OS LocaleService, which re-renders everyGetTranslation Expression.
RevisionCounter
Client.CurrentLocale
setLocale
LocaleService
SetConfig overrides config at runtime (empty value = keep current).translationMap and rtlLocales merge; other keys replace.
SetConfig
translationMap
rtlLocales
SetConfig(BasePath: "/Staging/locales/", ResourceVersion: "v2") SetConfig(CacheStrategy: "no-cache") // dev: always revalidate
GetConfig returns a snapshot (BasePath, DefaultLocale, Locales, CacheStrategy, …)for debugging.
GetConfig
If(IsRTL(), "layout-rtl", "layout-ltr") // container CSS class GetDirection() // "rtl" or "ltr"
Built-in RTL locales: ar-SA, ar-EG, he-IL, ur-PK, fa-IR (extend viaSetConfig(RtlLocales: …) — merges). init() patches the OS LocaleService so RTLholds across navigation even for locales OS doesn't ship natively.
ar-SA
ar-EG
he-IL
ur-PK
fa-IR
SetConfig(RtlLocales: …)
init()
.layout-rtl { direction: rtl; text-align: right; } .content { margin-inline-start: 16px; padding-inline-end: 8px; } /* auto-flips */
For text set directly in HTML (not through GetTranslation).
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 handle is tracked inside the library, so you don't store it in avariable — DisconnectObserver() with no argument tears down whatever ObserveDOM()last created. Calling ObserveDOM() again auto-disconnects the previous one (nostacking). data-trans nodes also re-translate automatically when a lazy fetch lands(they subscribe to the load event internally).
DisconnectObserver()
ObserveDOM()
Translates OutSystems' built-in validation messages ("Required field", "Invalidemail", …). Call after SwitchLocale.
TranslateValidationMessages(EspaceName: "MyApp")
Originals (default locale) are captured on first call and used as fallbacks, soswitching back restores them correctly. Kept intentionally — removing it wouldlose OS's built-in validation translations.
basePath is mandatory
Invalid JSON structure at key…
document.documentElement.dir
.is-rtl
{ "a.b": "v" }
{ "a": { "b": "v" } }
en-US
ar-SA → ar → en-US → en → DefaultValue
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
/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/
Translate
ObserveDOM
DisconnectObserver
Where: Application → OnReady (runs once on app load)
"v1.5"
?t=v1.5
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
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.
Where: Any Expression widget — set Function = Yes
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
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.
/* 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”, …).
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>
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.
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>
[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.
Cause: Key doesn't exist in any loaded JSON file.Fix: