30

How to rewrite location in nginx depending on the client-browser's language?

For example: My browser accept-language is 'uk,ru,en'. When I request location mysite.org nginx must forward to mysite.org/uk

mario
  • 138,064
  • 18
  • 223
  • 277
RKI
  • 383
  • 2
  • 4
  • 10

10 Answers10

28

You can manage $language_suffix by this setting when you cannot add AcceptLanguageModule module into your system.

rewrite (.*) $1/$http_accept_language

A more resilient approach would use a map:

map $http_accept_language $lang {
        default en;
        ~es es;
        ~fr fr;
}

...

rewrite (.*) $1/$lang;
gagarine
  • 3,840
  • 25
  • 34
Brian Coca
  • 297
  • 3
  • 4
  • 1
    Actually there shouldn't be a space between the ~ and the expression. – snøreven Oct 09 '12 at 20:41
  • 1
    Does this require the AcceptLanguageModule ? – Max L. Sep 24 '13 at 14:36
  • 4
    Not working for me, it is always bringing me to the english page even if I configure my browser for french only – Simon Rolin Oct 13 '13 at 13:59
  • 1
    this doesn't require AcceptLanguageModule – gagarine Jul 02 '14 at 13:47
  • 1
    Does this solution honour numerical preferences in the `Accept-Language` header? (See [this answer](http://stackoverflow.com/a/25065143/20578).) – Paul D. Waite Mar 26 '15 at 22:43
  • 5
    This mapping does not honor the preferences. To imporove this mapping you can change the regex from `~es es;` to `~*^es es;` and this is going to map the language only to the first language appearing on the `Accept-Language` header (assumming the browser delivers the prefered language first, wich I think is very frequent) – JorgeGarza May 27 '16 at 22:39
  • 1
    Where should we write that map part? Inside the http section? – indra257 Oct 15 '19 at 17:52
13

The downside of using AcceptLanguageModule is you cannot rely on automatic system updates anymore. And with every nginx update (even security one), you have to compile Nginx yourself. The second downside is that module assumes that the accept-language is sorted by quality values already. I rather prefer Lua because it can be installed easily in debian based distros:

apt-get install nginx-extras

My colleague Fillipo made great nginx-http-accept-lang script in Lua. It correctly handles quality values and does redirect user accordingly. I've made small modification to that script. It accepts supported languages as input parameter and returns the most qualified language according to Accept-Language header. With returned value you can do whatever you want. It can be used for rewrites, setting lang cookie ...

I'm only using language determination for root path only (location = /). And user lang cookie has preference over browser. My nginx conf looks like this:

map $cookie_lang $pref_lang {
    default "";
    ~en en;
    ~sk sk;
}

server {
    listen 80 default_server;

    root /usr/share/nginx/html;
    index index.html index.htm;

    # Make site accessible from http://localhost/
    server_name localhost;

    location = / {
        # $lang_sup holds comma separated languages supported by site
        set $lang_sup "en,sk";
        set_by_lua_file $lang /etc/nginx/lang.lua $lang_sup;
        if ($pref_lang) {
            set $lang $pref_lang;
        }
        add_header Set-Cookie lang=$lang;
        rewrite (.*) $scheme://$server_name/$lang$1;
    }

    location / {
        # First attempt to serve request as file, then
        # as directory, then fall back to displaying a 404.
        try_files $uri $uri/ =404;
   }
}
mauron85
  • 1,084
  • 12
  • 22
  • 1
    this is quite nice, but why not make a PR to the original script? – Valerio Sep 17 '15 at 15:16
  • 1
    Inspired by this solution I came up with my own that supports get parameters and cookies. You can try it out here: https://github.com/mallocator/nginx-lua-lang – Mallox Apr 06 '16 at 22:15
12

I think it's not good idea to use nginx map $http_accept_language because it does not honor quality value (q in Accept-Language header). Let's imagine you have:

map $http_accept_language $lang {
    default en;
    ~en en;
    ~da da;
}

And client will send Accept-Language: da, en-gb;q=0.8, en;q=0.7

Using nginx map will always map $lang to en because it simply find in header string. But correct mapping will be $lang = da (because Danisch has quality value q=1 which is bigger then English q=0.7 in this case) More on this in RFC: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html

ConcurrentHashMap
  • 4,692
  • 6
  • 38
  • 51
mauron85
  • 1,084
  • 12
  • 22
6

Okay, I've had the same problem and "misuse" Lua to make a redirect possible based on the browser language.

# Use Lua for HTTP redirect so the site works
# without the proxy backend.
location = / {
    rewrite_by_lua '
        for lang in (ngx.var.http_accept_language .. ","):gmatch("([^,]*),") do
            if string.sub(lang, 0, 2) == "en" then
                ngx.redirect("/en/index.html")
            end
            if string.sub(lang, 0, 2) == "nl" then
                ngx.redirect("/nl/index.html")
            end
            if string.sub(lang, 0, 2) == "de" then
                ngx.redirect("/de/index.html")
            end
        end
        ngx.redirect("/en/index.html")
    ';
}

Note: NGINx needs to have liblua compiled to it. For Debian/Ubuntu:

apt-get install nginx-extras
Mark
  • 61
  • 1
  • 3
  • Is it possible to check also if the url is ends with `mysite.org` ? – famas23 May 28 '19 at 15:46
  • This will throw an internal server error if http_accept_language is empty, if anyone wants to use this, be sure to add a check for `nil` on `ngx.var.http_accept_language` at the first line of the lua script. – TiEul Nov 30 '20 at 23:49
2

I know this is a very old thread, but I found it when trying to solve the same problem. Just wanted to share the solution I finally came with. It is different to the ones published above as if there are several languages mentioned in the Accept-Language, it will pick the first mentioned among the ones we can serve.

    #
    # Determine what language to redirect to
    # this sets the value of $lang variable to the language depending on the contents of Accept-Language request header
    # the regexp pattern automatically matches a known language that is not preceded by another known language
    # If no known language is found, it uses some heuristics (like RU for (uk|be|ky|kk|ba|tt|uz|sr|mk|bg) languages)
    #
    map $http_accept_language $lang {
        default en;
        "~*^((|,)\s*(?!(ru|es|fr|pt|en))\w+(-\w+)?(;q=[\d\.]+)?)*(|,)\s*en\b" en;
        "~*^((|,)\s*(?!(ru|es|fr|pt|en))\w+(-\w+)?(;q=[\d\.]+)?)*(|,)\s*es\b" es;
        "~*^((|,)\s*(?!(ru|es|fr|pt|en))\w+(-\w+)?(;q=[\d\.]+)?)*(|,)\s*ru\b" ru;
        "~*^((|,)\s*(?!(ru|es|fr|pt|en))\w+(-\w+)?(;q=[\d\.]+)?)*(|,)\s*fr\b" fr;
        "~*^((|,)\s*(?!(ru|es|fr|pt|en))\w+(-\w+)?(;q=[\d\.]+)?)*(|,)\s*pt\b" pt;
        "~*(^|,)\s*(uk|be|ky|kk|ba|tt|uz|sr|mk|bg)\b" ru;
        "~*(^|,)\s*(ca|gl)\b" es;
    }

    ...

    rewrite (.*) $1/$lang;

The limitation of this solution is that it assumes the languages in the Accept-Language header are listed in the order of their preference. Usually this is true, but it is not officially required. For example, if the header is "Accept-Language: da, en-US;q=0.1, pt-BR;q=1", the variable $lang will be set to "en" because it comes before "pt" even though pt has larger weight.

Choosing the right language taking into account all the weights does not seem to be possible in nginx without external scripts. This solution was good enough for me in all practical cases and it did not require any external modules.

  • > This solution was good enough for me in all practical cases and it did not require any external modules. While true, who wants to wrestle with this kind of regular expression hell? – wh81752 May 16 '19 at 16:59
  • @whaefelinger Well, I know who wants, I do. :-) All good solutions mentioned here require installing some extras to nginx. I feel installing anything in addition to nginx is an overkill for such a small task. So I spent half an hour and came up with a regexp that solves the problem. I thought why not share it, maybe it could help anyone else who does not consider regular expressions a hell? – Sergey Gershtein May 18 '19 at 11:20
  • > _[..] who does not consider regular expressions a hell?_ :-) That's not exactly what I wrote. I just imagine a poor fellow who must must support tons of languages and then something is wrong (besides that your solution assumes descending order). – wh81752 May 19 '19 at 12:18
  • Btw, adding LUA is easy and well supported. LUA has a tiny footprint and is very fast. So LUA appears to be a good candidate. However, regex support is poor in standard LUA. A 3rd party lib is required in addition. Then integration in NGINX may turn nasty. However, I understand that LUA is officially replaced with JavaScript. – wh81752 May 19 '19 at 12:29
  • @whaefelinger, Yes, I agree that supporting tons of languages with my approach won't probably be a good idea. However, for my 5 languages I'm going to stick with regexp instead of installing LUA just for this task. – Sergey Gershtein May 20 '19 at 13:29
  • @SergeyGershtein If I switch to a different language that is not listed here, then it goes to the previous language instead of english. – indra257 Nov 18 '19 at 18:15
  • @indra257 these lines mean to be applied only once. In my setup these lines only work for the root location. The root location fires the redirect to the chosen language and the visitor stays there. – Sergey Gershtein Nov 20 '19 at 05:00
1

Simple solution, without MapModule and AcceptLanguageModule :

   if ( $http_accept_language ~ ^(..) ) {
         set $lang $1;
   }
   set $args hl=$lang&$args;

Note that the "set $args hl=$lang&$args" sets the desired language code (eg. "en", "fr", "es", etc) in the "hl" query parameter. Of course you can use $lang in other rewriting rules if the query parameter does not fit. Example:

location ~/my/dir/path/ {
          rewrite ^/my/dir/path/ /my/dir/path/$1/ break;
          proxy_pass http://upstream_server;
   }
1

Lua example above is fine, but will fail with error 500 if browser does not send any Accept-Language headers.

Add this on top of it:

if ngx.var.http_accept_language == nil then
ngx.redirect("/en/")
end
Dom
  • 1,591
  • 5
  • 26
  • 32
Eugene V
  • 103
  • 6
0

You can use nginx_accept_language_module. Nginx has to be recompiled but its less work than integrating Lua.

Link to github

Dmytro
  • 171
  • 1
  • 7
0

In addition to @Marks answer above which does not honor language preferences. Here's a LUA chunk of code parsing Accept-Language Header value into language and preference value

-- need two LUA regex cause LUA's default regex is pretty broken
-- In my opinion a killer argument against using / supporting LUA

rx = "%s*([a-zA-Z-]+)%s*;%s*q%s*=%s*(%d*.?%d+)"
rx2 = "%s*([a-zA-Z-]+)%s*"

-- (arg .. ",") => concatenation operation
for chunk in (arg .. ","):gmatch("([^,]*),") do
    lang, q = string.match(chunk, rx)
    if (not lang) then
        lang = string.match(chunk, rx2)
        q = 1.0
    end
    print(string.format("lang=[%s] q=[%s]",lang, tonumber(q * 1.0)))
end

When applying, I'm getting:

$ lua demo.lua 'en-US , de , fr ; q = 0.1 , dk;q=1 '
lang=[en-US] q=[1.0]
lang=[de] q=[1.0]
lang=[fr] q=[0.1]
lang=[dk] q=[1.0]

$ lua demo.lua ' de'
lang=[de] q=[1.0]

$ lua demo.lua ' de;'
lang=[de] q=[1.0]

$ lua demo.lua ' de;q'
lang=[de] q=[1.0]

$ lua demo.lua ' de;q='
lang=[de] q=[1.0]

$ lua demo.lua ' de;q=0'
lang=[de] q=[0.0]

$ lua demo.lua ' de;q=0.1'
lang=[de] q=[0.1]

Eventually I'm using than a LUA script like below to redirect:

rx = "%s*([a-zA-Z-]+)%s*;%s*q%s*=%s*(%d*.?%d+)"
rx2 = "%s*([a-zA-Z-]+)%s*"


sup = {de = 0, en = 0, dk = 0}       -- supported languages
win = {lang = "en", q = 0}           -- default values / winner

for chunk in (arg[1] .. ","):gmatch("([^,]*),") do
    lang, q = string.match(chunk, rx)
    if (not lang) then
        lang = string.match(chunk, rx2)
        q = 1.0
    end
    lang = string.lower(lang)
    -- handle only supported languages
    if (sup[lang]) then
        q = tonumber(q)
        -- update winner table if a better match is found
        if (win.q < q) then
            win.q = q
            win.lang = lang
        end
    end
end

-- which language pref?
print(string.format("winner: %s",win.lang))

This gives:

$ lua test.lua 'en-US;q=.7 , de;q=0.9 , fr ; q = 0.1 , dk ; q  =  1 '
winner: dk
wh81752
  • 751
  • 8
  • 16
-2

So here's the compiled example for the original question (based on my case verified to work with nginx-1.1.x):

map $http_accept_language $lang {
    default en;
    ~ru ru;
    ~uk uk;
}

server {
    server_name mysite.org;
    # ...
    rewrite (.*) http://mysite.org/$lang$1;
}
Michael Shigorin
  • 804
  • 9
  • 11