{"id":24518641,"url":"https://github.com/nbusseneau/nginx-static-language-selector","last_synced_at":"2026-05-16T23:09:17.978Z","repository":{"id":70902705,"uuid":"242600287","full_name":"nbusseneau/NGINX-Static-Language-Selector","owner":"nbusseneau","description":"Lua script for NGINX Lua module making NGINX aware of client language preferences retrieved from multiple sources.","archived":false,"fork":false,"pushed_at":"2020-03-23T21:50:02.000Z","size":6,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-03-15T11:22:17.402Z","etag":null,"topics":["accept-language","language","language-selector","language-switcher","localization","nginx","nginx-lua","static"],"latest_commit_sha":null,"homepage":"","language":"Lua","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/nbusseneau.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-02-23T22:21:59.000Z","updated_at":"2021-10-09T16:10:03.000Z","dependencies_parsed_at":"2023-02-23T23:15:46.435Z","dependency_job_id":null,"html_url":"https://github.com/nbusseneau/NGINX-Static-Language-Selector","commit_stats":null,"previous_names":[],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/nbusseneau/NGINX-Static-Language-Selector","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nbusseneau%2FNGINX-Static-Language-Selector","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nbusseneau%2FNGINX-Static-Language-Selector/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nbusseneau%2FNGINX-Static-Language-Selector/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nbusseneau%2FNGINX-Static-Language-Selector/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/nbusseneau","download_url":"https://codeload.github.com/nbusseneau/NGINX-Static-Language-Selector/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nbusseneau%2FNGINX-Static-Language-Selector/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33121861,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-16T18:38:32.183Z","status":"ssl_error","status_checked_at":"2026-05-16T18:38:29.903Z","response_time":115,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["accept-language","language","language-selector","language-switcher","localization","nginx","nginx-lua","static"],"created_at":"2025-01-22T01:46:25.558Z","updated_at":"2026-05-16T23:09:17.916Z","avatar_url":"https://github.com/nbusseneau.png","language":"Lua","funding_links":[],"categories":[],"sub_categories":[],"readme":"# NGINX Static Language Selector\n\nLua script for NGINX Lua module (`ngx_http_lua_module`), making NGINX aware of client language preferences retrieved from multiple sources (query parameter, cookie value, `Accept-Language` HTTP header), according to a list of supported IETF BCP 47 language tags for a specific website.\n\n## Intent\n\n- Serving localized static content directly from NGINX.\n- Handle client preferences for all of the following cases:\n  - New client sessions.\n  - Recurring client sessions.\n  - Clients explicitly asking for a specific language.\n- Loose matching of regional language codes with regular language codes (or vice versa) when there is no exact match.\n\n## Use case\n\nAny type of static multilingual website where NGINX would be able to serve/redirect/rewrite clients to the right localized subdirectory or file if client language preferences were known.\n\nExamples of static multilingual websites structures:\n```\nsubdirectories_example\n├── shared\n|   └── style.css\n├── en\n|   └── index.html\n└── fr\n    └── index.html\n\nfilenames_example\n├── style.css\n├── index.html.en\n└── index.html.fr\n```\n\nHandling examples of a client accessing `example.com` with `fr` as language preference:\n- Transparently serve localized `index.html.fr` without redirect nor rewrite\n- Redirect or rewrite to `example.com/fr`\n- Redirect or rewrite to `example.com/?lang=fr`\n- Redirect or rewrite to `fr.example.com`\n\n## Language preferences matching from multiple sources\n\n### Algorithm\n\n- Initial state: a list of supported languages consisting of IETF BCP 47 language tags (2-3 letter-long tags separated by `-`, e.g. `en`, `en-US`, `fr`, `fr-FR`) is provided.\n\n- We check for client language preferences from the following sources, in order:\n  - Query parameter\n  - Cookie value\n  - `Accept-Language` HTTP header\n\n  All these sources are expected to be formatted according to the [`Accept-Language` header format](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language), e.g. `en-US,en;q=0.8,fr-FR;q=0.5,fr;q=0.3`, and thus also consist of IETF BCP 47 language tags.\n\n- For each source\n  - Each language tag in source is compared with the list of supported languages. If an exact match is found, return it.\n\n  - If no exact match can be found, each language tag in source is again compared with the list of supported languages. If a loose match between regional language codes and regular language codes is found, e.g. `en-US` with `en`, `fr` with `fr-FR`, return it.\n\n  - If no loose match can be found, the script processes the next source.\n\n- If no match has been found after all sources have been processed, the script defaults to the first supported language specified in the list.\n\nSample results:\n\n| Query parameter | Cookie value | `Accept-Language`  header | Supported  | Result  |\n|-----------------|--------------|---------------------------|------------|---------|\n|                 |              | `en-US,fr`                | `en,fr`    | `fr`    |\n|                 | `en-US`      | `en-US,fr`                | `en,fr`    | `en`    |\n| `fr-FR`         | `en-US`      | `en-US,fr`                | `en,fr`    | `fr`    |\n|                 |              | `en`                      | `en-US,fr` | `en-US` |\n|                 | `fr-FR`      | `en`                      | `en-US,fr` | `fr`    |\n| `en`            | `fr-FR`      | `en`                      | `en-US,fr` | `en-US` |\n|                 |              | `de-DE`                   | `en,fr`    | `en`    |\n|                 | `fr-FR`      | `de-DE`                   | `en,fr`    | `fr`    |\n\n### Analysis\n\nWe assume that the static localized website has `{switch language}` buttons that users may click to manually switch language.\n\n- Query parameter: very easy to use from client-side for switching language, e.g. in `a.href` of `{switch language}` buttons. It is explicit and instantaneous: if a client asks for a page with `?lang=fr`, the server is immediately 100% aware of client preferences. However, it is not suitable for remembering client preferences between sessions, as it is not persistent (unless the client bookmarks it).\n\n- Cookie value: easy to store from client-side, e.g. in an `onclick` handler for `{switch language}` buttons. It is suitable for remembering client preferences between sessions. However, cookie storage is handled by the browser and the we have no direct control over it. It is neither instantaneous nor trustable: there can be a delay between `onclick` handler asking for a cookie to be stored and actual cookie storage, and maybe no cookie will actually be stored. Thus, it is not suitable on its own for switching language: if a user clicks a `{switch language}` button, redirection may occur before the cookie value is actually set, in which case the server will not be aware of client preferences.\n\n- `Accept-Language` HTTP header: in the specific case of static websites without dynamic localization, it is not suitable for switching language from client-side without an HTTP redirection. It is not suitable for remembering client preferences between sessions since we have no control over it.\n\n### Priority\n\nGiven [Analysis](#analysis) and [Intent](#intent):\n- New client sessions should rely on `Accept-Language` HTTP header, as it is the only source available.\n- Recurring client sessions should rely on cookie value, as it is the only persistent source we can control.\n- Clients explicitly asking for a specific language should rely on query parameter, as it is the only reliable source that can be set from client-side.\n\nHence why we should always check sources in the following order:\n- Query parameter: explicit client preference.\n- Cookie value: persistent client preference.\n- `Accept-Language` HTTP header: default client preference.\n\n## Installation\n\n- Make sure NGINX Lua module (`ngx_http_lua_module`) is installed and enabled. On Debian/Ubuntu, this can be done by installing `nginx-extras`.\n- [Download the latest release](https://github.com/Skymirrh/NGINX-Static-Language-Selector/releases/latest) - or - [download `language_selector.lua` directly from `master`](https://raw.githubusercontent.com/Skymirrh/NGINX-Static-Language-Selector/master/language_selector.lua)\n- Put `language_selector.lua` at a `{path}` your NGINX installation will be able to read, e.g. `/etc/nginx/lua/language_selector.lua`.\n- Remember this `{path}` for [Usage](#usage).\n\n## Usage\n\nSyntax (within a NGINX `server` block):\n```nginx\nset_by_lua_file {$lang} {path}/language_selector.lua {$supported};\n```\nwhere:\n- `path` is the path leading to `language_selector.lua` (see [Installation](#installation)).\n- `$supported` is a comma-separated list of languages supported by the website, in IETF BCP 47 format. Example: `en-US,en,fr-FR,fr`\n- `$lang` holds a single value from the `$supported` list, selected according to client preferences. This variable can then be used however you see fit, e.g. `index` directive, `location` blocks, rewrite rules.\n\nExample with the `filenames_example` from [Use case](#use-case), transparently serving a localized `index.html` when clients visit `example.com`:\n```nginx\nserver {\n  server_name example.com;\n\n  ...\n\n  # Language selector\n  set $supported \"en,fr\";\n  set_by_lua_file $lang /etc/nginx/lua/language_selector.lua $supported;\n\n  # Files\n  root /var/www/filenames_example;\n  location / {\n    index index.html.$lang;\n  }\n\n  ...\n}\n```\n\n## Client-side cookie storage\n\nNow that the server is fully aware of client language preferences, the client-side can take advantage of it to:\n- Allow users to switch language manually.\n- Store client preferences in a persistent manner.\n\nHere are some examples of `{switch language}` buttons that you may implement client-side, so that users may manually switch language and at the same time store their preferences in a cookie.\n\n### JavaScript\n```html\n\u003ca id=\"switchLangFr\" href=\"?lang=fr\" onClick=\"switchLang('fr');\"\u003eFrench\u003c/a\u003e\n\n\u003cscript type=\"text/javascript\"\u003e\n  function switchLang(lang) {\n    document.cookie = \"lang=\"+lang;\n  }\n\u003c/script\u003e\n```\n\n### jQuery\n```html\n\u003ca id=\"switchLangFr\" href=\"?lang=fr\"\u003eFrench\u003c/a\u003e\n\n\u003cscript type=\"text/javascript\"\u003e\n  $(document).ready(function () {\n    $('#switchLangFr').click(function() { document.cookie = \"lang=fr\"; });\n  });\n\u003c/script\u003e\n```\n\n## Server-side cookie storage\n\nClient preferences can also be persisted in a cookie server-side, though I do not recommend this approach.\n\nIt is much more elegant and less error-prone to set cookies directly from client-side at the same time a user manually switches language, rather than server-side when the server checks for client preferences and realizes an explicit language has been asked by the client.\n\n### PHP\n\n```php\n\u003c?php\nif(isset($_GET['lang']))\n{\n  setcookie('lang', $_GET['lang']);\n}\n?\u003e\n\n\u003ca id=\"switchLangFr\" href=\"?lang=fr\"\u003eFrench\u003c/a\u003e\n```\n\n### NGINX\n\n```nginx\nserver {\n  ...\n  \n  if ($arg_lang) {\n    add_header Set-Cookie lang=$arg_lang;\n  }\n\n  ...\n}\n```","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnbusseneau%2Fnginx-static-language-selector","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnbusseneau%2Fnginx-static-language-selector","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnbusseneau%2Fnginx-static-language-selector/lists"}