Languages
Two kinds of text reach a person. One is what the model writes: it answers in the language it’s spoken to, or in the one the person asked for. The other is the fixed texts that Mercury, its plugins and its channels write themselves, the same every time: the refusal someone the agent doesn’t know gets, the reply to an account-linking code, the line saying a tool is running. Those come from language files.
English, and the languages the app adds
Section titled “English, and the languages the app adds”Every fixed text exists in English, written by the package that shows it. An app adds a language with a file of its
own, one per language, in the locales/ folder. The file maps each text’s key to its translation:
{ "core.access.unknown": "Questo assistente parla solo con le persone che conosce. Chiedi l'accesso a un amministratore.", "core.link.done": "Fatto: questo account ora è {owner}."}A key starts with the package the text belongs to: core. is Mercury’s own texts, web-search. the web search
plugin’s, google-chat. and http. the channels’, and so on, each under its own name. A word in braces, like {owner},
is filled in when the text is shown: keep it as it is, wherever it fits the sentence.
locales/en.json works the same way over the English texts: a key written there replaces the package’s English, and
the rest stay as the packages wrote them.
The app’s own texts, such as what a formatter rule shows for an empty list, go
under app.: written in English in locales/en.json, they’re declared there, and every other language’s file
translates them like the packages’ keys. A new app with the Jira plugin already has one, app.jira.no-issues.
Which language a person gets
Section titled “Which language a person gets”- The language they asked for. Someone who tells the agent “talk to me in Italian” gets Italian from then on, in every conversation: the agent saves it as one of their settings, like the name they want to be called by, and its own answers follow it too. Only the languages the app has can be picked; an app with English alone offers no choice.
- Otherwise, the one their app asks for, when the channel says: a web UI sends its browser’s languages over the
HTTP channel (the
Accept-Languageheader browsers send), Google Chat the language of the person’s Chat client. A language the app doesn’t have is passed over. - Otherwise, the app’s default language (
defaultLocale). - Otherwise, English.
Someone the agent doesn’t know yet, such as a person it refuses, gets the language their app asks for, else the default.
A missing translation
Section titled “A missing translation”A key a language’s file lacks shows as the key itself, core.link.done, not as the English text: English there
would look intended, and the gap would go unnoticed. When the agent starts, the log names the keys each file is
missing, and the keys no package declares (a typo, or a text a package no longer has):
[i18n] it: 2 keys missing, shown as the key: core.link.invalid, core.link.doneThat’s also the quickest way to write a new file: start it empty, {}, start the agent, and translate what the log
lists.
Setting it up
Section titled “Setting it up”The languages go in the app’s mercury.config.ts, each file imported:
import en from "./locales/en.json";import it from "./locales/it.json";
export default defineMercuryConfig({ // … i18n: { defaultLocale: "it", locales: { en, it } },});A language is named by its tag, it or pt-br. defaultLocale must be en or one of the languages listed, or the
agent doesn’t start. An app made with mfw create already has a locales/en.json, listed in the config: empty, or with the starting
formatter rules’ own texts (app.jira.no-issues with Jira).
Every fixed text goes through the language files: Mercury’s own, the plugins’ and the channels’ (a Google Chat card’s
title, the HTTP channel’s login page, google-chat. and http. keys). What the model writes isn’t one of them: it
answers in the person’s language on its own.
The time where the person is
Section titled “The time where the person is”A model doesn’t know what day it is. Mercury tells it: every message reaches the model after a line saying when and
where it was written, [Wednesday 2026-10-21 14:32 Europe/Rome], so “today”, “next Monday” or “is it still open?”
mean something. The line goes only to the model: the conversation’s archive, and what a web UI shows of it, keep the
message as it was written.
The time is the person’s, in their time zone, named as the official list of time zones (the IANA list) names it:
Europe/Rome, America/New_York. A fixed offset like +02:00 isn’t one, because it’s wrong for half the year. The
zone is picked like the language:
-
The one the person saved. Someone who tells the agent “I’m in Tokyo” gets Tokyo’s time from then on, in every conversation: the agent saves it as one of their settings. A web UI can offer it in its own settings too.
-
Otherwise, the one their app says, when the channel passes it on: a web UI sends its browser’s over the HTTP channel (the
X-Time-Zoneheader), Google Chat the one of the person’s Chat client, when Google includes it. -
Otherwise, the app’s own,
timeZonenext to the languages in the config:mercury.config.ts i18n: { defaultLocale: "it", locales: { en, it }, timeZone: "Europe/Rome" }, -
Otherwise, the one of the machine the agent runs on, often UTC inside a container.
A timeZone that isn’t a zone’s name stops the agent at startup.