VSA Monolith Framework

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

CommandWhat it does
i18n:missingKeys 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:syncAdds every used-but-missing key: English gets the humanized key (room_types → Room types), other languages get the English text as a placeholder
i18n:unusedEnglish keys never referenced literally. A report only — dynamic keys exist and cannot be resolved
i18n:reviewA CSV of group,key,en,<lang>,status for a translator, UTF-8 with a BOM and CRLF so Excel opens it correctly
i18n:importWrites 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:normalizeRewrites 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(…), and title_prop values as titles.*
  • resources/views/**/*.blade.php — vslang, with Blade and HTML comments ignored
  • app/**/*.php — Localization::trans('key', 'group'), defaulting to the validation group
  • config/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".

GroupFor
titlesScreen and dialog titles
menusSidebar items and group headers
buttonsButton and menu-action captions
labelsField labels and column headers
messagesConfirmations, toasts, empty states
validationServer 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

Terminal
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.