Language-specific redirects based on Accept-Language header with Cloudflare

Table of Contents

This is my first multilingual website which means, upon publishing it was my first time dealing with language-specific redirects.

All I knew beforehand was that I want the end user to be in charge. For this reason, I wanted my redirects to depend on the user's device (user's browser, to be more specific) language.

Whenever a browser sends a request to a server, the HTTP request header contains an array of http.request.accepted_languages[] with the user's preferred languages in descending weight order.

Cloudflare can get this value and enables you to set up "dynamic redirects" based on this data.

It is not called Dynamic Redirect anymore

This feature got announced around 2022 by the name Dynamic Redirect. If you Google around this feature, you are most likely to find information that refers to it as such.

It is not called Dynamic Redirect anymore. What you are looking for is Redirect Rules under Rules on the sidebar.

Having that said, dynamic redirects may live on as a technical term.

Create a new Redirect Rule

screenshot of Cloudflare admin showing where to create a new redirect rule First, you must name your redirect rule. Choose a descriptive name. You'll thank yourself later. Second, you must select the Custom filter expression option to reveal the expression editor. This is where the magic happen and the part we are about to focus on moving forward.

screenshot of Cloudflare admin showing the steps required to create a new redirect rule

The internet can be misleading

If you want to continue on, skip this section.

Googling around people seem to suggest to be specific with your language versions. Meaning, if you have an /en/ and a /fr/ version, then redirect every request there.

This is an example filter they usually recommend:

(starts_with(http.request.accepted_languages[0],"en") and not starts_with(http.request.uri.path, "/en")) or
(starts_with(http.request.accepted_languages[0],"fr") and not starts_with(http.request.uri.path, "/fr"))

I did not find that so helpful. Why?

It's oddly specific

It only filters URLs that are already on the language version (so it won't select URLs that have /en/ to redirect to /en/). It seemingly thinks in only two languages.

Sure, targeted languages are covered this way, but what if a user's has "de" or "es" as the highest-weighted Accept-Language?

It may break user experience

Another thing I'm not entirely sure of is what is going to happen if the user's first Accept-Language is "en" but would like to read the content in "fr". Because every single click is gonna be a new request towards the server and is going to be evaluated by the above. Does this mean that the user is going to be redirected to /en/ even after they selected /fr/ in a language selector? I don't know.

At this point some may scream "cookies" or "local storage" but these are not options for me as they are way too invasive to my taste.

This approach may break assets

Many website store assets in a generic folder, not inside the language folder. Like example.com/style.css or example.com/assets/fonts/font.woff. All of these are targeted by the filter above and are going to be redirected — responding with 404s. So you can either restructure the project or add excludes to the filter above. Either which, it is going to complicate things.

This is how I solved it

As this site has only two languages, the logic and therefore the setup is fairly simple.

I've only changed two things.

Only redirect requests towards the homepage

I want users who type hegedus.me or click on a link pointing to the homepage to be redirected to the corresponding language version.

I'm not sure any other kind of redirect is in my interest as most content on the site is only available in one language. Either HU or EN. Exceptions are the language-specific home pages, /blog and /about — those of which are available in both English and Hungarian.

You can filter requests towards a specific URL with this: (http.request.full_uri eq "https://hegedus.me/")

It may be worth noting, that Cloudflare process multiple kind of redirects in a specific order.

screenshot of Cloudflare admin showing the different kinds of redirects and the order they are processed

Since I've previously set up Page Rules to redirect three of the domain versions (http and both www) to https://hegedus.me/ and these are processed before the Redirect Rules, I can be sure that this is the URL going to hit the Redirect Rules.

Use English as a fallback language

I turned over the filters a bit. Of the two languages, HU is a bit more special. Content written in HU is clearly targeted towards HU speakers. While the content in EN is for everyone worldwide.

Therefore, I can say that if the browser's Accept-Language is "hu", redirect to /hu/. If it is anything but "hu", redirect to /en/. And really, this is the gist of it.

So my first rule is this, which redirects to hegedus.me/hu/

(http.request.full_uri eq "https://hegedus.me/") and (starts_with(http.request.accepted_languages[0],"hu"))

And my second rule is this, which redirects to hegedus.me/en/

(http.request.full_uri eq "https://hegedus.me/") and (not starts_with(http.request.accepted_languages[0],"hu"))

There may be an even simpler and more elegant approach, but it works. And we all know: it is not stupid if it works.

Pro tips

You can also target locales

If other than languages you also want to target locales, you can use for example "fr-CA" or "es-VE". Do note that using a simple "es" targets every single locale associated with the Spanish language.

Redirect Cloudflare Pages' pages.dev domain

If your website is hosted on Cloudflare Pages (like this one) then you automatically get a .pages.dev subdomain also serving your site.

The same content being available from different URLs are generally not a positive thing but thankfully you can redirect the .pages.dev subdomain with Bulk Redirects.

Anyway, I've learned something new, which is the whole point of redoing this website.

Sources: