Implement i18n support in frontend using Angular + Transifex + NGINX

This PR adds basic i18n support into the mempool frontend, together with
a smooth workflow for developers and translators to collaborate:

* Using the existing @angular/localize module, developers add i18n
metadata to any frontend strings their new features or changes modify

* Using the new npm script `i18n-extract-from-source`, developers
extract the i18n data from source code into `src/locale/messages.xlf`

* After pushing the updated `src/locale/messages.xlf` to GitHub, the
Transifex service will update its database from the new source data

* Using the Transifex website UI, translators can work together to
translate all the mempool frontend strings into their native languages

* Using the new npm script `i18n-pull-from-transifex`, developers can
pull in completed translations from Transifex, and commit them into git.

This flow requires an API key from Transifex, which can be obtained at
https://www.transifex.com/user/settings/api/ to be used with the python
script installed by `pip install transifex-client` - after preparing
these, run the npm script which will ask you for the API key the first
time. When downloading is complete, you can test building the frontend,
and if successful, commit the new strings files into git.

This PR implements a new locale selector in the footer of the homepage
dashboard, and includes WIP translations for the following languages:

* Czech (cs)
* German (de)
* Japanese (ja)
* Norwegian (nn)
* Spanish (es)
* Swedish (sv)
* Ukrainian (uk)
* Persian (fa)
* Portugese (pt)
* Turkish (tr)
* Dutch (nl)
* French (fr)
* Chinese (zh)
* Slovenian (sl)
* Korean (ko)
* Polish (pl)

The user-agent's `Accept-Language` header is used to automatically
detect their preferred language, which can be manually overriden by the
pull-down selector, which saves their preference to a cookie, which is
used by nginx to serve the correct HTML bundle to the user.

Remaining tasks include adding i18n metadata for strings in the Bisq and
Liquid frontend code, mouseover hover tooltip strings, hard-coded og
metadata inside HTML templates, and many other places. This will be done
in a separate PR.

When upgrading to add i18n support, mempool instance operators must take
care to install the new nginx.conf and nginx-mempool.conf files, and
tweak for their specific site configuration.

Fixes #81
This commit is contained in:
wiz
2020-12-02 04:19:33 +09:00
parent f151eb81c8
commit 4658b47007
60 changed files with 41826 additions and 451 deletions

View File

@@ -0,0 +1,195 @@
root /mempool/public_html/mainnet/;
index index.html;
add_header Onion-Location http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion$request_uri;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload";
set $frameOptions "DENY";
set $contentSecurityPolicy "frame-ancestors 'none'";
if ($http_referer ~ ^https://mempool.space/)
{
set $frameOptions "ALLOW-FROM https://mempool.space";
set $contentSecurityPolicy "frame-ancestors https://mempool.space";
}
if ($http_referer ~ ^https://mempool.ninja/)
{
set $frameOptions "ALLOW-FROM https://mempool.ninja";
set $contentSecurityPolicy "frame-ancestors https://mempool.ninja";
}
if ($http_referer ~ ^https://node100.bitcoin.wiz.biz/)
{
set $frameOptions "ALLOW-FROM https://node100.bitcoin.wiz.biz";
set $contentSecurityPolicy "frame-ancestors https://node100.bitcoin.wiz.biz";
}
if ($http_referer ~ ^https://wiz.biz/)
{
set $frameOptions "ALLOW-FROM https://wiz.biz";
set $contentSecurityPolicy "frame-ancestors https://wiz.biz";
}
add_header X-Frame-Options $frameOptions;
add_header Content-Security-Policy $contentSecurityPolicy;
# fallback
location / {
#return 302 https://mempool.space/$request_uri;
try_files /$lang/$uri /$lang/$uri/ $uri $uri/ /en-US/$uri /$lang/index.html /en-US/index.html =404;
}
# location block using regex are matched in order
# used to rewrite resources from /<lang>/ to /en-US/
location ~ ^/(ar|bg|bs|ca|cs|da|de|et|el|es|eo|eu|fa|fr|gl|ko|hr|id|it|he|ka|lv|lt|hu|mk|ms|nl|ja|no|nb|nn|pl|pt|pt-BR|ro|ru|sk|sl|sr|sh|fi|sv|th|tr|uk|vi|zh)/resources/ {
rewrite ^/[a-zA-Z-]*/resources/(.*) /en-US/resources/$1;
}
# used for cookie override
location ~ ^/(ar|bg|bs|ca|cs|da|de|et|el|es|eo|eu|fa|fr|gl|ko|hr|id|it|he|ka|lv|lt|hu|mk|ms|nl|ja|no|nb|nn|pl|pt|pt-BR|ro|ru|sk|sl|sr|sh|fi|sv|th|tr|uk|vi|zh)/ {
try_files $uri $uri/ /$1/index.html =404;
}
# add /sitemap for production SEO
location /sitemap {
try_files $uri =410;
}
# old /explorer redirect from v1 days
location /explorer {
rewrite /explorer/(.*) https://$host/$1 permanent;
}
# static API docs
location = /api {
#return 302 https://mempool.space/$request_uri;
try_files $uri $uri/ /en-US/index.html =404;
}
location = /api/ {
#return 302 https://mempool.space/$request_uri;
try_files $uri $uri/ /en-US/index.html =404;
}
location = /liquid/api {
#return 302 https://mempool.space/$request_uri;
try_files $uri $uri/ /en-US/index.html =404;
}
location = /liquid/api/ {
#return 302 https://mempool.space/$request_uri;
try_files $uri $uri/ /en-US/index.html =404;
}
location = /testnet/api {
#return 302 https://mempool.space/$request_uri;
try_files $uri $uri/ /en-US/index.html =404;
}
location = /testnet/api/ {
#return 302 https://mempool.space/$request_uri;
try_files $uri $uri/ /en-US/index.html =404;
}
location = /bisq/api {
#return 302 https://mempool.space/$request_uri;
try_files $uri $uri/ /en-US/index.html =404;
}
location = /bisq/api/ {
#return 302 https://mempool.space/$request_uri;
try_files $uri $uri/ /en-US/index.html =404;
}
# mainnet API
location /api/v1/donations {
proxy_pass http://127.0.0.1:8999;
# don't rate limit this API prefix
}
location /api/v1/donations/images {
proxy_pass http://127.0.0.1:8999;
proxy_cache cache;
proxy_cache_valid 200 1d;
}
location /api/v1/ws {
proxy_pass http://127.0.0.1:8999/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
}
location /api/v1 {
proxy_pass http://127.0.0.1:8999/api/v1;
limit_req burst=50 nodelay zone=api;
}
location /api/ {
proxy_pass http://[::1]:3000/;
limit_req burst=50 nodelay zone=electrs;
}
# liquid API
location /liquid/api/v1/ws {
proxy_pass http://127.0.0.1:8998/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
}
location /liquid/api/v1 {
proxy_pass http://127.0.0.1:8998/api/v1;
limit_req burst=50 nodelay zone=api;
}
location /liquid/api/ {
proxy_pass http://[::1]:3001/;
limit_req burst=50 nodelay zone=electrs;
}
# testnet API
location /testnet/api/v1/ws {
proxy_pass http://127.0.0.1:8997/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
}
location /testnet/api/v1 {
proxy_pass http://127.0.0.1:8997/api/v1;
limit_req burst=50 nodelay zone=api;
}
location /testnet/api/ {
proxy_pass http://[::1]:3002/;
limit_req burst=50 nodelay zone=electrs;
}
# bisq API
location /bisq/api/v1/ws {
proxy_pass http://127.0.0.1:8996/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
}
location /bisq/api/v1/markets {
proxy_pass http://127.0.0.1:8996/api/v1/bisq/markets;
#limit_req burst=50 nodelay zone=api;
}
location /bisq/api/v1 {
proxy_pass http://127.0.0.1:8996/api/v1;
limit_req burst=50 nodelay zone=api;
}
location /bisq/api {
proxy_pass http://127.0.0.1:8996/api/v1/bisq;
limit_req burst=50 nodelay zone=api;
}
# mainnet API
location /ws {
proxy_pass http://127.0.0.1:8999/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
}
location /ws/mainnet {
proxy_pass http://127.0.0.1:8999/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
}
location /ws/liquid {
proxy_pass http://127.0.0.1:8998/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
}
location /ws/testnet {
proxy_pass http://127.0.0.1:8997/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
}

View File

@@ -1,5 +1,4 @@
user nobody;
pid /var/run/nginx.pid;
worker_processes auto;
@@ -38,10 +37,6 @@ http {
# number of requests per connection, does not affect SPDY
keepalive_requests 100;
types_hash_max_size 2048;
proxy_cache off;
# enable gzip compression
gzip on;
gzip_vary on;
@@ -55,203 +50,107 @@ http {
client_max_body_size 10m;
# proxy cache
proxy_cache off;
proxy_cache_path /var/cache/nginx keys_zone=cache:20m levels=1:2 inactive=600s max_size=500m;
types_hash_max_size 2048;
# rate limit requests
limit_req_zone $binary_remote_addr zone=api:5m rate=50r/m;
limit_req_zone $binary_remote_addr zone=electrs:5m rate=1000r/m;
limit_req_zone $binary_remote_addr zone=api:5m rate=200r/m;
limit_req_zone $binary_remote_addr zone=electrs:5m rate=2000r/m;
limit_req_status 429;
# rate limit connections
limit_conn_zone $binary_remote_addr zone=websocket:10m;
limit_conn_status 429;
server {
listen 80 backlog=1024;
listen [::]:80 backlog=1024;
map $http_accept_language $header_lang {
default en-US;
~*^en-US en-US;
~*^en en-US;
~*^cs cs;
~*^es es;
~*^fa fa;
~*^ja ja;
~*^nl nl;
~*^pl pl;
~*^pt pt;
~*^sv sv;
~*^uk uk;
}
server_name mempool.space;
map $cookie_lang $lang {
default $header_lang;
~*^en-US en-US;
~*^en en-US;
~*^cs cs;
~*^es es;
~*^fa fa;
~*^ja ja;
~*^nl nl;
~*^pl pl;
~*^pt pt;
~*^sv sv;
~*^uk uk;
}
server {
listen 80;
server_name mempool.space mempool.ninja bsq.ninja node100.bitcoin.wiz.biz;
return 301 https://$host$request_uri;
}
server {
listen 127.0.0.1:81 backlog=1024;
listen [::]:443 ssl default http2 backlog=1024;
listen 443 ssl http2;
server_name bsq.ninja;
ssl_certificate /usr/local/etc/letsencrypt/live/bsq.ninja/fullchain.pem;
ssl_certificate_key /usr/local/etc/letsencrypt/live/bsq.ninja/privkey.pem;
include /usr/local/etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /usr/local/etc/letsencrypt/ssl-dhparams.pem;
set $redirect_uri https://mempool.space/bisq;
if ($uri = /tx.html) {
set $redirect_uri https://mempool.space/bisq/tx/$arg_tx;
}
if ($uri = /txo.html) {
set $redirect_uri https://mempool.space/bisq/tx/$arg_txo;
}
if ($uri = /Address.html) {
set $redirect_uri https://mempool.space/bisq/address/$arg_addr;
}
return 301 $redirect_uri;
}
server {
listen 443 ssl http2;
server_name node100.bitcoin.wiz.biz;
ssl_certificate /usr/local/etc/letsencrypt/live/node100.bitcoin.wiz.biz/fullchain.pem;
ssl_certificate_key /usr/local/etc/letsencrypt/live/node100.bitcoin.wiz.biz/privkey.pem;
include /usr/local/etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /usr/local/etc/letsencrypt/ssl-dhparams.pem;
include /usr/local/etc/nginx/nginx-mempool.conf;
}
server {
listen 443 ssl http2;
server_name mempool.ninja;
ssl_certificate /usr/local/etc/letsencrypt/live/mempool.ninja/fullchain.pem;
ssl_certificate_key /usr/local/etc/letsencrypt/live/mempool.ninja/privkey.pem;
include /usr/local/etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /usr/local/etc/letsencrypt/ssl-dhparams.pem;
include /usr/local/etc/nginx/nginx-mempool.conf;
}
server {
listen 127.0.0.1:81;
listen 443 ssl default http2 backlog=1024;
server_name mempool.space;
ssl_certificate /usr/local/etc/letsencrypt/live/mempool.space/fullchain.pem;
ssl_certificate_key /usr/local/etc/letsencrypt/live/mempool.space/privkey.pem;
include /usr/local/etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /usr/local/etc/letsencrypt/ssl-dhparams.pem;
root /mempool/public_html/mainnet/;
index index.html;
# security headers
set $frameOptions "DENY";
set $contentSecurityPolicy "frame-ancestors 'none'";
if ($http_referer ~ ^https://mempool.space/)
{
set $frameOptions "ALLOW-FROM https://mempool.space";
set $contentSecurityPolicy "frame-ancestors https://mempool.space";
}
if ($http_referer ~ ^https://wiz.biz/)
{
set $frameOptions "ALLOW-FROM https://wiz.biz";
set $contentSecurityPolicy "frame-ancestors https://wiz.biz";
}
add_header X-Frame-Options $frameOptions;
add_header Content-Security-Policy $contentSecurityPolicy;
add_header Link "<https://mempool.space$request_uri>; rel=\"canonical\"";
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload";
#add_header Onion-Location http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion$request_uri;
# /
location / {
try_files $uri $uri/ /index.html =404;
}
# # /sitemap
# location /sitemap {
# try_files $uri =410;
# }
#
# # /explorer
# location /explorer {
# rewrite /explorer/(.*) https://$host/$1 permanent;
# }
# /api
location = /api {
try_files $uri $uri/ /index.html =404;
}
location = /api/ {
try_files $uri $uri/ /index.html =404;
}
location /api/v1/donations/images {
# don't rate limit this URL prefix
proxy_pass http://127.0.0.1:8999;
proxy_cache cache;
proxy_cache_valid 200 1d;
}
location /api/v1/ws {
proxy_pass http://127.0.0.1:8999/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
limit_conn websocket 10;
}
location /api/v1 {
proxy_pass http://127.0.0.1:8999/api/v1;
limit_req burst=50 nodelay zone=api;
}
location /api/ {
proxy_pass http://[::1]:3000/;
limit_req burst=100 nodelay zone=electrs;
}
# /mainnet/api
location = /mainnet/api {
try_files $uri $uri/ /index.html =404;
}
location = /mainnet/api/ {
try_files $uri $uri/ /index.html =404;
}
location /mainnet/api/v1/ws {
proxy_pass http://127.0.0.1:8999/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
limit_conn websocket 10;
}
location /mainnet/api/v1 {
proxy_pass http://127.0.0.1:8999/api/v1;
limit_req burst=50 nodelay zone=api;
}
location /mainnet/api/ {
proxy_pass http://[::1]:3000/;
limit_req burst=100 nodelay zone=electrs;
}
# /liquid/api
location = /liquid/api {
try_files $uri $uri/ /index.html =404;
}
location = /liquid/api/ {
try_files $uri $uri/ /index.html =404;
}
location /liquid/api/v1/ws {
proxy_pass http://127.0.0.1:8998/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
limit_conn websocket 10;
}
location /liquid/api/v1 {
proxy_pass http://127.0.0.1:8998/api/v1;
limit_req burst=50 nodelay zone=api;
}
location /liquid/api/ {
proxy_pass http://[::1]:3001/;
limit_req burst=100 nodelay zone=electrs;
}
# /testnet/api
location = /testnet/api {
try_files $uri $uri/ /index.html =404;
}
location = /testnet/api/ {
try_files $uri $uri/ /index.html =404;
}
location /testnet/api/v1/ws {
proxy_pass http://127.0.0.1:8997/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
limit_conn websocket 10;
}
location /testnet/api/v1 {
proxy_pass http://127.0.0.1:8997/api/v1;
limit_req burst=50 nodelay zone=api;
}
location /testnet/api/ {
proxy_pass http://[::1]:3002/;
limit_req burst=100 nodelay zone=electrs;
}
# /bisq
location = /bisq/api {
try_files $uri $uri/ /index.html =404;
}
location = /bisq/api/ {
try_files $uri $uri/ /index.html =404;
}
location /bisq/api/v1/ws {
proxy_pass http://127.0.0.1:8996/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
}
location /bisq/api/v1/markets {
proxy_pass http://127.0.0.1:8996/api/v1/bisq/markets;
#limit_req burst=50 nodelay zone=api;
}
location /bisq/api/v1 {
proxy_pass http://127.0.0.1:8996/api/v1;
limit_req burst=50 nodelay zone=api;
}
location /bisq/api {
proxy_pass http://127.0.0.1:8996/api/v1/bisq;
limit_req burst=50 nodelay zone=api;
}
include /usr/local/etc/nginx/nginx-mempool.conf;
}
}

49
production/test-nginx Executable file
View File

@@ -0,0 +1,49 @@
#!/usr/bin/env zsh
PROTO=https
HOSTNAME=mempool.ninja
URL_BASE=${PROTO}://${HOSTNAME}
curltest()
{
read output
if [ "${output}" = "$1" ];then
echo "PASS: |${output}|"
else
echo "FAIL: |${output}|"
echo "WANT: |$1|"
exit 1
fi
}
echo "Starting tests to ${URL_BASE}"
echo "Test locale for / with no header or cookie"
curl -s ${URL_BASE}/ | grep '<html lang' | tr -d '\r\n' | curltest '<html lang="en-US">'
echo "Test locale for / with 'ja' lang header and no cookie"
curl -s -H 'Accept-Language: ja' ${URL_BASE}/ | grep '<html lang' | tr -d '\r\n' | curltest '<html lang="ja">'
echo "Test locale for / with 'ja' lang header and 'en' lang cookie"
curl -s -H 'Accept-Language: ja' --cookie 'lang=en' ${URL_BASE}/ | grep '<html lang' | tr -d '\r\n' | curltest '<html lang="en-US">'
echo "Test locale for / with 'ja' lang header and 'sv' lang cookie"
curl -s -H 'Accept-Language: ja' --cookie 'lang=sv' ${URL_BASE}/ | grep '<html lang' | tr -d '\r\n' | curltest '<html lang="sv">'
echo "Test locale for / with 'ja' lang header and 'foo' lang cookie"
curl -s -H 'Accept-Language: ja' --cookie 'lang=foo' ${URL_BASE}/ | grep '<html lang' | tr -d '\r\n' | curltest '<html lang="ja">'
echo "Test rewrite for /resources/pools.json with no header and no cookie"
curl -s -i ${URL_BASE}/resources/pools.json | grep -i content-type | tr -d '\r\n' | curltest 'content-type: application/json'
echo "Test rewrite for /sv/resources/pools.json with no header and no cookie"
curl -s -i ${URL_BASE}/sv/resources/pools.json | grep -i content-type | tr -d '\r\n' | curltest 'content-type: application/json'
echo "Test rewrite for /resources/pools.json with 'ja' lang header and no cookie"
curl -s -i -H 'Accept-Language: ja' ${URL_BASE}/resources/pools.json | grep -i content-type | tr -d '\r\n' | curltest 'content-type: application/json'
echo "Test rewrite for /ja/resources/pools.json with 'ja' lang header and no cookie"
curl -s -i -H 'Accept-Language: ja' ${URL_BASE}/ja/resources/pools.json | grep -i content-type | tr -d '\r\n' | curltest 'content-type: application/json'
#curl -s -i -H 'Accept-Language: sv' ${URL_BASE}/ja/resources/pools.json | grep -i content-type
#curl -s -i -H 'Accept-Language: foo' --cookie 'lang=sv' ${URL_BASE}/ja/resources/pools.json | grep -i content-type
#curl -s -i -H 'Accept-Language: foo' --cookie 'lang=sv' ${URL_BASE}/sv/resources/pools.json | grep -i content-type