This is the next entry in a build-in-public series where I extract a real, production Laravel CRM into a reusable SaaS core, one module at a time, and ship each piece as a Composer package. So far: the foundation layer, auth on top of Fortify, multi-tenancy, roles and permissions, and the platform activity log. This one is v0.6.0: multilanguage.

And this is the post where the feature sounds the most boring and taught me the most. A language switcher. Everybody has one. You would not expect it to hide two bugs. It did, and both of them lived exactly where two systems meet.

Half of this already shipped, and that is the point

Here is the honest framing. The hard part of i18n was already in the box, since the very first foundation release.

Back in v0.1.0 the core already had the locale resolution chain: a single middleware that figures out the active language from the user's stored preference, then the session, then a cookie, then the Accept-Language header, then an optional geo lookup, then a default. Every one of those candidates is validated against a single allow-list before it is applied, so a junk code like ua or English can never reach the app or the database. There was a ValidLocale rule backing that same allow-list, and a HasLocalePreference contract so the core never assumes which column on your user model holds the locale.