Field by field
Titles and texts get one value per language. Prices, images, dates and references stay shared, so a new photo shows in every language at once.
Localization
Declare your languages once. Each field is translatable or shared, translations sit next to the original, and your editors, your AI and your agents translate in the same editor. No copy of every page per language.

Titles and texts get one value per language. Prices, images, dates and references stay shared, so a new photo shows in every language at once.
One entry holds every language. Adding a language never doubles your pages, your menus or your relations.
Switch language in the toolbar and edit on the translated page. Empty fields show the original as a hint.
Translate with AI fills what is missing with your brand context, keeps HTML and product names, and waits for your review.
Claude, Codex or Cursor read the original, write the translation and list what is left, without ever replacing reviewed text.
Anything not translated yet, down to one row of a list, falls back on the original. Each language has its state: missing, partial, outdated, complete.
Languages live in leiho.schema.json with the rest of your content model. The first one is the default: its content does not move.
{
"leiho": "collections",
"version": 1,
"i18n": { "default": "en", "locales": ["en", "fr", "de"] },
"collections": [
{ "id": "service", "path": "/services/{slug}", "fields": [
{ "name": "title", "type": "text" },
{ "name": "price", "type": "number", "localized": false }
] }
]
}Load the export in each language. Translated items have /fr/ paths and the address of every language, ready for a language switcher and hreflang.
import { loadLeihoContent, indexLeihoContent } from 'leiho/content';
const fr = indexLeihoContent(
(await loadLeihoContent({ url: 'https://cms.example.com', locale: 'fr' })).data
);
const page = fr.byPath('/fr/services/audit');
page.translations; // { en: '/services/audit', fr: '/fr/services/audit' }Translations are stored in one table, one row per entry and language, apart from the original. Ten languages are ten rows, not ten copies of your site to keep in sync.
leiho/list-translations shows what is missing, leiho/get-translation gives the original by path, leiho/update-translation writes it. On a published entry an agent can only fill empty translations.
Free and open source. Try it on a real WordPress, in your browser.