feat(i18n): nine languages, each at its own address; release v6.3.0
The site now speaks German, French, Spanish, Ukrainian, Russian, Chinese, Korean and Hindi alongside English — 623 strings per language, 5,607 translations, covering the landing page, the key exchange, the chat, group calls and every error along the way. Each locale is a real page at a real URL (/de/, /fr/, …), generated at build time from locales/*.json. That is the whole point: the app is client-rendered, so a language that only swaps strings at runtime has no address for a crawler to index and no link anyone can share. Each page carries its own canonical, a reciprocal hreflang cluster and translated schema.org. The URL decides the language, always. A stored preference only applies where the address does not say, and nothing ever redirects on Accept-Language — that is how sites become invisible to search engines outside one country. robots.txt and sitemap.xml did not exist before; both are generated now. Bugs found on the way, each with a test that would have caught it: - partner logos used page-relative paths and 404'd from any /xx/ page - post-build stamped ?v= only into the root shell, leaving locales behind - "Back online" appeared on every page load, not just after being offline - update timestamps were hard-coded to US format for every reader - t() threw on a partial window, taking whole components down with it
This commit is contained in:
+30
-2
@@ -54,6 +54,33 @@ http {
|
||||
# (e.g. qr-local.js ~1.2MB) when nothing changed, while still picking up
|
||||
# new releases immediately.
|
||||
~^/dist/ "no-cache, must-revalidate";
|
||||
# The install prompt is a plain browser module and imports the dictionary by a
|
||||
# bare path, so that import carries no ?v= to bust the cache with. Left on the
|
||||
# default one-year immutable rule below, a visitor would keep the translations
|
||||
# they first loaded for a year.
|
||||
~^/src/i18n/ "no-cache, must-revalidate";
|
||||
# Crawler-facing files must stay re-fetchable: the default below is a
|
||||
# one-year immutable cache, which would freeze a sitemap for a year.
|
||||
~^/robots\.txt$ "public, max-age=3600";
|
||||
~^/sitemap\.xml$ "public, max-age=3600";
|
||||
}
|
||||
|
||||
# Which app shell an unmatched navigation falls back to. A request under /de/ must
|
||||
# land on /de/index.html, not the English one — otherwise every deep link into a
|
||||
# localized page silently switches language. The entries between the markers are
|
||||
# rewritten by scripts/build-i18n.js from locales/site.json.
|
||||
map $uri $sb_shell {
|
||||
default /index.html;
|
||||
# BEGIN generated locale shells
|
||||
~^/de/ /de/index.html;
|
||||
~^/fr/ /fr/index.html;
|
||||
~^/es/ /es/index.html;
|
||||
~^/uk/ /uk/index.html;
|
||||
~^/ru/ /ru/index.html;
|
||||
~^/zh/ /zh/index.html;
|
||||
~^/ko/ /ko/index.html;
|
||||
~^/hi/ /hi/index.html;
|
||||
# END generated locale shells
|
||||
}
|
||||
|
||||
# CDN-Cache-Control is read by Cloudflare (and other CDNs) *independently* of the
|
||||
@@ -71,6 +98,7 @@ http {
|
||||
~^/meta\.json$ "no-store";
|
||||
~^/config/ice-servers\.js$ "no-store";
|
||||
~^/dist/ "no-store";
|
||||
~^/src/i18n/ "no-store";
|
||||
}
|
||||
|
||||
server {
|
||||
@@ -99,13 +127,13 @@ http {
|
||||
# Real asset files must return 404 when missing — never fall back to the
|
||||
# HTML shell, which would be served with the wrong content type and break
|
||||
# module/script loading (e.g. a missing config/ice-servers.js).
|
||||
location ~* \.(js|mjs|jsx|css|json|map|woff2?|ttf|otf|png|jpe?g|gif|webp|svg|ico|mp3|mp4|webm)$ {
|
||||
location ~* \.(js|mjs|jsx|css|json|map|xml|txt|woff2?|ttf|otf|png|jpe?g|gif|webp|svg|ico|mp3|mp4|webm)$ {
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
# SPA-style fallback so unknown navigation routes still load the app shell.
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
try_files $uri $uri/ $sb_shell;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user