Skip to content

Fall back to the default locale's route when a locale has no localized routes - #22

Merged
kipcole9 merged 2 commits into
elixir-localize:mainfrom
rubas:fix/q-locale-fallback
Sep 27, 2026
Merged

kipcole9 merged 2 commits into
elixir-localize:mainfrom
rubas:fix/q-locale-fallback

Conversation

@rubas

@rubas rubas commented Sep 26, 2026

Copy link
Copy Markdown
Contributor

Before

Localize.Plug.PutLocale accepts any supported locale, also one without Gettext translations. In the test app, ja is supported, and Gettext has en, fr and de:

# Accept-Language: ja, then PutLocale
~q"/users"
#=> ** (CaseClauseError) no case clause matching: :ja

MyApp.Router.LocalizedHelpers.user_path(conn, :index)
#=> ** (ArgumentError) no function clause for ... for locale Localize.LanguageTag.new!("ja")

path_for(:"fr-CH", "/users")
#=> ** (CaseClauseError) no case clause matching: :"fr-CH"

With supported_locales: [:"de-CH", ...] and Gettext de, the route keys are :"de-CH", so path_for(:de, "/users/log-in") and path_for("de-CH", "/users/log-in") raise too.

Problem

  • A visitor whose browser prefers a supported locale without translations gets a 500 on every page that renders a ~q link.
  • path_for/2 and url_for/2 accept only the exact CLDR locale id atom of a route. The moduledoc shows path_for(@locale, "/users") with a locale from the request, which is a LanguageTag, and that raises.

After

# Accept-Language: ja (no routes), default locale en
~q"/users/1/faces"                                 #=> "/users/1/face" (the en route)
MyApp.Router.LocalizedHelpers.user_path(conn, :index) #=> "/users"
url_for(:ja, "/users/1/faces")                     #=> "http://localhost/users/1/face"

path_for("fr", "/users")                           #=> "/users_fr"
path_for(:"fr-CH", "/users")                       #=> "/users_fr"
path_for(Localize.LanguageTag.new!("fr-CH"), "/users") #=> "/users_fr"
path_for("not a locale", "/users")                 #=> ** (Localize.InvalidLocaleError)

A locale without routes gets the route of the default locale. If the default locale has no routes either, an ArgumentError names both locales and the locales that have routes.

Details

Cause. Localize.VerifiedRoutes.sigil_q/2, path_for/2 and url_for/2 generate case <locale id> do with one clause per Gettext locale and no other clause. The public functions in Localize.Routes.LocalizedHelpers pass Localize.get_locale() to helper/7, which has clauses only for the same locales.

Design. A new Localize.Routes.route_locale/2 returns the locale whose routes to use. ~q, path_for, url_for and the localized helpers call it.

  • A LanguageTag whose cldr_locale_id has routes is returned as is. This is the usual case for ~q, which runs once per link, so it costs one list check.
  • Other input goes through Localize.validate_locale/1. This accepts atoms, strings and tags, and maps :de to :"de-CH" when de-CH is the supported locale. An invalid locale raises the validation error, because that is a caller bug.
  • A valid locale without routes falls back to Localize.default_locale(). The path as written in ~q is not a safe fallback: localize registers only translated paths, so that path may have no route.

Helpers for routes localized for only some locales (for example localize "fr" do ... end) raise as before for the other locales.

The three macros now share one private locale_case/4, which removes the duplicated case generation.

Tests. sigil_q_test.exs, path_for_test.exs and routes_test.exs cover :ja in ~q, url(~q...), path_for, url_for and a localized helper, the string, regional and LanguageTag input forms, and an invalid locale.

Checks. mix test 273 passed. mix format --check-formatted, mix compile --warnings-as-errors clean. mix credo --strict reports only the existing module name issue in test/sigil_q_interpolation_test.exs.


AI disclosure: AI models helped us find this issue, write the change, and review it.

A locale without localized routes (supported, but without Gettext
translations) raised CaseClauseError in ~q and ArgumentError in the
localized helpers. path_for/url_for also accept any locale form that
Localize.validate_locale/1 accepts.
@kipcole9
kipcole9 merged commit a49bb1c into elixir-localize:main Sep 27, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants