Skip to content
Local environment Preproduction — not production data

Internationalization

A workspace keeps one locale per language it speaks. Each locale is a YAML file of translation keys, and fcode.i18n("key") resolves a key against the locale the current execution or form render is using.

Locales live under Internationalization in the sidebar. Adding one asks only for its identifier — en, es, pt-BR, whatever you already call that language — and drops you into the editor to write the translations.

A locale is a YAML mapping of keys to text. Nesting is a convenience for whoever writes the file, not a data model: nested keys are addressed with dots, so these two are the same locale.

i18n/en.yaml
greetings:
hello: "Hi %{name}"
farewell: "See you"
requests:
approved: "Your request was approved"
# identical to the file above
"greetings.hello": "Hi %{name}"
"greetings.farewell": "See you"
"requests.approved": "Your request was approved"

That means a workspace can override a single key from a parent without repeating its structure.

%{name} placeholders are filled in with the arguments you pass to the helper.

const greeting = fcode.i18n("greetings.hello", { name: "Ada" });
// "Hi Ada" when running in `en`
async function main() {
fcode.log(fcode.i18n("greetings.hello", { name: fcode.context.parameters.name }));
fcode.log(fcode.i18n("greetings.farewell"));
}

The helper never fails. A key with no translation anywhere resolves to the key itself, so a missing translation shows up as greetings.hello in your output instead of breaking the run. A placeholder you pass no argument for is left exactly as written.

A third argument holds options — today only version, which reads the key from a published version of the locale. See versioning translations.

Modules can call fcode.i18n too — a process that only reaches it through one of its modules still gets its translations.

Form schemas are rendered by the browser, so there is no runtime to resolve tokens in. Write the same call directly in the schema and the platform substitutes it before serving the schema:

{
"type": "object",
"properties": {
"reason": {
"type": "string",
"title": "fcode.i18n(\"form.reason.label\")",
"description": "fcode.i18n(\"form.reason.help\", { max: \"500\" })"
}
}
}

The browser receives a schema already written in one language; translations never reach the client.

Which locale an execution or render uses depends on how it was triggered:

TriggerHow to choose
FormFcode-Locale header or ?locale= query parameter
WebhookFcode-Locale header or ?locale= query parameter
Run nowLocale selector in the run dialog
ScheduleLocale selector when creating or editing the schedule
RerunReuses the original execution’s locale

When nothing names a locale, the workspace’s primary locale is used — and when the workspace has not chosen one, its first locale (alphabetically) is. So translations work as soon as you create a locale, without having to configure anything. The query parameter wins over the header, so a caller that can only set a URL can override a header a proxy added.

Terminal window
curl "https://code.factorialhr.com/platform/api/my-team/webhooks/my-process?locale=pt-BR"
# or
curl -H "Fcode-Locale: pt-BR" "https://code.factorialhr.com/platform/api/my-team/webhooks/my-process"

Set the workspace’s main language under Settings → Details → Primary locale. It does two things:

  • it is the locale used when a caller names none;
  • it is the fallback for keys the chosen locale has not translated yet.

Leaving it unset is fine: the workspace’s first locale, alphabetically, is used for both. Choosing one explicitly matters once you have more than one locale and the alphabetically-first is not the language you want to default to.

So a partially translated es still resolves every key: what es translates comes from es, and the rest comes from the primary locale. Only a key missing from both resolves to its own name.

Locales inherit from parent workspaces like modules and team variables do. A locale a parent defines is listed here as read-only, with the parent’s name next to it.

Writing a locale whose identifier a parent owns creates an override in this workspace, and the override is layered key by key — not file by file. Keys the override does not mention keep resolving to the parent’s text, so a child workspace can change one greeting without copying the parent’s whole file.

Because both files stay live, both stay in the list: an overridden locale appears twice, once as this workspace’s file and once as the parent’s, and clicking either opens that file. Deleting the override leaves the parent’s text resolving on its own again.

The CLI keeps both files too. Your own locale is i18n/<locale>.yaml, and what you inherit is i18n/<locale>.inherited.yaml, read-only and kept out of git — one file, holding the merge of every parent workspace that defines the locale. A local run layers them exactly as the cloud does.

Locales are versioned like processes and modules are. Publishing a version snapshots the YAML file under a tag, and a snapshot is immutable: the same tag can never be published twice, so a call pinned to it always resolves the same text.

Publish one from the version selector in the locale editor, or from View all versions, which opens the locale’s versions and aliases page. Both are the same surface a module has.

Pass the version in the helper’s third argument, the options object:

fcode.i18n("legal.terms", null, { version: "v1.0.0" });
fcode.i18n("greetings.hello", { name: "Ada" }, { version: "production" });

The same options work in a form schema, where they are resolved at render time like the rest of the token:

{ "title": "fcode.i18n(\"form.reason.label\", null, { version: \"v1.0.0\" })" }

A version name is a tag or an alias. Aliases are per locale — production on en and production on es are two different aliases — and repointing one changes what every call pinned to that name resolves, without touching any code.

Resolution is per file. Each locale file in the inheritance chain answers with its snapshot at that tag, or with its current content when it has no snapshot at that tag, and the result is layered key by key as usual. So:

  • a locale created after the version was cut still contributes its keys;
  • a parent workspace that never published the tag still contributes its text;
  • a mistyped tag resolves everything against the current files, rather than blanking your output.

Deleting a version sends the calls pinned to it back to the current files. Deleting a locale deletes its versions with it.

Creating a workspace version publishes a version of every locale and rewrites the snapshots it publishes so that bare fcode.i18n calls pin that tag — in process code, in module code and in form schemas. A release therefore ships with its translations frozen, exactly as it ships with its modules frozen, and fixing a released typo means publishing again.

Your working copy is never rewritten: bare calls there keep resolving against the current files.

Locales are a CLI resource like any other. They live in i18n/<locale>.yaml, with what the workspace inherits alongside them in i18n/<locale>.inherited.yaml:

Terminal window
fcode i18n:pull # fetch every locale, inherited ones included
fcode i18n:status # what changed locally vs the cloud
fcode i18n:add pt-BR # track a new local file
fcode i18n:push # create or update in the cloud
fcode i18n:remove pt-BR # stop tracking it locally
fcode i18n:reset # discard local changes

fcode pull and fcode push include locales, so the usual whole-workspace commands already cover them. The primary locale lives in team.json as primaryLocale and syncs with fcode team:push.

Local runs resolve fcode.i18n against the same files, so fcode run my-process behaves like the cloud does. Pass --locale to run in a specific one:

Terminal window
fcode run my-process --locale pt-BR

Locales are part of the public API and both SDKs. They are addressed by their identifier, and PUT creates or replaces, so a sync does not have to know whether the workspace already had the locale.

GET /{team}/rest/locales
GET /{team}/rest/locales/{locale}
PUT /{team}/rest/locales/{locale}
DELETE /{team}/rest/locales/{locale}
import { FcodeI18n } from "@factorialco/fcode-sdk";
const i18n = new FcodeI18n();
await i18n.list();
await i18n.set("pt-BR", 'greetings:\n hello: "Olá %{name}"\n');
await i18n.delete("pt-BR");