Translations
One PHP file per language, a set of commands that keep them in step with the code, and a CSV round trip for the person doing the translating.
Where the text lives
app/Locales/<lang>.php holds one language: its
name, its code, then groups of
key => text. The localization package serves it over the API,
LocaleManager applies it in the browser through
vslang="group.key" and LocaleManager.trans(), and the
server uses the same file for navigation labels and validation messages.
The CLI reads these files with PHP's own tokenizer, so a group declared twice is
merged — where a plain require would silently lose the earlier block.
What a missing key looks like at runtime. In the browser,
LocaleManager replaces the element's text with the key
itself, overwriting whatever the blade had — so a missing
titles.enrolled_students shows up as
enrolled_students on screen. On the server it renders as the key
with underscores as spaces, and logs a warning per request.
The commands
| Command | What it does |
|---|---|
i18n:missing | Keys the code uses that a locale lacks, grouped by file. Exits 1 when en lacks any — that is the pre-commit and CI form, as --lang=en |
i18n:sync | Adds every used-but-missing key: English gets the humanized key (room_types → Room types), other languages get the English text as a placeholder |
i18n:unused | English keys never referenced literally. A report only — dynamic keys exist and cannot be resolved |
i18n:review | A CSV of group,key,en,<lang>,status for a translator, UTF-8 with a BOM and CRLF so Excel opens it correctly |
i18n:import | Writes back only the changed cells of the target language. Blank cells are skipped; a row whose English no longer matches is a conflict; keys no locale has are refused unless --add |
i18n:normalize | Rewrites every locale in the managed format. --check exits 1 if a file is not in it, which is what the pre-commit hook runs |
Where keys are found
public/js/**/*.js—vslang="…",setAttribute('vslang', …),LocaleManager.trans(…), andtitle_propvalues astitles.*resources/views/**/*.blade.php—vslang, with Blade and HTML comments ignoredapp/**/*.php—Localization::trans('key', 'group'), defaulting to thevalidationgroupconfig/apps/*.php— page titles, navigation group and submenu labels, the sidebar logout title
References built at runtime — a template literal, a value out of an array, a
variable passed to trans() — are listed rather than
resolved, because there is nothing static to resolve.
The key convention
group.snake_case_key, for example vslang="labels.start_date".
| Group | For |
|---|---|
titles | Screen and dialog titles |
menus | Sidebar items and group headers |
buttons | Button and menu-action captions |
labels | Field labels and column headers |
messages | Confirmations, toasts, empty states |
validation | Server and validator messages |
A snake_case key stays stable when the English wording changes. An
English-text key such as 'Add Building' has to be renamed in the code
and in every locale just to fix a typo — or it quietly lies. It also cannot collide
with the :: and => syntax the files use, and it sorts
and greps predictably. The group says where the text appears, which is what lets a
translator keep a button short and a message complete.
Existing English-text keys stay as they are — do not mass-rename them.
i18n:sync humanizes only snake_case keys, so both kinds work side by
side.
The managed format
A header comment, name, code, then the groups
validation, titles, menus and
buttons followed by the rest alphabetically; one block per group; keys
sorted case-insensitively; four-space indent; single-quoted strings.
Edit values by hand as much as you like — i18n:normalize puts a
hand-added key back where it belongs. make:page writes this format too.
The review round trip
vsa i18n:sync # every used key exists in every locale
vsa i18n:review --lang=km --out=km-review.csv # send this to the translator
# they fill or fix the "km" column only, then Save As "CSV UTF-8"
vsa i18n:import km-review.csv --lang=km --dry-run
vsa i18n:import km-review.csv --lang=km
vsa i18n:normalize --check && git diff app/Locales
Text that is rightly identical to English — OK, brand names,
Email — goes in app/Locales/.same-as-english.json, either
for every language or per language, so review stops flagging it as
untranslated.