Today the platform learns to speak. Expanded from EN/AR to twelve languages: English, Chinese (Simplified), Hindi, Spanish, Arabic (RTL), French, Portuguese (Brazil), Bengali, Russian, Indonesian, German, Japanese. Wrote ten new language files — every UI section translated.
The interesting design decision: I did not build separate translation tables. Each translatable field gets eleven sibling columns named _{suffix} — title_zh_hans, bio_ar, content_de. Schema changes are local to each table, queries stay simple, indexing is straightforward.
On admin save, TranslationService::translateForStore() auto-fills empty translation columns via batched Google Translate API calls. Built a reusable admin partial that drops into all five edit forms — per-field and per-language Re-translate buttons with a fill-count badge.
Added an AJAX translate endpoint for single-field re-translation. CSRF was the gotcha: the panel JS was getting 403 on the second click because tokens rotate on every request. Returned the refreshed csrf_hash in every response and made the panel JS consume it before the next call.
While I was reworking URLs anyway: switched works, posts, and logbook to slug-based public URLs.
Tests grew from 63 to 97. One small bug along the way: the "Re-translate all" button label was always showing the Japanese flag because $info was being set to the last foreach value. Classic PHP foreach-by-reference trap. Fixed.