5. Translating
Kewos contains no hard-coded visible string, neither on the server nor in the interface. Everything goes through a key and one translation file per language.
Your module follows the same rule. Without it the engine falls back on the code’s technical
names: a column called published_year, a value shown as worn, a section titled
section.work. Nothing is broken, it is simply unreadable.
The file
i18n/en.json:
{ "module.library": "Library",
"model.library_title": "Catalogue", "model.library_copy": "Copies", "model.library_member": "Members", "model.library_loan": "Loans",
"section.work": "The work", "section.reference": "Reference",
"library_title.name": "Title", "library_title.author": "Author", "library_title.isbn": "ISBN", "library_title.published_year": "Published", "library_title.genre": "Genre", "library_title.genre.fiction": "Fiction", "library_title.genre.science": "Science",
"library_loan.state": "State", "library_loan.state.out": "Out", "library_loan.state.returned": "Returned", "library_loan.state.lost": "Lost",
"workflow.check_in": "Check in", "workflow.declare_lost": "Declare lost",
"group.library_reader": "Reader", "group.library_librarian": "Librarian"}Then i18n/fr.json with exactly the same keys and other values.
The keys, once and for all
| Shape | What it names |
|---|---|
module.<module> | The module in the launcher. |
model.<model> | The model in menus and screen titles. |
<model>.<field> | A field, everywhere it appears. |
<model>.<field>.<value> | A selection value. |
section.<key> | A form section title. |
workflow.<transition> | A state transition button. |
group.<group> | A permission group. |
<model>.dashboard.<key> | A dashboard indicator or chart. |
What it changes

The kanban columns read “Out”, “Returned”, “Lost”; the module is called “Library”; the cards say “Member” and “Due back”. You touched no screen: one mapping file, and everything falls into place.
The other languages
Add i18n/de.json, i18n/es.json with the same keys. The user’s language decides, and a key
missing from one file falls back to the default language.
The second directory, the one for server messages
i18n/ translates the interface: what the engine renders from your declarations, the field
names, the values, the buttons. It does not translate the sentences your code produces.
A business error message, an email subject, a line on a PDF: those go through t("key") on the
server, and the server does not read i18n/. It reads a second directory:
modules/library/├── i18n/ <- the interface (the view engine)│ ├── fr.json│ └── en.json└── translations/ <- your messages, resolved by t() ├── fr.json └── en.json{ "library.error.already_out": "This copy is already out." }raise BusinessRuleError(t("library.error.already_out"))Same rule as above: the same keys in every language, and a test comparing the key sets.
The same file, in KM
A KM module carries its translations inside its file, one block per language:
translate en { model.library_title "Catalogue" library_title.name "Title" library_title.genre.fiction "Fiction" section.work "The work" workflow.check_in "Check in"}
translate fr { model.library_title "Catalogue" library_title.name "Titre" library_title.genre.fiction "Fiction" section.work "L'œuvre" workflow.check_in "Rendre"}The keys are exactly the same, and this step’s rule does not move: a key present in one language and absent from another is a defect, whatever the format.