foam3

FOAM Messages: Beginner Guide

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.

Where translations live

Two kinds of files hold i18n data. Knowing them up front makes the examples below make sense:

Overview

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.

Defining Messages

Basic Static Messages

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.

Multi-Language Support

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 — as Locale rows in a locales.jrl (keyed Class.MESSAGE_NAME) or via the Translation Console (advanced guide).

Don’t set both message and messageMap on one entry. message only writes the current locale’s text into messageMap, so combining the two makes the outcome depend on declaration order. For multiple languages use messageMap alone, and always include an en fallback.

A messageMap declares the code defaults. At runtime, the application boot path can override these from localeDAO — see the advanced guide.

Inline Localized Labels

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 String property 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 property label is declared I18NString (foam3/src/foam/lang/types.js:1432), but that class adds no translation: it extends String, 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 a Locale row keyed Class.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 a messages entry and feed it to the label — via a labelFormatter (this.add(data.MY_MSG)) for a property, or a render()-time label: arg for an action. An entry from messages[] does not become a label: on its own.

Template Messages with Parameters

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:

  1. Explicitly provided values during rendering
  2. this[key] for instance properties or other class constants
  3. Dot notation for nested FObject properties.

There 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: an expression only 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();
    }
  }
]

Message Name and Class Constant Conflicts

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'
    }
  }
]

Sharing Messages Across a Class Hierarchy

(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. Locale rows and the Translation Console key a message by the class that declares it — Order.STATUS_CHANGED in the example above, never ExpressOrder.STATUS_CHANGED. Keep this in mind when moving a message up to a base class: any existing Locale rows keyed to the old subclass path stop matching and must be re-keyed (advanced guide).

Porting Hardcoded Strings

Step 1: Identify Hardcoded Strings

Look for string literals in:

Step 2: Define Messages

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.

Step 3: Use in Views and Methods

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]
    ]
  };
}

Table Cell Formatter Translation

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).

Property Label Formatter Gotcha

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.

Next Steps

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.