yum upgrades for production use, this is the repository for you.
Active subscription is required.
Technical Briefing: NGINX Accept-Language Module — Header-Based Language Detection
The Problem
Stock NGINX cannot parse the Accept-Language header. Language-based redirects therefore typically require a map block full of regexes, an njs script, or a full OpenResty/Lua layer.
Real headers are messy:
fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5
They include regional subtags, quality values, spaces, and wildcards — all painful to match correctly.
The Fix
The NGINX Accept-Language module provides a single directive, set_from_accept_language, shipped as the prebuilt nginx-module-accept-language package. No Lua, njs, or regex hacks are needed.
Directive Usage
set_from_accept_language $lang en fr de;
You supply a variable name and the list of languages your site supports. The module walks the header left to right and performs a case-insensitive prefix match, so fr-CA or fr-CH matches fr. The first matching tag wins.
Behavior Caveats
- The module ignores
q=weights and trusts header order. This matches mainstream browser behavior, which already sends languages in preference order. - If there is no
Accept-Languageheader or nothing matches,$langbecomes the first language listed — so put your default language first. - Output is always one of your listed values, so
$langis safe inreturn,error_page, or logging; untrusted header input never reaches URLs.
Installation
RPM
sudo dnf install https://extras.getpagespeed.com/release-latest.rpm
sudo dnf install nginx-module-accept-language
Installing also pulls the latest stable NGINX from the same repo, upgrading a stock 1.28.x setup to current 1.30.x in the same transaction.
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/Ubuntu
Set up the GetPageSpeed APT repository, then:
sudo apt-get update
sudo apt-get install nginx-module-accept-language
The package handles module loading automatically — no load_module directive needed.
Basic i18n Configuration
Language trees live under /en/, /fr/, /de/; only the exact site root redirects. Declare the variable, then in location = / add the Vary header and the redirect:
set_from_accept_language $lang en fr de;
location = / {
add_header Vary Accept-Language always;
return 302 /$lang/;
}
Deep links like /fr/pricing are never rewritten, so shared URLs keep working regardless of browser locale.
Verified Behavior (Rocky Linux 10, NGINX 1.30.5)
Accept-Language header |
Result |
|---|---|
fr-FR,fr;q=0.9,en;q=0.8 |
/fr/ |
de-CH |
/de/ (regional subtag matched by prefix) |
es-ES,es;q=0.9 |
/en/ (unsupported falls back to first listed) |
| (no header) | /en/ |
pt;q=0.9, de;q=0.8 |
/de/ (pt skipped, next tag wins) |
Cookie Override
Auto-detection is a first-visit convenience, not a cage. A stock map combines cleanly with the module’s variable to let an explicit stored choice win:
map $cookie_site_lang $lang_final {
default $lang_detected;
en en;
fr fr;
de de;
}
The map whitelists cookie values, so a tampered cookie falls through to header detection. Verified: site_lang=de with a French header → /de/; bogus site_lang=xx → /fr/.
Audience Logging
Declare the variable once at the http level and add it to a log format:
log_format i18n '$remote_addr "$request" $status "$http_accept_language" lang=$lang';
access_log /var/log/nginx/i18n.log i18n;
Sample 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 a given language tree is worth building.
Why map Falls Short
map tests the whole header string once. 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.
Why Not njs/Lua
They parse correctly but require an entire scripting runtime, extra packages, and code you maintain and security-patch yourself. The module is a compiled 68 KB binary with zero configuration surface.
Performance
The module registers a variable handler, so it runs only when $lang is actually evaluated, doing a single linear pass with plain case-insensitive comparisons — no regex engine, no interpreter, no allocations in the match path. On requests that never touch $lang, cost is exactly zero.
SEO Rules
- Send
Vary: Accept-Languageon the redirect response so shared caches and CDNs store one redirect per language. - Add
hreflangannotations on language trees, includingx-defaultpointing at the root redirector. - Redirect only
location = /— Googlebot crawls mostly withoutAccept-Languageand must reach every language tree directly.
Troubleshooting
unknown directive "set_from_accept_language"onnginx -t: module not loaded. On RPM systems, confirm theload_moduleline is at the very top ofnginx.conf, above theeventsandhttpblocks.variable already defined: "lang": the same variable name is declared in more than one place (e.g., twoserverblocks). Each variable is registered once for the whole configuration — declare it a single time at thehttplevel.- Regional variants need ordering care: a supported
zhalso catcheszh-TWvisitors. List the specific variant before the generic one, e.g.set_from_accept_language $lang en zh-TW zh;, and remember the fallback is always the first entry. - Behind a CDN: confirm the edge honors
Vary: Accept-Languageon the root URL, or exclude/from edge caching — otherwise the first visitor’s language gets cached and served to everyone.
Ecosystem Notes
nginx-module-accept-language is one of 140+ NGINX modules prebuilt, security-patched, and rebuilt against every NGINX release in the GetPageSpeed Premium Repository, with packages for major RHEL-compatible, Debian, and Ubuntu releases.
GetPageSpeed Amplify runs scheduled gixy scans across every host and ties findings to live NGINX runtime metrics; it is drop-in compatible with the deprecated nginx-amplify-agent (EOL January 2026).
Module source and issue tracker: dvershinin/nginx_accept_language_module on GitHub.
Read the full article: NGINX Accept-Language Redirects: No Lua Required
