Skip to main content

Frequently asked questions

How do I handle plurals?​

Write one form with {count} where the number goes, and pass the number as count. Every language gets the forms it needs — French two, Russian four, Arabic six — and the primary language gets its own too, so "1 article" is not a ternary of yours.

import { I18nKeylessText } from "i18n-keyless-react";

function CartLine({ count }: { count: number }) {
return (
<I18nKeylessText count={count}>
{"{count} articles dans votre panier"}
</I18nKeylessText>
);
}

The API stores an ICU plural message per language with exactly the CLDR categories of that language, validated against Intl.PluralRules; the SDK picks the branch with the same rules. Add ordinal for a rank (1st, 2nd, 1er). The whole story, with the tables: Plurals and genders.

How do I handle genders?​

Pass the value in select. The model writes one variant per value, in every language, with the agreement that language needs:

import { I18nKeylessText } from "i18n-keyless-react";

function SignedUp({ gender }: { gender: "male" | "female" }) {
return (
<I18nKeylessText select={{ gender }}>Vous êtes inscrit</I18nKeylessText>
);
}

female renders "Vous êtes inscrite" in French and "Zostałaś zarejestrowana" in Polish; a value the row never saw is added on first sight. Any closed set works the same way — a role, a day of the week. A name is not a closed set: it stays a replace. Details in Plurals and genders.

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.

HostedSelf-hosted
PriceMonthly subscription, per project, from €4 by active users (pricing)Fully free, unlimited projects; donations optional
Machine, database, AI keyOursYours
UpdatesAutomaticYou pull the image
Your stringsOn our serversOn 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. Fully free, donations optional.

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: pt and pt-BR, es and es-MX, en and en-GB, zh-Hans and zh-Hant are 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.

One maintainer: what happens if the project stops?​

Fair question. Here is what stops and what does not.

What keeps working without anyone at i18n-keyless:

  • Your app. Every SDK caches translations locally (localStorage, MMKV / AsyncStorage, memory on the server) and serves them when the API does not answer. A client with an empty cache falls back to the source language, never to a blank.
  • The SDKs. They are MIT and published on npm, PyPI, RubyGems, pub.dev, Packagist, Maven Central and as a Go module. Nobody can pull them, and you can fork them.
  • The server. The very image behind i18n-keyless.com is public under the Elastic License 2.0 (ambroselli-io/i18n-keyless-server), and the protocol between SDK and server is written down. Run it with docker run, point the SDKs at it with API_URL, and you are back — including AI translation of new strings, with your own AI key.
  • Your data. The JSON export is one click and holds every row of the project. Take one whenever you like.

What would stop: the hosted instance at i18n-keyless.com — its dashboard and the AI translation of new strings for projects hosted there. Nothing you already have.

Is context not just a key in disguise?​

No, and the difference is what it is for, not how it looks.

A keyA context
RequiredOn every stringOnly where the meaning is ambiguous
UniqueYes, in a global namespace you maintainNo — two strings may share one
What it holdsA name for the stringA description of its meaning
How manyOne per string, thousands per appA handful per app

Close is a button label and a distance; Proche is a friend and a shop nearby. Those are the strings that get a context, and it says what they mean ("the window", "distance"), which is exactly what a translator would ask you. The rest carry nothing. The source text remains the lookup in both cases; two identical texts with two contexts are two rows. See Add context.

Can I review the copy in a pull request?​

Yes — better than with keys, in fact. The source string is in the code, so every wording change is a diff the reviewer reads in plain language:

- <I18nKeylessText>Your order is on its way</I18nKeylessText>
+ <I18nKeylessText>Your order has shipped</I18nKeylessText>

With keys, that same review is t("order.status") on both sides and the actual change is in a JSON file the reviewer has to open.

What is not in git is the translation into the other languages. It lives in the database, as it does with every translation platform, and you review it in the dashboard: each cell has a reviewed mark that separates what a person checked from what the AI wrote, and an edit by hand is never overwritten by the AI. The JSON export gives you the same data as a file, to diff or to keep next to the code if you want it there.

Why not Tolgee, which is open source too?​

Tolgee is a good product, and it is the right choice for some teams. It is a key-based translation platform under the Apache 2.0 licence, and it has things i18n-keyless does not: in-context editing (alt-click a string in your running app), screenshot context, translation memory, and a workflow for human translators. If you have translators, or your product needs those tools, take Tolgee.

i18n-keyless is a different shape: no keys, no locale files, AI translation by default with a manual override the AI never rewrites, and a self-hosted image that runs on SQLite in one container. There is no browser extension for in-context editing; the equivalent is a screenshot of the screen with the wrong string, given to an AI agent connected to the MCP server, which finds the string and overrides the translation for you. The server is source-available under the Elastic License 2.0, not Apache; the SDKs are MIT. The comparison page puts the prices side by side, and i18n-keyless vs Tolgee goes through the differences one by one.