Skip to content

Handle Locales as Part of URLs#6429

Open
OughtPuts wants to merge 1 commit intorubygems:masterfrom
OughtPuts:ho/middleware-locale-handling
Open

Handle Locales as Part of URLs#6429
OughtPuts wants to merge 1 commit intorubygems:masterfrom
OughtPuts:ho/middleware-locale-handling

Conversation

@OughtPuts
Copy link
Copy Markdown

@OughtPuts OughtPuts commented Apr 16, 2026

TLDR

This PR offers a solution for URL-based locale handling via middleware.

Background

This PR addresses: #6400 (comment)

rubygems.org supports 9 languages, but locale switching was temporarily disabled in #6399 to address a bot abuse incident. Bots were exploiting the locale system (appending locale strings to URLs) to bypass Fastly's CDN cache entirely and hit the Rails origin directly, causing repeated application crashes.

The previous implementation also stored locale in session cookies in session[:locale] and read it from params[:locale] query parameters, meaning the same URL (/gems/rails) could return different content depending on user state. This made thorough CDN caching effectively impossible, since the cache couldn't treat any URL as a stable, language-specific resource.

Approach Details

With this approach:

  • requests to non-default locale paths are intercepted in middleware
  • the locale is extracted from the path and stored in the request env
  • PATH_INFO / SCRIPT_NAME are rewritten so Rails can continue routing against the existing unscoped routes
  • ApplicationController sets I18n.locale from the middleware-provided value
  • existing path helpers continue to generate locale-prefixed links during localized requests
  • the locale switcher is reintroduced using helper logic that rewrites the current path for the selected locale

Why this approach

One of the main motivations here was to avoid helper churn.

Because the middleware rewrites the request before Rails routing runs, most existing helpers can stay as they are:

page_path("about")
rubygem_path("rails")
stats_path

During a localized request like /de/pages/about, those helpers still generate /de/... links because SCRIPT_NAME is set by the middleware.

UI Changes

Re-introduced locale switchers:

image image image

Alternative Considered: Scoped Routes

The other option is to put locale into the route definitions directly, for example:

scope "(:locale)", locale: /en|de|fr/ do
  resources :gems, controller: :rubygems, only: :show
  get "/pages/:id", to: "pages#show", as: :page
end

That is a more "Rails-native" approach, and we could pivot to it if that feels preferable.

The main tradeoff with scoped routes is helper churn. The problem is that many existing route helpers currently rely on positional arguments, and once locale becomes part of the route those calls become ambiguous. In practice, helpers like page_path("about") and rubygem_path("rails") often need to become page_path(id: "about") and rubygem_path(id: "rails"), with similar changes across the app and this approach needing to be adopted by contributors going forward.

So the tradeoff as I see it is:

  • middleware approach: more custom request/path handling, less helper churn
  • scoped-routes approach: more conventional routing, but more widespread outbound URL updates

Changes Included Here

  • added Gemcutter::Middleware::LocaleFromPath
  • re-enabled locale setting in ApplicationController
  • added locale_switch_path helper for locale menu links
  • reintroduced locale switcher UI in the layouts
  • added middleware and system test coverage for localized requests and locale switching

Notes / Feedback Requested

I wanted to put up the middleware version first because it keeps the rest of the app much closer to its current shape.

That said, as explained above I think there are two viable directions here:

  1. keep the middleware-based approach
  2. pivot to scoped routes and accept the helper changes

I’d especially welcome feedback on whether the team would prefer:

  • a smaller-touch but more custom middleware solution, or
  • a more conventional routing solution with broader helper updates

Testing

bin/rails test test/middleware/locale_from_path_test.rb
bin/rails test test/system/locale_test.rb
bin/rubocop \
  lib/gemcutter/middleware/locale_from_path.rb \
  test/middleware/locale_from_path_test.rb \
  test/system/locale_test.rb \
  app/helpers/application_helper.rb \
  app/controllers/application_controller.rb

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Development

Successfully merging this pull request may close these issues.

1 participant