webx-ui/localization
Webx UI Localization brings localization tools to your Laravel UI. It offers helpers and components for managing translations, switching locales, and keeping multilingual interfaces consistent, so you can ship apps that feel native across languages with less effort.
Architecture fit. This is a conventional Laravel locale vertical — locales table + Locale model, a Locales service facade, a HasTranslations trait that stores all locales as a {"en": "...", "uk": "..."} JSON map in one column, $table->translatable() blueprint macros, two middleware aliases (webx.locale, webx.panel-locale), a OneSpellingPerAddress route guard, and two Artisan commands. That conventionality is the TPM-relevant fact: the work is estimable by any Laravel team without a spike, and no exotic infrastructure (queues, external services, exotic drivers) enters the critical path.
The one item that requires product sign-off before sprint one is the single JSON column per translated attribute in place of a side translation table. Reads and writes stay single-row, but per-language uniqueness and indexed filtering move out of the database and into application scopes (whereTranslation, whereTranslationLike, orderByTranslation). Leaving JSON maps for per-locale tables later is a data migration, not a config change. Assign a named owner, record the decision with content-volume and reporting assumptions, and gate it before the first translated row is written.
Integration feasibility. The package is small; its value is not, and that asymmetry is the main planning hazard. /api/cms/locales and translation endpoints live in webx-ui/module-admin; translated address uniqueness lives in webx-ui/routing; cache invalidation rides on webx-ui/cache (CacheGroups::LOCALES). The planning unit is therefore a three-to-four package cluster — budget, version pinning, and upgrade rehearsals must cover the cluster, not this artifact alone. One genuinely useful bootstrap property: until the locales table exists, the service answers from config('webx-localization.locales'), so install-before-migrate does not fail. That materially de-risks phased rollouts and lets config and middleware land ahead of the schema.
Technical risk, ranked for a steering committee:
webx-ui/webx-ui (php/packages/localization); issues and PRs route upstream with no SLA, and there is no independent adoption to learn from. Every upgrade is "wait for upstream, then re-pin." Frame the decision as adopting webx-ui as a platform, not adopting this package à la carte — the risk read is wrong in both directions otherwise.Locale model on save. Concurrent saves or raw SQL can produce two defaults or a denormalised code; recovery requires a cache clear. Acceptable, but it is an operating rule the team must actually follow, and it belongs in the runbook.webx-ui/routing's scope — scope it as a separate workstream or you will promise something this package cannot deliver.lang edits require webx:locales:clear (or WEBX_LOCALIZATION_CACHE=false in dev). Stale translations are the most probable day-one support ticket; plan a deploy hook, not a wiki page.vendor/webx-ui/localization is unsupported; a missing seam is framed as an upstream bug. Good discipline, but it converts every gap into an upstream dependency with no committed turnaround.Key questions for the maintainers before committing.
whereTranslationLike — index-assisted or full scan?panel() dictionary generation have a size ceiling when 'namespaces' is widened across modules?Stack fit. Native Laravel, no impedance mismatch. Config publish tag webx-localization-config, per-module lang publish tags (webx-admin-lang plus each module's own -lang), middleware aliases registered by the provider, blueprint macros global after boot. The package's ten validation languages are appended behind the application's own lang/ strings, so app overrides win. Confirm that ordering in a half-day spike rather than assuming it — it determines whether our product copy or vendor copy ships, and it is the cheapest possible test of a claim the whole content plan rests on.
Migration path. Greenfield: publish config → define 'locales' → php artisan webx:locales:seed → register middleware → adopt HasTranslations on new models. Brownfield: add $table->translatable('title', 'body') as nullable columns, backfill existing single-language values into {"<default>": "<value>"} maps, then switch reads to forLocale() / getTranslations(). Run a dual-read window — read through the trait while the old column is still authoritative — before dropping legacy fields. Budget the backfill as its own ticket with a rollback script; it is the one irreversible step in the sequence and it touches production content.
Compatibility. Two boundaries need explicit product acknowledgement, not engineering footnotes. First, translated slugs are out of scope and belong to webx-ui/routing — if the roadmap promises localized URLs, that is a second workstream with its own estimate. Second, the 'strategy' => 'prefix' | 'header' | 'none' switch lets you defer the SEO/URL-structure debate, but it is a launch-time decision with permanent redirect implications, not a toggle you flip post-launch. OneSpellingPerAddress hard-codes the consequence: unknown prefix → 404, default prefix → 301 unless prefix_default is on. Changing strategy after launch means a redirect-map project.
Sequencing. Five gates, roughly in order:
prefix_default, locked before launch.webx.panel-locale is configured independently of webx.locale, which lets you plan "public site in N languages, admin in 2." But panel translation costs scale with modules × languages in vendor content, not with our code — that is a recurring line item, not a one-time engineering task.Maintenance. Low code surface, high coupling. The package itself will rarely need attention; keeping it in lockstep with the monorepo siblings (module-admin, routing, cache) is the ongoing cost. Set a re-pin cadence tied to upstream tags and rehearse one upgrade on a branch before the first production release so the second one isn't a discovery. Add a standing rule to the engineering handbook: never edit vendor/webx-ui/localization; report the gap upstream and track it. Without that rule, the first missed seam becomes a silent fork.
Support. Two recurring ticket classes are predictable enough to pre-write runbooks for. (1) Stale translations — content edited, cache not cleared; fix is webx:locales:clear, prevention is a deploy hook. (2) Content team deletes a language row to "hide" it — the docs are explicit that the correct action is is_active = false, because translated text persists in every JSON map and a row deletion orphans the corpus. Underwrite a product policy from that: we can retire a language from the UI without losing the content. It matters for re-launch decisions, archival obligations, and content-team trust. Encode it as a permission restriction, not a training slide.
Scaling. The scaling ceiling here is query shape, not traffic. Reads and writes are single-row, which is excellent; but whereTranslation/orderByTranslation will not match native-column performance under heavy filtering, sorting, or aggregation on translated fields, and whereTranslationLike may be a full scan. The package suits read-heavy public pages with caching in front and thousands — not millions — of rows across a handful of languages. Set an explicit tripwire: if analytics or per-language reporting on translated content becomes a roadmap item, that is the trigger to revisit the storage model, and the earlier JSON-map decision record should name it as the revisit condition. Also ask for the emitted SQL before launch so the first slow query isn't the discovery.
Failure modes, with the ones worth pre-planning:
OneSpellingPerAddress is a tested invariant — don't reimplement it — but the prefix_default choice must be exercised in staging with real URLs, because the redirect implications are permanent.lang files over code changes, and flag any needed vendor change as a schedule risk rather than absorbing it silently.Ramp-up. Steepest curve is not the package — it is the surrounding model. Allocate roughly: half a day to verify the lang/ append-behind ordering; one day to stand up config, migration, seed, and middleware in a scratch environment; two to three days for the brownfield backfill with dual-read on a real dataset; then a dedicated slice for the panel dictionary (panel(), 'namespaces', webx.panel-locale) that scales with module count. Engineers need three things up front that the docs make easy to miss: the storage model is JSON-in-one-column and the trait is the only supported read/write path; the locales table wins over config after migration and config is seed-only; and the extension surface is exactly the published config, published lang tags, and model traits — a missing seam is a bug to report, not a workaround to build.
Bottom line for the steering deck. Low, predictable cost profile — MIT, no vendor contract, no per-seat fees — against a real platform-coupling risk from a zero-adoption, single-vendor, read-only split. Adopt as part of a webx-ui platform decision, not as an isolated bet. Lock the storage model and the URL strategy before the first translated row or the first indexed page. Treat translated-slug handling and panel translation as separately scoped workstreams with their own estimates and owners.
How can I help you explore Laravel packages today?