Frequently asked questions
How do I handle plurals?
There is no plural API — no _one / _other suffix, no ICU syntax, no count option.
Each form is a full sentence you write yourself, and each sentence is its own key. You pick
the branch in JavaScript; the model renders each sentence in the target language's own
grammar.
import { I18nKeylessText } from "i18n-keyless-react";
function CartLine({ count }: { count: number }) {
if (count === 0) {
return <I18nKeylessText>Votre panier est vide</I18nKeylessText>;
}
if (count === 1) {
return <I18nKeylessText>1 article dans votre panier</I18nKeylessText>;
}
return (
<I18nKeylessText replace={{ "{count}": String(count) }}>
{"{count} articles dans votre panier"}
</I18nKeylessText>
);
}
The number itself never goes through the API: replace is applied to the translated
sentence, so {count} articles dans votre panier is one key whatever the number is. See
Replace values.
One limit, stated plainly: a language with more than two plural forms (Russian, Polish,
Arabic) gets one translation for {count} articles, not one per form. Where that
matters, override the translation with the wording you want.
How do I handle genders?
The same way: one sentence per case, and context to tell the model who speaks and who is
addressed. context is a free string, per key.
import { I18nKeylessText } from "i18n-keyless-react";
function SignedUp({ isFemale }: { isFemale: boolean }) {
return isFemale ? (
<I18nKeylessText context="said to a woman">Vous êtes inscrite</I18nKeylessText>
) : (
<I18nKeylessText context="said to a man">Vous êtes inscrit</I18nKeylessText>
);
}
context is part of the key's identity — a project keys its rows on
(source text, context, namespace). The same sentence with two contexts is two rows, so the
two forms never collide. More in Add context.
How do I format dates, numbers and currencies?
i18n-keyless translates strings; it never formats values. Use the platform's Intl with
the current language, which useCurrentLanguage() returns.
import { I18nKeylessText, useCurrentLanguage } from "i18n-keyless-react";
function Invoice({ amount, dueAt }: { amount: number; dueAt: Date }) {
const lang = useCurrentLanguage();
const price = new Intl.NumberFormat(lang ?? "en", {
style: "currency",
currency: "EUR",
}).format(amount);
const date = new Intl.DateTimeFormat(lang ?? "en", { dateStyle: "long" }).format(dueAt);
return (
<I18nKeylessText replace={{ "{price}": price, "{date}": date }}>
{"{price} à payer avant le {date}"}
</I18nKeylessText>
);
}
Put the formatted value back into the sentence with replace: the sentence stays one key,
and the number keeps the reader's own separators. In Node.js you already hold the reader's
language — you pass it to every call — so give that same code to Intl.
Self-hosted or subscription: which one?
They run the very same software. What differs is who operates it.
| Hosted | Self-hosted | |
|---|---|---|
| Price | Monthly subscription, per project | Free to install, one project; €30 once for unlimited projects |
| Machine, database, AI key | Ours | Yours |
| Updates | Automatic | You pull the image |
| Your strings | On our servers | On your server, and nowhere else |
| Licence | — | Checked offline, forever, no call home |
The rule: take the subscription unless you want to run a server. Take the licence when you already have one, when your data may not leave your infrastructure, or when you want your own AI provider and your own AI bill. €30 once, for life.
Install instructions: Self-host it.
Can I customise the translation prompt?
No — the prompt is the product's, and it is the same on every instance. Three things shape the output instead:
context, per key. Free text, sent with the string, and the only field of yours that reaches the model. It is where "this is a back button", "said to a woman" or "keep the brand name in English" belongs. See Add context.- The project's languages. The primary language and the supported list. Regional codes
are honoured:
ptandpt-BR,esandes-MX,enanden-GB,zh-Hansandzh-Hantare asked for by name. - The last word: yours. Any translation can be replaced by hand in the dashboard, or
from your code with
forceTemporary. See Force a temporary translation.
There is no project-level prompt, glossary or tone setting. A project holds a name, its
languages, its keys and its billing link — nothing else travels to the model. If your whole
project needs the same instruction, put it in the context of the keys that need it.
How do I fix a typo?
Two different cases.
A typo in your source text. The source text is the key, so correcting it is a new key: a new row and a fresh translation. Nothing breaks in production — the old string simply stops being asked for, and the cleaning is automatic: the dashboard lists the rows your apps used in the last 30 days, so a string nobody asks for any more leaves the Translations screen, the count and the JSON export on its own. Want it gone today? The per-row menu deletes it.
A typo in a translation. Open the Translations screen and edit the cell. The fix is permanent: the API only ever fills cells that are empty, so no later translation overwrites what you wrote, and every client picks the new text up at its next refresh.
forceTemporary does the same thing from your code, but it writes the cell on every call
for that key — so it beats a dashboard edit. Use it while you are coding, then remove it
once the dashboard holds the right text.
Export your translations as JSON
The Translations screen has an Export JSON button, next to Languages and Manage Team. It downloads every row of the project — not only the 30-day-active list the table shows, but every key ever translated — as one JSON file: the primary language, the supported languages, and each row with one key per language code.
Use it to back up a project outside the dashboard, to diff what changed between two points in time, or to seed another project (self-hosted or hosted) with the same dictionary. The file is a plain export, not a live sync: re-importing it is a manual job of your own, since the API has no bulk-import endpoint.