Reputation: 13891
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
Reputation: 1
According to Angular 17+
There are few edits needed for nginx and angular NGINX Edits
http {
# Browser preferred language detection (does NOT require
# AcceptLanguageModule)
map $http_accept_language $accept_language {
~*^de de;
~*^fr fr;
~*^en en;
}
# ...
}
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;
}
# ...
}
"projects": {
"angular.io-example": {
// ...
"i18n": {
"sourceLocale": "en-US",
"locales": {
"fr": {
"translation": "src/locale/messages.fr.xlf",
"baseHref": ""
}
}
},
"architect": {
// ...
}
}
}
// ...
}
Enjoy!
Upvotes: 0
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
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
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:
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;
}
}
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.
Angular CLI: 6.0.7
Node: 8.11.2
Angular: 6.0.3
Upvotes: 10
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
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