2

I have an angular application, that I've localized in multiple languages (let's say english, and french).

I would like to serve different locale version of the app depending on the url:

  • myapp.io/en/account
  • myapp.io/en/settings
  • myapp.io/fr/account
  • ...

I tried to follow the steps in the official documentation, with no luck.

So this is my nginx config:


http {
  # Browser preferred language detection (does NOT require
  # AcceptLanguageModule)
  map $http_accept_language $accept_language {
    ~*^en en;
    ~*^fr fr;
  }

  server {
    listen 4200;
    server_name localhost;

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

    add_header Access-Control-Allow-Origin "*";
    add_header Access-Control-Allow-Methods "GET, POST, PATCH, PUT, DELETE, OPTIONS";
    add_header Access-Control-Allow-Headers "DNT, User-Agent, X-Requested-With, If-Modified-Since, Cache-Control, Content-Type, Range, Origin, Accept";
    add_header Access-Control-Expose-Headers "Content-Length,Content-Range";

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

    # 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 ~ ^/(en|fr) {
      try_files $uri $uri/ /index.html;
    }

    gzip on;
    gzip_min_length 1000;
    gzip_proxied expired no-cache no-store private auth;
    gzip_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript;
  }
}

... and this is how I've configured the router:

const routes: Routes = [
  {
    path: "account",
    component: AccountComponent,
  },
  {
    path: "settings",
    component: SettingsComponent,
  },
  {
    path: "",
    redirectTo: "account",
    pathMatch: "full",
  },
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

At this point I can visit to https://myapp.io/en/. I can navigate the website, and reach https://myapp.io/en/account. If I press reload at this point I get this nginx error:

Screenshot error

Does anyone know what I am doing wrong? Or can anyone point to an open source working example, or a good tutorial?

Update

I've changed the nginx configuration as suggested here; this fixed the problem in the screenshot, but still a problem remains: when I try to load the host (https://myapp.io/) I get redirected to https://myapp.io:4200/en/

Update 2

Fixed changing this block:

location = / {
  try_files $uri /$accept_language/index.html;
}

1 Answers1

3

You didn't follow the official documentation correctly. At least you missed the language code in the last try_files directive argument:

location ~ ^/(en|it)/ {
    try_files $uri /$1/index.html;
}

The next part will be my considerations about the aforementioned nginx example from Angular official documentation, which I'm find to be of very poor quality (I mean the nginx example only, not the whole documentation).

There isn't any sense to use ?$args suffix in the last try_files directive parameter as suggested in that example:

try_files $uri /$1/index.html?$args;

The angular app will continue to see query arguments already present (or not present) in the browser address bar, so that suffix does nothing but an extra CPU cycles spent by nginx on variable interpolation. That suffix makes sense when the request should in turn be processed with some PHP script, but this isn't the case here.

I don't see any sensible reason to use a regex here:

if ($accept_language ~ "^$") {
    set $accept_language "it";
}

Much more performant exact matching can be used here instead:

if ($accept_language = "") {
    set $accept_language "it";
}

And even that one isn't really needed if you add the default item to your map block:

map $http_accept_language $accept_language {
    ~*^en en;
    ~*^it it;
    default it;
}

However it won't work if supported language won't be specified as the very first one. Imagine a request header like Accept-Language: de;q=0.9, en;q=0.8. A default Italian language will be selected for this user while even not being specified in the preferred language list (rather than English one). If you want to make it considering the languages precedence set by user in his browser settings, for two supported languages you can use the following:

map $http_accept_language $accept_language {
    ~en.*it(*SKIP)(*F)|it  it; # when "it" substring present and not preceded with "en"
    ~it.*en(*SKIP)(*F)|en  en; # when "en" substring present and not preceded with "it"
    default                it;
}

(Regex provided by Wiktor Stribiżew)

Three languages, considering the precedence order, will require the following map block:

map $http_accept_language $accept_language {
    ~(?:en|fr).*it(*SKIP)(*F)|it  it;
    ~(?:fr|it).*en(*SKIP)(*F)|en  en;
    ~(?:en|it).*fr(*SKIP)(*F)|fr  fr;
    default                       it;
}

and so on.

Instead of

rewrite ^/$ /$accept_language permanent;

I'd rather use (again, for the efficiency)

location = / {
    return 301 /$accept_language/;
}

Moreover, I don't think the 301 permanent redirection code is the one that should be used here at all. If by any means user will change his languages preferences, he won't receive an app page according to his new language settings since this redirect will already get cached by his browser. I think the 302 temporary redirection will be more appropriate here:

location = / {
    return 302 /$accept_language/;
}

And for the same reason of efficiency I'd also consider to split main location block in two (or more, depending on supported languages count):

location /en/ {
    try_files $uri /en/index.html;
}
location /it/ {
    try_files $uri /it/index.html;
}
...

Nginx is not that kind of thing where DRY principles are always for good. Another advantage of such a configuration will be an ability to use it with the alias directive without additional workarounds (if you'd try to use that try_files $uri /$1/index.html; with the angular app directory defined using an alias directive rather than root one, you'd face one of well-known side effects described in this nginx trac ticket).

Ivan Shatsky
  • 13,267
  • 2
  • 21
  • 37
  • This was already of great help; I've updated my question - any chance you could have a look, and help me understanding this remaining problem? Thank you – Random Bruno May 24 '22 at 06:02
  • This is not a problem, but a behavior by design, both configurations (angular official example and mine fixed one) will behave this way. I think the solution can be to build one default language configuration (lets say an English one) without the language prefix, but I don't know for sure how it can be done, my angular knowledge is limited. – Ivan Shatsky May 24 '22 at 07:36