yum upgrades for production use, this is the repository for you.
Active subscription is required.
Your site speaks three languages, but your NGINX config does not. Stock NGINX has no built-in way to parse the Accept-Language header, so the practical fix is the NGINX Accept-Language module: its set_from_accept_language directive, shipped as the prebuilt nginx-module-accept-language package, picks the best supported language for every visitor in one line of configuration, without Lua, njs, or regex hacks.
If you have ever searched for “nginx accept-language redirect”, you have seen the workarounds: a map block with a pile of regexes, an njs script, or a whole OpenResty/Lua layer bolted on just to read one request header. All of that exists because the header is genuinely annoying to parse by hand. A real-world value looks like this:
Accept-Language: fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5
Regional subtags, quality values, spaces, wildcards. A regex that handles all of it correctly stops being a one-liner very quickly. Meanwhile, all you actually want is: “this visitor prefers French, my site has /fr/, send them there.”
This guide shows how to do exactly that with the NGINX accept-language module: install the package, add one directive, and redirect first-time visitors to /en/, /fr/ or /de/ based on their browser preference. Every configuration below was tested end to end on a clean Rocky Linux 10 server with NGINX 1.30.
How the NGINX Accept-Language Module Works
The module provides a single directive. You give it a variable name and the list of languages your site actually supports:
set_from_accept_language $lang en fr de;
On each request, the module walks the visitor’s Accept-Language header from left to right and compares every language tag against your supported list. The comparison is a case-insensitive prefix match, therefore a browser sending fr-CA or fr-CH matches your fr just fine. The first header tag that matches any supported language wins, and $lang is set to that supported language.
Two behaviors are worth knowing precisely, because they differ from what some tutorials claim:
- Header order decides, not quality values. The module discards
q=weights and trusts the order of the header. In practice this is the right trade-off: every mainstream browser already sends languages in preference order, so the leftmost tag is the preferred one anyway. - The first language in your list is the fallback. When the visitor sends no
Accept-Languageheader at all, or nothing in it matches,$langbecomes the first language you listed. Put your default language first.
Because the output can only ever be one of the values you listed, $lang is safe to use in return, error_page, or logging. Untrusted header input never reaches your URLs.
Installing nginx-module-accept-language
The module is packaged and continuously rebuilt against current NGINX by GetPageSpeed for all major Linux distributions.
RHEL, CentOS, AlmaLinux, Rocky Linux, Amazon Linux
sudo dnf install https://extras.getpagespeed.com/release-latest.rpm
sudo dnf install nginx-module-accept-language
Installing the module also brings in the latest stable NGINX from the same repository, so a stock 1.28.x setup is upgraded to the current 1.30.x in the same transaction. Then load the module at the very top of /etc/nginx/nginx.conf, above any other directive:
load_module modules/ngx_http_accept_language_module.so;
Debian and Ubuntu
First, set up the GetPageSpeed APT repository, then install:
sudo apt-get update
sudo apt-get install nginx-module-accept-language
On Debian/Ubuntu, the package handles module loading automatically. No
load_moduledirective is needed.
Package details for each platform live on the module pages: RPM-based systems and Debian/Ubuntu.
Directive Reference
| Directive | Syntax | Context |
|---|---|---|
set_from_accept_language |
set_from_accept_language $variable lang1 [lang2 ...]; |
http, server, location |
The variable name is yours to choose. One important rule: each variable name may be declared only once in the entire configuration. Declare it at the http level when several server blocks need it (more on that in Troubleshooting).
Configuration 1: Redirect Visitors by Browser Language
The classic i18n setup: language trees under /en/, /fr/ and /de/, and the site root decides where a new visitor lands.
server {
listen 80;
server_name example.com;
root /var/www/i18n;
# Pick the best supported language; first one (en) is the fallback
set_from_accept_language $lang en fr de;
location = / {
add_header Vary Accept-Language always;
return 302 /$lang/;
}
location / {
try_files $uri $uri/ =404;
}
}
Only the exact site root redirects. Deep links like /fr/pricing are never rewritten, so shared URLs keep working for everyone regardless of their browser locale.
Verified behavior on Rocky Linux 10 with NGINX 1.30.5:
curl -s -o /dev/null -w "%{redirect_url}\n" -H "Accept-Language: fr-FR,fr;q=0.9,en;q=0.8" http://localhost/
# http://localhost/fr/
curl -s -o /dev/null -w "%{redirect_url}\n" -H "Accept-Language: de-CH" http://localhost/
# http://localhost/de/ (regional subtag matched by prefix)
curl -s -o /dev/null -w "%{redirect_url}\n" -H "Accept-Language: es-ES,es;q=0.9" http://localhost/
# http://localhost/en/ (unsupported language falls back to the first listed)
curl -s -o /dev/null -w "%{redirect_url}\n" http://localhost/
# http://localhost/en/ (no header at all falls back too)
curl -s -o /dev/null -w "%{redirect_url}\n" -H "Accept-Language: pt;q=0.9, de;q=0.8" http://localhost/
# http://localhost/de/ (pt is skipped, the next tag wins)
Configuration 2: Let an Explicit Choice Beat Auto-Detection
Auto-detection is a first-visit convenience, not a cage. When a visitor clicks your language switcher, store the choice in a cookie and let it override the header. A stock map combines cleanly with the module’s variable:
# An explicit choice stored in a cookie wins over header detection
map $cookie_site_lang $lang_final {
default $lang_detected;
en en;
fr fr;
de de;
}
server {
listen 80;
server_name example.com;
root /var/www/i18n;
set_from_accept_language $lang_detected en fr de;
location = / {
add_header Vary Accept-Language always;
return 302 /$lang_final/;
}
location / {
try_files $uri $uri/ =404;
}
}
The map whitelists cookie values, so a tampered cookie falls through to header detection instead of reaching the redirect. Verified:
# Cookie beats the header
curl -s -o /dev/null -w "%{redirect_url}\n" \
-H "Accept-Language: fr-FR,fr;q=0.9" -H "Cookie: site_lang=de" http://localhost/
# http://localhost/de/
# A bogus cookie value is ignored
curl -s -o /dev/null -w "%{redirect_url}\n" \
-H "Accept-Language: fr-FR,fr;q=0.9" -H "Cookie: site_lang=xx" http://localhost/
# http://localhost/fr/
Configuration 3: Log the Detected Language
Before rolling out language trees, it pays to know your actual audience split. Declare the variable once at the http level and add it to a log format:
# In the http block (e.g. /etc/nginx/conf.d/00-lang.conf)
set_from_accept_language $lang en fr de;
log_format i18n '$remote_addr "$request" $status '
'"$http_accept_language" lang=$lang';
Then use access_log /var/log/nginx/i18n.log i18n; in any server. Sample of the verified output:
127.0.0.1 "GET / HTTP/1.1" 302 "de-DE,de;q=0.9,en;q=0.5" lang=de
127.0.0.1 "GET / HTTP/1.1" 302 "pt-BR" lang=en
A week of this log tells you whether that Portuguese tree is worth building.
What About map, njs, or Lua?
NGINX’s built-in map directive can approximate language detection with regexes like ~*^fr fr;. That works while the very first header tag is one you support. It falls apart on real headers: map tests the whole header string once, so for Accept-Language: pt;q=0.9, de;q=0.8 a regex map anchored at the start sees pt, matches nothing, and serves the default even though the visitor also accepts German. The module walks all tags and gets this case right.
We use the map approach ourselves where it fits; our guide to localized NGINX error pages shows both methods side by side for the error-page use case. This article’s site-wide redirects are exactly where the map shortcut breaks down and the module earns its place.
The njs and Lua routes solve the parsing correctly, but at the cost of an entire scripting runtime, extra packages, and code you now maintain and security-patch yourself. For one request header, that is a lot of machinery. A compiled 68 KB module with zero configuration surface is the boring, right-sized tool.
Performance
The NGINX accept-language module registers a variable handler, which means it runs only when $lang is actually evaluated, and it does a single linear pass over the header with plain case-insensitive comparisons. No regex engine, no interpreter, no allocations in the match path. On requests that never touch $lang, the cost is exactly zero. This is as close to free as request-time logic gets in NGINX.
SEO Best Practices for Language Redirects
Language redirects sit on your most-visited URL, so a few rules keep search engines happy:
- Use 302, not 301. The redirect target depends on who is asking. A 301 is cacheable and may be reused for the next visitor with different preferences, and Google treats a permanent redirect on the root as a signal to index the target instead.
- Send
Vary: Accept-Languageon the redirect response, as the configs above do, so shared caches and CDNs store one redirect per language rather than one for everyone. - Keep
hreflangannotations on your language trees, includingx-defaultpointing at the root redirector. The redirect handles humans;hreflanghandles crawlers. - Never redirect deep URLs. Match
location = /only. Googlebot crawls mostly withoutAccept-Languageand must be able to reach every language tree directly. - Honor explicit choices with the cookie override from Configuration 2. Auto-detection guessing wrong once is fine; guessing wrong on every visit is how you lose readers.
Troubleshooting
unknown directive "set_from_accept_language" on nginx -t: the module is not loaded. On RPM-based systems, confirm the load_module line sits at the very top of nginx.conf, above the events and http blocks:
nginx: [emerg] unknown directive "set_from_accept_language" in /etc/nginx/conf.d/00-lang.conf:1
variable already defined: "lang": the same variable name is declared in more than one place, for example in two server blocks:
nginx: [emerg] variable already defined: "lang" in /etc/nginx/conf.d/site2.conf:7
Each set_from_accept_language variable is registered once for the whole configuration. Declare it a single time at the http level and every server block can use it.
Regional variants need ordering care. The prefix match means a supported zh also catches zh-TW visitors. If you maintain separate Simplified and Traditional Chinese trees, list the specific variant before the generic one, for example set_from_accept_language $lang en zh-TW zh;, and remember that the fallback is always the first entry.
Behind a CDN? Confirm your edge honors Vary: Accept-Language on the root URL, or exclude / from edge caching. Otherwise the first visitor’s language gets cached and served to everyone.
Get the Module
nginx-module-accept-language is one of 140+ NGINX modules prebuilt, security-patched, and rebuilt against every NGINX release in the GetPageSpeed Premium Repository. One subscription covers every module on all your servers, with packages for every major RHEL-compatible, Debian, and Ubuntu release. Subscribe and install in under a minute.
Conclusion
A working nginx-module-accept-language setup only stays correct until the next config edit. Language redirects sit at your busiest entry point, and a careless change there can quietly break caching or misroute every international visitor. GetPageSpeed Amplify runs scheduled gixy scans across every host and ties findings to live NGINX runtime metrics. Drop-in compatible with the deprecated nginx-amplify-agent (EOL January 2026).
Browser-language redirects in NGINX do not require Lua, njs, or a fragile regex map. The nginx accept-language module reduces the whole problem to one tested directive: install the package, declare your supported languages, and redirect the root. The Accept-Language header does the rest, with an explicit cookie override for visitors who know better than their browser settings.
Module source and issue tracker: dvershinin/nginx_accept_language_module on GitHub.
