Ahmed Siouani
Ahmed Siouani

Reputation: 13891

Nginx configuration for angular i18n application

I built an angular-5 application using i18n that supports both french and english. I then deployed a separate version of the app for each supported language

 - dist
    |___ en/
    |    |__ index.html
    |___ fr/
         |__ index.html

I also added the following nginx configuration to serve the application in both languages;

server {
    root /var/www/dist;
    index index.html index.htm;
    server_name host.local;

    location ^/(fr|en)/(.*)$ {
        try_files $2 $2/ /$1/index.html;
    }
}

What I wanted to do is to serve both applications and allow switching between the english and the french version.

Let's say for example I'm on host.local/en/something if I switch to host.local/fr/something I should get the french version of the "something" page.

With the nginx configuration I shared, I get a 404 not found error every time I refresh pages when browing my apps which also prevents me from browsing my apps independently from one another and switching between them.

What did I miss? What's the appropriate Nginx conf to achieve that?

Upvotes: 15

Views: 16767

Answers (6)

TimyShark
TimyShark

Reputation: 1

According to Angular 17+

Deploy Multiple locales

There are few edits needed for nginx and angular NGINX Edits

  1. Edit /etc/nginx/nginx.conf

    http {
    # Browser preferred language detection (does NOT require
    # AcceptLanguageModule)
    map $http_accept_language $accept_language {
        ~*^de de;
        ~*^fr fr;
        ~*^en en;
    }
    # ...
}
  1. Edit /etc/nginx/sites-available/mysite

server {
listen 80;
server_name localhost;
root /www/data;

# Fallback to default language if no preference defined by browser
if ($accept_language ~ "^$") {
    set $accept_language "fr";
}

# Redirect "/" to Angular application in the preferred language of the browser
rewrite ^/$ /$accept_language permanent;

# Everything under the Angular application is always redirected to Angular in the
# correct language
location ~ ^/(fr|de|en) {
    try_files $uri /$1/index.html?$args;
}
# ...

}

  1. Edit angular.json

"projects": {
"angular.io-example": {
  // ...
  "i18n": {
    "sourceLocale": "en-US",
    "locales": {
      "fr": {
        "translation": "src/locale/messages.fr.xlf",
        "baseHref": ""
      }
    }
  },
  "architect": {
    // ...
  }
  }
  }
  // ...
  }
  1. All links href=".." should be relative ; must not start with "/"
  2. build : ng build --localize, on the browser http://localhost/en-US

Enjoy!

Upvotes: 0

dyingangel666
dyingangel666

Reputation: 103

This is my solution to solve this for multiple projects:

nginx.conf

http {
    server {

        # Sets our default language (it's the angular template default language)
        set $defaultLang "de";

        listen 80;

        root /usr/share/nginx/html;
        index index.html;
        include /etc/nginx/mime.types;

        ################## IMPORTANT (don't change this) ##################

        # Make sure when routing to location, server uses the correct angular project subfolder
        # Matches the following urls:
        # http://localhost/de
        # http://localhost/de/
        # http://localhost/de/login
        # http://localhost/notexist/login => In this case, try_files doesn't found a matching index.html and jumps into the @languageFallback
        location ~ "^(/([a-z]{2,2})/)(/?.*)?$" {
          try_files $uri $uri /$2/index.html @languageFallback;
        }

        # Make sure when routing to the root the root index is used (and we redirect through the small JS script -> redirect.js)
        # Matches the following urls:
        # http://localhost
        # http://localhost/
        location / {
          try_files $uri $uri/ /index.html;
        }

        # Language fallback which is used when user tries to open a language which doesn't exist
        # E.g When user trying to open http://localhost/notexist/login but it doesnt exist, then we rewrite the url to
        # http://localhost/de/login
        location @languageFallback {
          rewrite "^(/([a-z]{2,2})/)(/?.*)?$" $scheme://$http_host/$defaultLang/$3 last;
        }
    }
}

Then i have an additional index.html with that small script inside, which is copied to the nginx root:

additional index.html

<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <meta content="IE=edge" http-equiv="X-UA-Compatible"/>
  <title></title>
  <script>
    (function()
    {
      let redirectUrl;
      const supportedLanguages = ['de', 'en'];
      const fallbackLanguage = 'de';

      // Read browser locale and use this as default language (only when no locale in localstorage was found)
      let locale = (navigator.language || navigator['userLanguage']).slice(0, 2);
      const storedLocale = localStorage.getItem('locale');

      console.info('BROWSER LOCALE: ', locale);
      console.info('STORED LOCALE: ', storedLocale);

      //Check if a locale was already set in localstorage and use this or set the default language by default
      //and browsers locale is not supported by app we fallback to the fallback language
      if (!storedLocale)
      {
        if (supportedLanguages.indexOf(locale) === -1)
        {
          locale = fallbackLanguage;
        }
      }
      else
      {
        locale = storedLocale;
      }

      redirectUrl = location.origin + '/' + locale + '/';
      console.info('REDIRECT TO: ', redirectUrl);

      // Redirect to correct language
      location.replace(redirectUrl);
    })();

  </script>
</head>
<body>
</body>
</html>

This file is used to route a user to the correct language subproject depending on its browser language or when the user change the language within the application it's stored in the localstorage and this language is used with higher priority.

And my angular.json

{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "version": 1,
  "newProjectRoot": "",
  "projects": {
    "my-project": {
      "i18n": {
        "sourceLocale": {
          "code": "de",
          "baseHref": "/"
        },
        "locales": {
          "en": {
            "translation": "src/locales/messages.en.xlf",
            "baseHref": "/"
          }
        }
      },
      "root": "",
      "sourceRoot": "src",
      "projectType": "application",
      "prefix": "sg",
      "schematics": {
        "@schematics/angular:component": {
          "style": "scss"
        }
      },
      "architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:browser",
          "options": {
            "aot": true,
            "outputHashing": "all",
            "outputPath": "dist/my-project",
            "resourcesOutputPath": "assets/fonts",
            "baseHref": "/",
            "index": "src/index.html",
            "main": "src/main.ts",
            "polyfills": "src/polyfills.ts",
            "tsConfig": "tsconfig.app.json",
            "assets": [
              "src/assets"
            ],
            "styles": [
              "src/styles.scss"
            ],
            "scripts": [],
            "stylePreprocessorOptions": {
              "includePaths": [
                "src/app"
              ]
            }
          },
          "configurations": {
            "production": {
              "optimization": true,
              "sourceMap": false,
              "extractCss": true,
              "namedChunks": false,
              "extractLicenses": true,
              "vendorChunk": false,
              "buildOptimizer": true,
              "fileReplacements": [
                {
                  "replace": "src/environments/environment.ts",
                  "with": "src/environments/environment.prod.ts"
                }
              ],
              "i18nMissingTranslation": "error",
              "budgets": [
                {
                  "type": "initial",
                  "maximumWarning": "2mb",
                  "maximumError": "5mb"
                },
                {
                  "type": "anyComponentStyle",
                  "maximumWarning": "6kb"
                }
              ]
            },
            "dev": {
              "budgets": [
                {
                  "type": "anyComponentStyle",
                  "maximumWarning": "6kb"
                }
              ],
              "i18nMissingTranslation": "error"
            }
          }
        },
        "serve": {
          "builder": "@angular-devkit/build-angular:dev-server",
          "options": {
            "browserTarget": "my-project:build"
          },
          "configurations": {
            "dev": {
              "browserTarget": "my-project:build:dev"
            }
          }
        },
        "extract-i18n": {
          "builder": "@angular-devkit/build-angular:extract-i18n",
          "options": {
            "browserTarget": "my-project:build"
          }
        },
        "test": {
          "builder": "@angular-devkit/build-angular:karma",
          "options": {
            "main": "src/test.ts",
            "polyfills": "src/polyfills.ts",
            "tsConfig": "tsconfig.spec.json",
            "karmaConfig": "karma.conf.js",
            "styles": [
              "src/styles.scss"
            ],
            "scripts": [],
            "assets": [
              "src/assets"
            ]
          }
        },
        "lint": {
          "builder": "@angular-devkit/build-angular:tslint",
          "options": {
            "tsConfig": [
              "tsconfig.app.json",
              "tsconfig.spec.json"
            ],
            "exclude": [
              "**/node_modules/**"
            ]
          }
        },
        "xliffmerge": {
          "builder": "@ngx-i18nsupport/tooling:xliffmerge",
          "options": {
            "xliffmergeOptions": {
              "srcDir": "src/locales",
              "genDir": "src/locales",
              "i18nFile": "messages.xlf",
              "i18nBaseFile": "messages",
              "i18nFormat": "xlf",
              "encoding": "UTF-8",
              "defaultLanguage": "de",
              "languages": [
                "en"
              ],
              "removeUnusedIds": true,
              "supportNgxTranslate": false,
              "ngxTranslateExtractionPattern": "@@|ngx-translate",
              "useSourceAsTarget": true,
              "targetPraefix": "",
              "targetSuffix": "",
              "beautifyOutput": true,
              "allowIdChange": false,
              "autotranslate": false,
              "apikey": "",
              "apikeyfile": "",
              "verbose": false,
              "quiet": false
            }
          }
        }
      }
    }
  },
  "defaultProject": "my-project"
}

And finally some important npm script for that stuff from my package.json

"build": "ng build --prod --localize",
"i18n": "ng xi18n --format=xlf --output-path=src/locales --out-file=messages.xlf",
"xliffmerge": "ng run my-project:xliffmerge",
"translate": "npm run i18n; npm run xliffmerge"

Upvotes: 5

lardum
lardum

Reputation: 66

Angular 9 has a new option to build all language versions at once. It also sets base HREF for each version of the application by adding the locale to the configured baseHref.

ng build --prod --localize

Then you need to copy all builds to nginx serve directory

COPY /dist/my-app/ /usr/share/nginx/my-app/

And configure nginx as shown in the previous answers.

Upvotes: 5

nyxz
nyxz

Reputation: 7438

After building with:

ng build --prod --base-href /fr/ --output-path dist/fr
ng build --prod --base-href /en/ --output-path dist/en

copy the builds to nginx serve directory:

cp -r dist/* /usr/share/nginx/my-app

Then I found 2 different NGINX configs that work for me:

Using root path

server {
    root /usr/share/nginx/my-app;
    location /en/ {
        autoindex on;
        try_files $uri$args $uri$args/ /en/index.html;
    }

    location /fr/ {
        autoindex on;
        try_files $uri$args $uri$args/ /fr/index.html;
    }

    # Default to FR
    location / {
        # Autoindex is disabled here + the $uri$args/ is missing from try_files
        try_files $uri$args /fr/index.html;
    }
}

Using alias

server {
    listen 80 default_server;
    index index.html;

    location /en/ {
        alias /usr/share/nginx/my-app/en/;
        try_files $uri$args $uri$args/ /en/index.html;
    }

    location /fr/ {
        alias /usr/share/nginx/my-app/fr/;
        try_files $uri$args $uri$args/ /fr/index.html;
    }

    # Default to FR
    location / {
        alias /usr/share/nginx/my-app/fr/;
        try_files $uri$args $uri$args/ /fr/index.html;
    }
}

Note: In the root path solution you can remove the autoindex on option but you'll also have to remove the $uri$args/ part from the try_files or else you'll get "directory index of "[directory]" is forbidden error".

FYI: You can find useful those nice explanations on ROOT vs ALIAS.

Versions

Angular CLI: 6.0.7
Node: 8.11.2
Angular: 6.0.3

Upvotes: 10

Fateh Mohamed
Fateh Mohamed

Reputation: 21397

i've done the same thing on iis, first you have to build your app with "base-href" option:

 ng build --output-path=dist/fr --prod --bh /fr/
 ng build --output-path=dist/en --prod --bh /en/

and for nginx use this config

location /fr/ {
  alias /var/www/dist/fr/;
  try_files $uri$args $uri$args/ /fr/index.html;
}
location /en/ {
 alias /var/www/dist/en/;
 try_files $uri$args $uri$args/ /en/index.html;
}

and for navigation from /en/someroute to /fr/someroute , you can get the current router url in your component where you have the language switcher

getCurrentRoute() {
    return this.router.url;
}

and when click on a language selector you redirect to the same route with the selected language :

 <li *ngFor="let language of languages;let i=index" >
    <a  href="/{{language.lang}}/#{{getCurrentRoute()}}"  (click)="changeLanguage(language.lang)">
        {{language.lang}}
    </a>
 </li>

change language method

changeLanguage(lang: string) {
    const langs = ['en', 'fr'];
    this.languages = this.allLanguages.filter((language) => {
        return language.lang !== lang;
    });
    this.curentLanguage = this.allLanguages[langs.indexOf(lang)].name
    localStorage.setItem('Language', lang);
    if (isDevMode()) {
        location.reload(true);
    }
}

Upvotes: 27

cnst
cnst

Reputation: 27258

There is a common misunderstanding in the way that http://nginx.org/r/try_files works. If you look closer in the docs (from the above link), you'll notice that although the first and intermediate parameters in the try_files directive are of type "file", the final one is called the "uri"; which, in your case, once you fix your http://nginx.org/r/location to handle the regexp appropriately (your location code is missing the ~ modifier), may result in an infinite loop, as you confirm in your comments.

Note that generally, the regular expressions are not recommended to be used in nginx for maximum performance in simple situations where regex use might as well be avoided, so, I'd recommend you to have two independent locations, one each for English and French.

location /fr/ {
    try_files $uri /fr/index.html =410;
}
location /en/ {
    try_files $uri /en/index.html =410;
}

Note that the above code assumes that you perform correct URL handling from within your webapp frontend itself — if a given resource is shared between the /en/ and /fr/ versions, then it would be requested directly via the / base, without a /en/ or /fr/ specifier. The =410 part of the code behaves similarly to how =404 would, except that the error is slightly different to make it easier to debug which directive is responsible for the error.

Upvotes: 0

Related Questions