This is the getting-started guide to defining translatable strings inside FOAM models. For the runtime translation system — Locale rows, localeDAO, the language picker, menu/DAO-title keys, the Translation Console, extraction scripts, and lifecycle diagrams — see FOAM i18n — Advanced & Runtime.
Two kinds of files hold i18n data. Knowing them up front makes the examples below make sense:
*.js) — messages: entries and inline label: { en, fr } maps declare a string’s default text per language, right next to the model. This is what this guide covers.locales.jrl journals — rows of foam.i18n.Locale that supply or override translations at runtime, keyed by a source string. Framework defaults live in foam3/src/foam/i18n/{en_us,fr_fr,pt_pt}/locales.jrl; app-specific rows go in journals/locales.jrl. The languages offered in the picker are seeded in foam3/src/languages.jrl. All of these are covered in the advanced guide.FOAM’s messages: feature provides a built-in internationalization (i18n) system for defining translatable strings within models. Messages are defined as axioms that install properties on classes, resolving to localized strings based on the current locale.
Add a messages array to your model definition with message objects:
foam.CLASS({
package: 'foam.core.auth',
name: 'User',
messages: [
{ name: 'USERNAME_REQUIRED', message: 'Username required' },
{ name: 'INVALID_FIRST_NAME', message: 'Invalid characters in first name: ' },
{ name: 'RESET_PASSWORD_SENT', message: "Password reset request sent." }
]
});
This installs properties like USERNAME_REQUIRED on the class that return the message string.
Use messageMap to provide translations for different locales:
messages: [
{
name: 'MY_STRING',
messageMap: {
en: 'English',
fr: 'Francais',
}
}
]
FOAM resolves messages by checking messageMap[foam.locale], then messageMap[foam.language], then messageMap.en.
You don’t have to translate every string into every language. Declare only the languages you’ve done — English alone is fine. Any locale with no entry falls back to
messageMap.en(foam3/src/foam/i18n/Messages.js:96), so untranslated strings show English rather than break. Add the rest later at runtime, with no code change — asLocalerows in alocales.jrl(keyedClass.MESSAGE_NAME) or via the Translation Console (advanced guide).
Don’t set both
messageandmessageMapon one entry.messageonly writes the current locale’s text intomessageMap, so combining the two makes the outcome depend on declaration order. For multiple languages usemessageMapalone, and always include anenfallback.
A
messageMapdeclares the code defaults. At runtime, the application boot path can override these fromlocaleDAO— see the advanced guide.
Any String-valued attribute on a model — a property label or placeholder, or an action label/ariaLabel — accepts a locale map directly, with no separate messages entry:
properties: [
{
class: 'String',
name: 'defaultText',
label: { en: 'Default / English', fr: 'Par défaut / anglais' }
}
],
actions: [
{
name: 'save',
label: { en: 'Save for selected locale', fr: 'Enregistrer pour la locale sélectionnée' }
}
]
FOAM picks the value for foam.locale, then the language prefix (fr for fr-CA), then falls back to en (foam3/src/foam/lang/types.js:47-62). This is the shortest way to localize a single label.
Caveat — it’s a plain default, coerced once. The
Stringproperty adapt picks the current locale’s string when the value is set and stores that plain string (foam3/src/foam/lang/types.js:47-62). A propertylabelis declaredI18NString(foam3/src/foam/lang/types.js:1432), but that class adds no translation: it extendsString, inherits this adapt, and only reports label reads to the Translation Console while it is open (foam3/src/foam/lang/types.js:113-136). That covers the common cases: switching language reloads the page (foam3/src/foam/core/auth/LanguageChoiceView.js:81) and re-picks the locale, and the text can still be overridden at runtime by aLocalerow keyedClass.PROPERTY.label— no message needed (advanced guide). What an inline map can’t do is use${}templates or expose the string as a reusable constant. For those, define amessagesentry and feed it to the label — via alabelFormatter(this.add(data.MY_MSG)) for a property, or a render()-timelabel:arg for an action. An entry frommessages[]does not become alabel:on its own.
Enable the template flag to use ${} placeholders for dynamic content:
messages: [
{
name: 'MSG_CONSTANT1',
message: 'Message text with one ${templateKey1} or more ${templateKey2} template keys',
template: true
},
{
name: 'MSG_CONSTANT2',
message: '${this.MSG_CONSTANT1} or ${this.propertyName}',
template: true
}
]
Template keys resolve from:
this[key] for instance properties or other class constantsThere are two ways to fill in the ${} placeholders.
Way 1 — pass the values in. The placeholder name matches a key you pass:
messages: [
{ name: 'GREETING', message: 'Hello, ${name}!', template: true }
]
// Call it with that key:
this.add(this.GREETING({ name: 'Sam' })); // "Hello, Sam!"
Way 2 — read the values from the object itself. Write ${this.propertyName} and call the message with no arguments. FOAM reads the property straight off the object:
properties: [
{ name: 'firstName' }
],
messages: [
{ name: 'GREETING', message: 'Hello, ${this.firstName}!', template: true }
]
// No arguments needed — it reads this.firstName:
this.add(this.GREETING()); // "Hello, Sam!" when firstName is "Sam"
Heads up when using Way 2 inside an
expression: anexpressiononly re-runs when a property in its argument list changes. So list every property your message reads — otherwise the text won’t update when that property changes.messages: [ { name: 'FULL_NAME', message: 'Hello, ${this.firstName} ${this.lastName}!', template: true } ], properties: [ { name: 'greetingMsg', // BUG: only firstName is listed. Change lastName and this does NOT // re-run — the greeting keeps the old last name. // FIX: list both → expression: function(firstName, lastName) expression: function(firstName) { return this.FULL_NAME(); } } ]
Messages install constants on the owning class. Properties, actions, listeners, methods, inner classes, and existing messages also reserve class constant names. Do not add a message whose constantized name collides with any existing slot on the same model.
For example, a rememberMe property reserves REMEMBER_ME, so a message for the text “Remember me” must use a safe alternate name:
properties: [
{ class: 'Boolean', name: 'rememberMe' }
],
messages: [
{
name: 'REMEMBER_ME_MESSAGE', // or 'REMEMBER_ME_MSG'
messageMap: {
en: 'Remember me',
fr: 'Se souvenir de moi'
}
}
]
(Optional read : for when the volume of messages increase and they start to look similar across different classes)
Messages inherit. A message defined on a base class is usable from every subclass — as a class constant (SubClass.MY_MSG) and on instances (this.MY_MSG, X.data.MY_MSG) — with no re-declaration. Template messages inherit the same way, and their ${} keys still resolve against the object the message is called on, not the base class.
So when several sibling models repeat the same strings, define them once on the shared parent and delete the copies:
// Parent — defines once
foam.CLASS({
name: 'Order',
messages: [
{
name: 'STATUS_CHANGED',
messageMap: {
en: 'Order status changed to ${newStatus}',
fr: 'Statut de la commande passé à ${newStatus}'
},
template: true
}
]
});
// Subclass — no messages block needed
foam.CLASS({
name: 'ExpressOrder',
extends: 'Order',
actions: [
{
name: 'ship',
code: function(X) {
// X.data is an ExpressOrder — the parent's
// message resolves through inheritance:
X.notify(X.data.STATUS_CHANGED({ newStatus: 'SHIPPED' }), '', this.LogLevel.INFO, true);
}
}
]
});
A subclass may also redeclare a message with the same name to override the parent’s wording for that subclass only.
Runtime-override keys follow the defining class.
Localerows and the Translation Console key a message by the class that declares it —Order.STATUS_CHANGEDin the example above, neverExpressOrder.STATUS_CHANGED. Keep this in mind when moving a message up to a base class: any existingLocalerows keyed to the old subclass path stop matching and must be re-keyed (advanced guide).
Look for string literals in:
Add messages to your model for each hardcoded string:
// Before
properties: [
{
name: 'enabled',
tableCellFormatter: function(value, obj) {
this.add(value ? 'Y' : 'N');
}
}
]
// After
messages: [
{ name: 'ENABLED_YES', message: 'Y' },
{ name: 'ENABLED_NO', message: 'N' }
],
properties: [
{
name: 'enabled',
tableCellFormatter: function(value, obj) {
this.add(value ? obj.ENABLED_YES : obj.ENABLED_NO);
}
}
]
Real example from Script.js.
Replace string literals with message references:
// Before
choices: [
[4, 'Low'],
[5, 'Medium'],
[6, 'High']
]
// After
messages: [
{ name: 'PRIORITY_LOW', message: 'Low' },
{ name: 'PRIORITY_MEDIUM', message: 'Medium' },
{ name: 'PRIORITY_HIGH', message: 'High' }
],
// In property view
view: function(_, X) {
return {
class: 'foam.u2.view.ChoiceView',
choices: [
[4, X.data.PRIORITY_LOW],
[5, X.data.PRIORITY_MEDIUM],
[6, X.data.PRIORITY_HIGH]
]
};
}
A function-form tableCellFormatter runs with this bound to the cell element, not the model: FOAM calls it as f.call(cellElement, value, obj, axiom) (foam3/src/foam/u2/view/FnFormatter.js:18-20). Every element imports translationService? (foam3/src/foam/u2/Element2.js:473), so a formatter can translate through the element itself, and a model-level imports: ['translationService?'] added for the formatter’s sake is dead code.
A custom formatter replaces the default view, so it must translate what the default view would have translated, for example a unit label:
tableCellFormatter: function(value, obj, axiom) {
this.add(value, ' ').translate('foam.units.' + axiom.units, axiom.units);
}
translate(source, opt_default) looks the key up and adds the text, falling back to the default (foam3/src/foam/u2/Element2.js:1301-1320). Always pass the default text when calling getTranslation directly: its third argument is what the Translation Console records as the source string, and a lookup with no default is not recorded at all (foam3/src/foam/i18n/TranslationConsole.js:571-572).
PropertyBorder calls prop.labelFormatter on the label element and passes the rendered object as the first argument. That means this is the label Element, while data is the object whose property is being rendered.
Correct:
labelFormatter: function(data) {
this.add(data.PASSWORD_MESSAGE);
}
Incorrect:
labelFormatter: function(data) {
this.add(this.PASSWORD_MESSAGE);
}
Use this.add(...) because this is the element receiving the label text. Read messages and object state from data.
Once you are comfortable with messages: and inline labels, the advanced guide covers the runtime side: how Locale rows in localeDAO override defaults at boot, how the language picker is wired, source-key conventions for menus/actions/DAO titles, the automated extraction scripts, and the Translation Console.