All notable changes to this project will be documented in this file. This project adheres to Semantic Versioning.
[1.3.0] — 2026-10-24
Breaking changes
Requires
localize ~> 1.4. Localize 1.4 returns{:error, exception}for a value of the wrong type, so the type checkslocalize_webcarried ahead of its validators are removed; an invalid currency or territory is now reported as Localize reports it,Localize.UnknownCurrencyErrororLocalize.UnknownTerritoryError, where it wasLocalize.InvalidValueError.Localize.Routes.LocalizedHelpers.hreflang_links/1returns{:ok, safe}or{:error, exception}rather than the rendered HTML, and no longer emits an annotation it cannot validate. A locale that does not resolve returnsLocalize.InvalidLocaleErrorand a URL that is not fully qualified returnsLocalize.RelativeUrlError; search engines ignore both silently, so the site appeared not to declare the variant at all.
Added
Localize.HTML.Hreflang.links/2rendersrel="alternate"hreflangannotations from a map of locale to URL, validating each locale and rendering it in canonical BCP 47 form, so"es_MX"becomes"es-MX". The key"x-default"designates the fallback variant and is emitted as supplied.hreflang_links/2inLocalize.VerifiedRoutesrenders the annotation set for a route across every configured locale, so verified routes have the same support the generated helpers had.
Changed
- A
Localize.LanguageTaggiven topath_for/2,url_for/2or~q, or found by the:routesource ofLocalize.Plug.PutLocale, is used as it stands rather than validated again, so a tag whose locale has no routes takes the default locale's. Build tags withLocalize.validate_locale/1.
Fixed
Localize.HTML.Localenames each locale in its option list on Localize 1.4, where every option showed its code: its default:preferwas:default, which Localize 1.4 rejects.
[1.2.0] — 2026-09-27
Breaking changes
Localize.Plug.PutLocalenow checks[:route, :path, :query, :session, :accept_language]by default, so a locale in the URL wins. The previous default put:sessionand:accept_languagefirst, and a URL locale never took effect in a browser; pass:fromto restore it. Closes #25.Localize.HTML.Message.render_to_safe/3returns{:error, exception}for a message it cannot render, where it raised. The<.message>component andLocalize.HTML.t/2log a warning and render the escaped source, or the children of an unknown markup tag, rather than raising.Localize.HTML.Monthreturns{:error, %Localize.UnknownCalendarError{}}for a:calendarthat is not a calendar module, such as the CLDR type:hebrew, and an error for an unknown:style, where both were silently rendered as Gregorian:widenames.Localize.HTML.Localelists the locales configured for Localize (Localize.supported_locales/0) by default, rather than every CLDR locale. Pass:localesto list others.
Added
Localize.Routes.localize_live_session/3defines one live session per locale, so a live navigation to another locale reloads the page in that locale rather than keeping the old one. A localizedliveroute outside it now warns at compile time. Closes #24.Localize.Plug.PutLocalesets avaryheader for each request header it read to find the locale (accept-language, andcookiefor:sessionand:cookie), so a shared cache stores one response per language. Thanks to @rubas for the PR (#21).
Fixed
The select helpers in
Localize.HTMLreturn{:error, exception}for any invalid option — a bad:locale,:selected, list,:style,:collatoror:mapper, or options that are not a keyword list — where 201 such inputs raised.Localize.HTML.Territoryno longer raises with its default options.Localize.AcceptLanguage,Localize.Plug.put_locale_from_session/2, theget_locale/1functions,locale_from_host/1andhreflang_links/1return an error ornilfor input of the wrong type, and the LiveViewon_mountexamples no longer match on a result a first visit cannot produce.Localize.AcceptLanguage.best_match/1skips a tag that matches no supported locale and tries the next one. It previously returned the requested language carrying the first supported locale's data, soes-ES,fr-CH;q=0.9served the first supported locale rather than French. Thanks to @rubas for the PR (#19).~q,path_for/2,url_for/2and the localized helpers render the default locale's route for a locale without localized routes, or an invalid one, where they raised.path_for/2andurl_for/2also accept a string orLocalize.LanguageTag. Thanks to @rubas for the PR (#22).~qtranslates only the literal path, not a path inside the query string or a string literal inside#{...}code. Thanks to @rubas for the PR (#17).Nested resources are translated when their parent
resourceshas options such asonly:, and a localized route accepts a module attribute as itsprivate:option. Thanks to @rubas for the PRs (#18, #26).Localize.Plug.put_locale_from_session/2returns aLocalize.Plug.NoSessionLocaleErrorfor an empty session, where it returned a tuple, and thePutSessionexample no longer passes the removed:appsoption. Thanks to @rubas for the PRs (#20, #23).
[1.1.0] — 2026-08-11
Added
Localize.HTML.Subdivisiongenerates<select>tags and option lists for the subdivisions of a territory — US states, Canadian provinces, French departments — localized and sorted withLocalize.Collation. CLDR writes a subdivision code as its territory plus the ISO 3166-2 code, so:uscabecomes an option value of"ca"; passfull_codes: trueto keep the CLDR form.
Fixed
Localize.HTML.Currency.select/3,Localize.HTML.Locale.select/3,Localize.HTML.Territory.select/3andLocalize.Plug.put_locale_from_session/2are specified as returning{:error, Exception.t()}. Each reports an invalid locale as an exception struct rather than the{module, message}tuple its specification claimed — the same correction made toLocalize.HTML.Unit.select/3in 1.0.0, which the others were missed in.Localize.HTML.Month.select/3andmonth_options/1now validate the:localeoption, returning{:error, Exception.t()}as every other select does. They previously ignored an invalid locale and rendered month names in the default one, so a typo produced English months rather than an error.
[1.0.1] — 2026-08-03
Fixed
~qnow resolves the#{locale},#{language}and#{territory}interpolation forms, which previously raiseda dynamic ~p interpolation must follow a static segmentat compile time in every position. Substituting the token left the enclosing::binarywrapper in place, so Phoenix saw a literal inside a dynamic segment and matched none of its route-verification clauses. Thanks to @sumerokr for the report. Closes #15.mix phx.routes <Router>.LocalizedRoutesnow works, where it raisedfunction ... formatted_routes/1 is undefined. The generated module hosts route definitions but is not a router, so theformatted_routes/1and__helpers__/0callbacksPhoenix.Router.ConsoleFormattercalls are now defined on it. Thanks to @sumerokr for the report. Closes #16.
Documentation
Align the documentation module names to the canonical Phoenix generator forms, and require
~> 1.0in the installation snippets rather than the long-superseded~> 0.1.0. Thanks to @sumerokr for the PRs. Closes #13, #14.Localize.VerifiedRoutesdocuments that every option other than:gettextpasses through toPhoenix.VerifiedRoutes, and that:staticsin particular must be kept when converting fromuse Phoenix.VerifiedRoutes— without it the asset paths in the default Phoenix layouts warn that no route matches (#16).
Removed
- The
LocalizeWebmodule. It defined no functions — only a moduledoc listing the plugs, routes and HTML helpers, all of which live underLocalize.*and are unaffected.
[1.0.0] — 2026-07-31
The first stable release, built on Localize 1.0.
Changes
- Requires
localize ~> 1.0.
Fixed
Localize.HTML.Unit.select/3is specified as returning{:error, Exception.t()}. Localize 1.0 reports an invalid unit or locale as an exception struct rather than a{module, message}tuple, so the previous specification did not describe what the function returns.currency_options/1,locale_options/1,territory_options/1andunit_options/1no longer claim to return an error tuple. They validate by raising, so the documented error return could not occur.
[0.8.0] — 2026-06-25
Bug Fixes
Localize.Plug.PutSessionno longer rewrites the session (and emits a redundantSet-Cookie) when the stored locale is unchanged, avoiding needless response bandwidth and preventing caching layers from treating responses as private. Ported fromex_cldr_plugswith thanks @maltoe.
[0.7.0] — 2026-05-13
Enhancements
Localize.HTML.Message— new function component (andLocalize.HTML.message/1facade) that renders an MF2 message preserving inline markup. Thelinkdefault renderer uses Phoenix's<.link>and so acceptshref,navigate, orpatchMF2 attributes; per-call:componentsandconfig :localize_web, :mf2_markup, components: %{…}override the defaults, and unknown tags raiseLocalize.HTML.Message.UnknownMarkupError.Localize.HTML.t/1andt/2— new compile-time macros for HEEx templates that combine Gettext extraction, MF2 binding interpolation, and markup rendering in one call:{t("Read {#bold}terms{/bold}")}. Elixir#{@user.name}interpolations have theassignsprefix stripped so derived binding names match what a developer would write (@user.name→user_name).
Test infrastructure
mix testnow runsmix localize.download_localesfirst, populating CLDR data for the locales referenced by the suite (en,fr,de,th,ja,ar,zh,zh-Hans,zh-Hant). Fresh checkouts and CI no longer fail on missing locale display data.
[0.6.0] — 2026-05-11
Enhancements
- Add
path_for/2andurl_for/2macros toLocalize.VerifiedRoutesto render a verified path or URL in an explicit locale without changing the process-wide locale, supporting language-switcher and hreflang use cases that need every configured locale rendered in one template pass.
[0.5.1] — 2026-04-25
Bug Fixes
Ignore out-of-range and zero-weight q-values when parsing
accept-languageheaders per RFC 9110. Thanks to @rubas for the PR. Closes #6.Expose
month_select/3andmonth_options/1on theLocalize.HTMLfacade as documented in the moduledoc. Thanks to @rubas for the PR. Closes #9.Honor the
:calendaroption inLocalize.HTML.Monthby sourcing month labels from the CLDR calendar returned by the calendar module'scldr_calendar_type/0function. Thanks to @rubas for the PR. Closes #10.
Changes
- Document that
fetch_session/1must run beforeLocalize.Plug.PutLocalewhen:sessionor:cookiesources are used. Thanks to @rubas for the PR. Closes #7.
[0.5.0] — 2026-04-17
Bug Fixes
- Be more lenient when parsing invalid
accept-languageheaders. Duplicateq=might be invalid syntax but they shouldn't crash the parser. Thanks to @woylie for the report. Closes #3.
[0.4.0] — 2026-04-16
Changes
- Don't call
Localize.default_locale/0at compile time. That causeslocalizeto be loaded at compile time which causes issues on machines with constrained resources. Defer the call to runtime.
[0.3.0] — 2026-04-16
Changes
- Make
phoenix_html_helpersa required dependency (it was optional).
[0.2.0] — 2026-04-15
Changes
- Fix docs links in the package.
[0.1.0] — 2026-04-13
Highlights
Initial release of localize_web, providing Phoenix integration for the Localize library. This library consolidates the functionality previously provided by ex_cldr_plugs, ex_cldr_routes, and cldr_html into a single package.
Locale discovery plugs that detect the user's locale from the accept-language header, query parameters, URL path, session, cookies, hostname TLD, or custom functions. Includes session persistence and LiveView support.
Compile-time route localization that translates route path segments using Gettext, generating localized routes for each configured locale. Supports locale interpolation, nested resources, and all standard Phoenix route macros.
Verified localized routes via the
~qsigil, providing compile-time verification of localized paths that dispatch to the correct translation based on the current locale.Localized HTML form helpers that generate
<select>tags and option lists for currencies, territories, locales, units of measure, and months with localized display names.
See the README for full documentation and usage examples.