NGINX / Server Setup

How to Serve Localized NGINX Error Pages Based on Browser Language

by ,


We have by far the largest RPM repository with NGINX module packages and VMODs for Varnish. If you want to install NGINX, Varnish, and lots of useful performance/security software with smooth yum upgrades for production use, this is the repository for you.
Active subscription is required.

When users encounter a 404 “Page Not Found” or 500 “Server Error” page, showing them an error message in their preferred language significantly improves the user experience. This comprehensive guide demonstrates how to configure NGINX to serve localized error pages based on the browser’s Accept-Language header, providing a more user-friendly experience for your international visitors.

Why Use NGINX Localized Error Pages?

Serving error pages in the user’s native language offers several key benefits:

  • Improved User Experience: Users can understand what went wrong without language barriers
  • Lower Bounce Rates: Visitors are more likely to stay on your site when they can read error messages
  • Professional Appearance: Demonstrates attention to detail and consideration for international audiences
  • Better SEO: Reduces user frustration signals that search engines may factor into rankings
  • Brand Trust: Users feel more confident when they see content in their language

According to various studies, over 72% of internet users prefer browsing websites in their native language. A localized error page tells your users: “We care about your experience.”

Understanding the Accept-Language Header

When a browser makes a request, it sends an Accept-Language HTTP header that indicates the user’s preferred languages. This header typically looks like:

Accept-Language: ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7

This example indicates:
ru-RU (Russian, Russia) is the most preferred (implicit q=1.0)
ru (Russian) has a quality value of 0.9
en-US (English, US) has a quality value of 0.8
en (English) has a quality value of 0.7

The q parameter (quality value) ranges from 0 to 1, where higher values indicate stronger preference.

Two Methods for Detecting Browser Language

NGINX provides two effective approaches for parsing the Accept-Language header:

Method 1: Using the map Directive (No Modules Required)

The simplest approach uses NGINX’s built-in map directive with regular expressions. This method works with any standard NGINX installation and requires no additional modules.

Method 2: Using the Accept-Language Module (More Accurate)

For more accurate language detection that properly handles quality values, you can use the nginx-module-accept-language module available from the GetPageSpeed repository.

Let’s explore both methods in detail.

Method 1: NGINX Localized Error Pages Using the map Directive

This approach works with any NGINX installation and provides reliable language detection for most use cases.

Step 1: Create the Language Mapping

Add this configuration to the http block of your nginx.conf:

http {
    # Parse Accept-Language header to determine user's preferred language
    # The first matching pattern wins
    map $http_accept_language $lang {
        default en;           # English as fallback
        ~*^ru    ru;         # Russian
        ~*^es    es;         # Spanish
        ~*^de    de;         # German
        ~*^fr    fr;         # French
        ~*^zh    zh;         # Chinese
        ~*^pt    pt;         # Portuguese
        ~*^ja    ja;         # Japanese
        ~*^ko    ko;         # Korean
        ~*^it    it;         # Italian
        ~*^nl    nl;         # Dutch
        ~*^pl    pl;         # Polish
        ~*^ar    ar;         # Arabic
    }

    # ... rest of your http configuration
}

The ~*^ru pattern means:
~* – case-insensitive regular expression
^ru – matches strings starting with “ru”

Step 2: Configure Error Pages with Language Variable

In your server block, configure the error_page directive to use the $lang variable:

server {
    listen 80;
    server_name example.com;
    root /var/www/html;

    # Localized error pages using the $lang variable
    error_page 404 /errors/$lang/404.html;
    error_page 500 502 503 504 /errors/$lang/50x.html;

    location / {
        try_files $uri $uri/ =404;
    }

    # Internal location for serving error pages
    location /errors/ {
        internal;
        root /var/www/html;
        # Fallback to English if the localized page doesn't exist
        try_files $uri /errors/en/404.html =500;
    }
}

Key configuration points:

  • error_page with variables: The NGINX error_page directive supports variables in the URI, allowing dynamic path construction
  • internal directive: Ensures error pages can only be accessed through internal redirects, not directly by users
  • try_files fallback: If a translation doesn’t exist, falls back to English

Step 3: Create the Error Page Directory Structure

Create the directory structure for your localized error pages:

# Create directories for each supported language
mkdir -p /var/www/html/errors/{en,ru,es,de,fr,zh,pt,ja,ko,it,nl,pl,ar}

Step 4: Create Localized Error Page Files

Create the HTML files for each language. Here’s an example structure:

English (/var/www/html/errors/en/404.html):

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>404 - Page Not Found</title>
    <style>
        body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; 
               display: flex; justify-content: center; align-items: center; 
               height: 100vh; margin: 0; background: #f5f5f5; }
        .container { text-align: center; padding: 40px; }
        h1 { color: #333; font-size: 72px; margin: 0; }
        p { color: #666; font-size: 18px; }
        a { color: #007bff; text-decoration: none; }
    </style>
</head>
<body>
    <div class="container">
        <h1>404</h1>
        <p>The page you're looking for doesn't exist.</p>
        <p><a href="/">Return to Homepage</a></p>
    </div>
</body>
</html>

Russian (/var/www/html/errors/ru/404.html):

<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>404 - Страница не найдена</title>
    <style>/* Same styles as above */</style>
</head>
<body>
    <div class="container">
        <h1>404</h1>
        <p>Запрашиваемая страница не найдена.</p>
        <p><a href="/">Вернуться на главную</a></p>
    </div>
</body>
</html>

Spanish (/var/www/html/errors/es/404.html):

<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>404 - Página no encontrada</title>
    <style>/* Same styles as above */</style>
</head>
<body>
    <div class="container">
        <h1>404</h1>
        <p>La página que buscas no existe.</p>
        <p><a href="/">Volver a la página principal</a></p>
    </div>
</body>
</html>

Step 5: Test the Configuration

Verify your NGINX configuration syntax:

nginx -t

If successful, reload NGINX:

nginx -s reload

Test with different Accept-Language headers using curl:

# Test English (default)
curl -s -H "Accept-Language: en-US,en;q=0.9" http://localhost/nonexistent

# Test Russian
curl -s -H "Accept-Language: ru-RU,ru;q=0.9" http://localhost/nonexistent

# Test Spanish
curl -s -H "Accept-Language: es-ES,es;q=0.9" http://localhost/nonexistent

# Test fallback (unsupported language falls back to English)
curl -s -H "Accept-Language: ja-JP,ja;q=0.9" http://localhost/nonexistent

Method 2: Using the Accept-Language Module

For more sophisticated language detection that properly handles quality values and complex Accept-Language headers, use the nginx-module-accept-language module.

Installation on Rocky Linux, AlmaLinux, CentOS, RHEL

Install the module from the GetPageSpeed repository:

# Install the GetPageSpeed repository
dnf -y install https://extras.getpagespeed.com/release-latest.rpm

# Install the accept-language module
dnf -y install nginx-module-accept-language

Enable the Module

Add the following at the top of your /etc/nginx/nginx.conf:

load_module modules/ngx_http_accept_language_module.so;

Configure with set_from_accept_language

The module provides the set_from_accept_language directive:

load_module modules/ngx_http_accept_language_module.so;

http {
    server {
        listen 80;
        server_name example.com;
        root /var/www/html;

        # Use the module to detect language
        # List supported languages in order of preference
        set_from_accept_language $lang en ru es de fr zh pt ja;

        # Localized error pages
        error_page 404 /errors/$lang/404.html;
        error_page 500 502 503 504 /errors/$lang/50x.html;

        location / {
            try_files $uri $uri/ =404;
        }

        location /errors/ {
            internal;
            root /var/www/html;
            try_files $uri /errors/en/404.html =500;
        }
    }
}

The set_from_accept_language directive:
– Parses the full Accept-Language header
– Returns the best matching language from your supported list
– Falls back to the first language in the list if no match is found
– The first parameter ($lang) is the variable to store the result
– Subsequent parameters are the languages your site supports

Advantages of the Module Approach

Using the nginx-module-accept-language offers several benefits over the map approach:

  1. Proper Quality Value Handling: The module correctly interprets q values to select the best matching language
  2. Simpler Configuration: One directive instead of multiple map patterns
  3. More Accurate Matching: Handles complex Accept-Language headers more reliably
  4. Maintained Code: The module is actively maintained and tested

Complete Configuration Example

Here’s a production-ready configuration combining best practices:

load_module modules/ngx_http_accept_language_module.so;

user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /run/nginx.pid;

events {
    worker_connections 1024;
}

http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" "$http_accept_language" lang=$lang';

    access_log /var/log/nginx/access.log main;

    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;

    server {
        listen 80;
        listen [::]:80;
        server_name example.com www.example.com;
        root /var/www/html;

        # Detect user's preferred language
        set_from_accept_language $lang en ru es de fr zh pt ja ko it;

        # Custom error pages based on language
        error_page 400 /errors/$lang/400.html;
        error_page 401 /errors/$lang/401.html;
        error_page 403 /errors/$lang/403.html;
        error_page 404 /errors/$lang/404.html;
        error_page 500 502 503 504 /errors/$lang/50x.html;

        # Main application
        location / {
            try_files $uri $uri/ /index.php$is_args$args;
        }

        # Error pages location (internal only)
        location /errors/ {
            internal;
            root /var/www/html;
            # Comprehensive fallback chain
            try_files $uri /errors/en/$uri /errors/en/404.html =500;
        }

        # Optional: Add language header for debugging
        add_header X-Detected-Language $lang;
    }
}

Localized Error Pages for Reverse Proxy Setups

When using NGINX as a reverse proxy, you need to intercept errors from the upstream server:

server {
    listen 80;
    server_name example.com;

    set_from_accept_language $lang en ru es de fr;

    location / {
        proxy_pass http://backend_server;
        proxy_intercept_errors on;  # Intercept upstream errors

        # Handle upstream errors with localized pages
        error_page 500 502 503 504 /errors/$lang/50x.html;
        error_page 404 /errors/$lang/404.html;
    }

    location /errors/ {
        internal;
        root /var/www/html;
        try_files $uri /errors/en/404.html =500;
    }
}

The proxy_intercept_errors on directive tells NGINX to handle error responses from the upstream server using the configured error_page directives.

Best Practices for Localized Error Pages

1. Always Provide a Fallback

Never assume a translation exists. Either ensure all translation exist or always configure try_files with an English fallback:

try_files $uri /errors/en/404.html =500;

2. Keep Error Pages Lightweight

Error pages should load quickly even under server stress:
– Use inline CSS instead of external stylesheets
– Avoid JavaScript dependencies
– Minimize images or use inline SVG
– Keep total page size under 50KB

3. Include Helpful Information

Good error pages should include:
– A clear explanation of what happened
– A link to the homepage
– A search box (if applicable)
– Contact information for support

4. Log the Detected Language

Add the detected language to your access log for analytics:

log_format main '$remote_addr - $lang - "$request" $status';

5. Test All Languages

After deployment, test each language:

for lang in en ru es de fr zh; do
    echo "Testing $lang:"
    curl -s -H "Accept-Language: $lang" http://localhost/nonexistent | head -1
done

Troubleshooting Common Issues

Error Pages Not Displaying in Correct Language

Problem: Users see English error pages regardless of their browser language.

Solutions:
1. Check that the map or set_from_accept_language directive is in the correct context (http block for map)
2. Verify the $http_accept_language variable is populated: add it to your access log
3. Ensure pattern matching is correct (case-insensitive ~*)

500 Error Instead of Custom Error Page

Problem: Server returns a generic 500 error instead of the custom page.

Solutions:
1. Verify error page files exist and have correct permissions:
bash
ls -la /var/www/html/errors/en/
chmod 644 /var/www/html/errors/en/*.html

2. Check the try_files fallback path exists
3. Review NGINX error logs: tail -f /var/log/nginx/error.log

Module Not Loading

Problem: NGINX fails to start with module error.

Solutions:
1. Verify module is installed: ls /usr/lib64/nginx/modules/ngx_http_accept_language_module.so
2. Ensure load_module is at the very top of nginx.conf, before any other directives
3. Check NGINX version compatibility with the module

Performance Considerations

The map directive and the accept-language module both have minimal performance impact:

  • Map directive: Evaluated once per request, stored in a hash table
  • Accept-language module: Lightweight parsing with O(n) complexity where n is the number of supported languages

For high-traffic sites, both methods handle millions of requests per second without measurable overhead.

Conclusion

Serving localized NGINX error pages is a straightforward enhancement that significantly improves the user experience for international visitors. Whether you use the built-in map directive for simplicity or the nginx-module-accept-language for more accurate language detection, the implementation requires minimal configuration changes.

Remember to:
– Create error pages for all your supported languages
– Always provide an English fallback
– Keep error pages lightweight and helpful
– Test thoroughly with different Accept-Language headers
– Monitor your access logs to understand your language distribution

By following this guide, you’ll create a more inclusive and professional web experience for users worldwide, potentially reducing bounce rates and improving overall satisfaction with your website.

D

Danila Vershinin

Founder & Lead Engineer

NGINX configuration and optimizationLinux system administrationWeb performance engineering

10+ years NGINX experience • Maintainer of GetPageSpeed RPM repository • Contributor to open-source NGINX modules

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes:

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

This site uses Akismet to reduce spam. Learn how your comment data is processed.