Router/Controller question for multi-lingual website

I’m a newbie so it may seem like a silly question, but I can’t find the right way to do the following…

What I want : urls with locale parameter followed by whatever path. Important: paths are localised too (I’d like to add “of course”, but it’s not often the case), so " /fr/photos" and “/en/pictures” should point to the same controller and action (the locale information should just be in the params).

If I use in my router scope "/:locale/", I get the locale in the params but I cannot specify that the path “photos” should be only when the locale is “fr”.

If I use the root scope, I can write the full paths but I won’t get the locale set in params (and it would be a pain to write different ctrlers)

What I’m thinking is the best option is to declare one scope per locale like this scope "/fr/", scope "/en/", … so every scope defines its own localised paths, and write one function per locale that would set the “locale” param that I would call via pipe_through.

I’d be glad to know the recommended way, as I do not know enough to make a good judgement!

You can create your own plug to set locale for you, but a easy way is to use set_locale.

So in your router you define your basic locale and scopes.

router.ex

...
  pipeline :app do
    ...
    plug SetLocale, gettext: MyApp.Gettext, default_locale: "en"
    ...
  end
...
  scope "/:locale", MyAppWeb do
    pipe_through [:browser, :app, :browser_session]
    get "/", PageController, :index
  end
...

This will add locale as a params to your controller.
At this point you have a locale (fr, en, any…), so it’s up to you to use (to translate with gettext) or not (to provide photos) inside your controller.

controller.ex

  def new(conn, params) do
    locale =
      params["locale"]
    ...
    conn
    |> render("new.html", changeset: changeset,
      locale: locale)
  end
1 Like

As a note, there is a standard header (Accept-Language) passed in for the browser to request a locale, why not use that instead of encoding it into the path?

The header is nice as a default, but people might want so view a different language than the default one.

3 Likes

Exactly this. My browser sends a German request, and i do not bother for much resources, but when I hear some technical documentation, I prefer to read it in English as the quality of the text is usually better, as well as it’s usually more complete as well.

This is especially true when I’m browsing Microsoft documentation, which is partially translated by humans and by machines or simply leaving out complete paragraphs…

as a general approach I usually try to avoid hard coding locale params in the urls, I use the language header or a cookie if the user need to view the page in a different language

You might also find Cldr.Plug.SetLocale in the package ex_cldr useful. (I’m the author). It can:

  1. Set locale for gettext and cldr (either or both)
  2. Identify locale from the Accept-Languge header, the url, the session, a cookie or a query param or any combination in any priority
3 Likes

That’s good to guess the default locale, but if you are not using your own browser you will not want to have the same links open in a locale you do not know. Also, as I want the urls to be different based on the locale, this wouldn’t do (aside from “/”).

This looks like my first solution (I do use cldr’s setlocale in my pipeline btw).

But with

  scope "/:locale", MyAppWeb do
    pipe_through [:browser, :app, :browser_session]
    get "/", PageController, :index
  end

there is no way to define localised urls like get "/photos", PageController, :index and get "/pictures", PageController, :index, as both “/fr/pictures” and “/en/photos” would resolve and I do not want that.

When urls are not localised that’s a good possibility, until you travel and have to use browsers with chinese locale by default :wink:

@kip (I can’t reply to your post as I have 3 consecutive posts…)

I am using it for the root url in order to guess the locale! It works like a charm.

In order to make it to work with the url, I have to use the option “:path”, with a parameter name right? So as I cannot define my routes with a scope like scope "/:locale", MyAppWeb do, I cannot set this parameter name. It worked until I decided to have localised urls :wink: (UNLESS there is a way to restrict routes depending on the value of :locale (e.g. define get "/pictures", PageController, :index only when :locale == "en").

You can still have scopes / pipeplines with locale or not (/photos) in a correct order in your router. In my case I have one that dosen’t need (llike /) and other that check via plug the accepted one and redirect the user to default or available language (gettext).

example without pipeline / locale

  scope "/", MyAppWeb do
    pipe_through :browser
    get "/", Tracker.Controller, :index
  end

I have this for my landing page indeed!

Btw, I have a syntax problem with plugs: when used in a pipeline, we can set options after a comma like

  pipeline :toto do
    plug Cldr.Plug.SetLocale,
      apps: [:cldr, :gettext],
      from: [:path, :accept_language],
      param: "locale",
      default: Cldr.default_locale,
      gettext: TotoWeb.Gettext

But how am I supposed to set those options if I want to use it after pipe_through instead (obviously the comma won’t do)?

  scope "/", TotoWeb do
    pipe_through [:browser, Cldr.Plug.SetLocale ???]

I am using another pipeline so it’s not a big deal, but I am curious…

In most browsers you can set the language to send pretty easily, in other browsers (*cough*chrome*cough*) there are extensions that give you a click-drop-down box. :slight_smile:

Baking them into the URL’s mean that if you send someone a URL and they prefer another language then you lock them into a given language until they can somehow find out how to change it on the site (potentially in a language they are not strong with).

Still pretty trivial to change the default in most browsers (even chrome without an extension just a few more clicks then).

That’s always painful though, you essentially have duplicated content then, and if search engines realize that then they downrank the site last I heard.

Overriding it with a query parameter I think is fine, but it should never be part of the path.

How would you handle SEO, canonical url’s, alternative hreflang tags .etc ? For a publicly facing website you can’t ignore these, otherwise you will confuse the search engines.

Also, if you share/post a link somewhere, anyone following that might have a different Accept-Language header than yours (which the app also handles), will get something possibly different than what you might have wanted to share in the first place. Links should be resilient in this regard.

/paintings won’t suffice in Italian anyway, where you might want to use /dipinti in this case

1 Like

That’s what the additional query parameters are for, and don’t forget to set the query-less version with <link rel="alternate" href="http://example.com/" hreflang="x-default" /> on each page as well and make sure all pages have all links to all the various languages of that same form and with an accept-language-less version defaulting to en as recommended by Google (notice how google’s link even encodes the language as a query argument as well, and without it then it uses the header, and without that it defaults to en).

I’d wager most people do not know it’s even a possibility. As for myself, if it’s not my browser I won’t change such settings even if it’s easy.

It’s fine as long as I provide the urls for each locale in the header. Query parameters should absolutely not be used for locale switching. Your own link from google in your next post makes it clear. Whether it’s the domain which changes or the path is not important (but not query params), as long as the localised links are provided.

Back to the topic, I’m still using my 3rd option and have thus one scope per locale. My problem is that to make it work I had to define one pipeline per locale, as I don’t know how to pass arguments/options to pipes I put in the pipe_through… Is there a way?

E.g. instead of pipe_through [:browser_locale_fr], pipe_through [:browser_locale_es], …, I’d like to have one pipeline and pass it the locale pipe_through [:browser_locale ??? "fr" ???].

How would your alternate hreflang tags differ from each other? Only through the query parameter ? Something like this for example.com homepage?

<link rel="alternate" href="http://example.com/?locale=en" hreflang="en" />
<link rel="alternate" href="http://example.com/?locale=it" hreflang="it" />
<link rel="alternate" href="http://example.com/" hreflang="x-default" />

Using query parameters for locale switching is strongly discouraged. I’m not surprised that Google breaks it’s own rules. That’s nothing new.

How Google crawls locale-adaptive pages

If your site has locale-adaptive pages (that is, your site returns different content based on the perceived country or preferred language of the visitor), Google might not crawl, index, or rank all your content for different locales. This is because the default IP addresses of the Googlebot crawler appear to be based in the USA. In addition, the crawler sends HTTP requests without setting Accept-Language in the request header.

IMPORTANT: We recommend using separate locale URL configurations and annotating them with rel=alternate hreflang annotations.

https://support.google.com/webmasters/answer/6144055?hl=en&ref_topic=2370587

URL parameters
site.com?loc=de
Not recommended.
URL-based segmentation difficult
Users might not recognize geotargeting from the URL alone
Geotargeting in Search Console is not possible

https://support.google.com/webmasters/answer/182192?hl=en#locale-specific-urls

3 Likes