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.
Writing translations
Section titled “Writing 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.
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.
Using translations in a process
Section titled “Using translations in a process”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"));}def main(): logger.info(fcode.i18n("greetings.hello", {"name": fcode.context.parameters.name})) logger.info(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.
Using translations in a form
Section titled “Using translations in a form”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.
Choosing the locale
Section titled “Choosing the locale”Which locale an execution or render uses depends on how it was triggered:
| Trigger | How to choose |
|---|---|
| Form | Fcode-Locale header or ?locale= query parameter |
| Webhook | Fcode-Locale header or ?locale= query parameter |
| Run now | Locale selector in the run dialog |
| Schedule | Locale selector when creating or editing the schedule |
| Rerun | Reuses 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.
curl "https://code.factorialhr.com/platform/api/my-team/webhooks/my-process?locale=pt-BR"# orcurl -H "Fcode-Locale: pt-BR" "https://code.factorialhr.com/platform/api/my-team/webhooks/my-process"The primary locale
Section titled “The primary locale”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.
Inheriting locales
Section titled “Inheriting locales”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.
Versioning translations
Section titled “Versioning translations”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.
Pinning a version in the helper
Section titled “Pinning a version in the helper”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" });fcode.i18n("legal.terms", None, {"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.
What a pinned call resolves
Section titled “What a pinned call resolves”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.
Freezing a release
Section titled “Freezing a release”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.
Syncing with the CLI
Section titled “Syncing with the CLI”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:
fcode i18n:pull # fetch every locale, inherited ones includedfcode i18n:status # what changed locally vs the cloudfcode i18n:add pt-BR # track a new local filefcode i18n:push # create or update in the cloudfcode i18n:remove pt-BR # stop tracking it locallyfcode i18n:reset # discard local changesfcode 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:
fcode run my-process --locale pt-BRManaging locales from the API
Section titled “Managing locales from the API”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/localesGET /{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");from fcode_sdk import FcodeI18n
i18n = FcodeI18n()
i18n.list()i18n.set("pt-BR", 'greetings:\n hello: "Olá %{name}"\n')i18n.delete("pt-BR")