feat(i18n): nine languages, each at its own address; release v6.3.0
CodeQL Analysis / Analyze CodeQL (push) Canceled after 0s
Deploy Application / deploy (push) Canceled after 0s
Mirror to Codeberg / mirror (push) Canceled after 0s
Mirror to PrivacyGuides / mirror (push) Canceled after 0s

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:
lockbitchat
2026-08-29 12:46:15 -04:00
parent 943e04e7ff
commit 98d42ac2fb
73 changed files with 33861 additions and 1299 deletions
+18 -23
View File
@@ -1,3 +1,5 @@
import { t } from '../../i18n/index.js';
// "Trusted by our partners" — partner ecosystem section.
// Translated from the Claude Design component (Partners.dc.html) into the
// project's React.createElement style: a full-bleed dark band with partner
@@ -26,21 +28,21 @@ const BecomePartner = () => {
{
id: 'aegis',
name: 'Aegis Investment',
logo: 'logo/aegis.png',
logo: '/logo/aegis.png',
logoHeight: '42px',
url: 'https://aegis-investment.com/',
desc: 'Capital partner securing confidential financial communications across its portfolio.',
role: 'Strategic backer',
desc: t('partners.aegis.desc'),
role: t('partners.aegis.role'),
delay: '.5s'
},
{
id: 'furi',
name: 'FuriLabs',
logo: 'logo/furi.png',
logo: '/logo/furi.png',
logoHeight: '54px',
url: 'https://furilabs.com/',
desc: 'Privacy-first Linux phones that ship SecureBit as a default secure channel.',
role: 'Technology partner',
desc: t('partners.furilabs.desc'),
role: t('partners.furilabs.role'),
delay: '.56s'
}
];
@@ -107,8 +109,8 @@ const BecomePartner = () => {
key: 'icon',
style: { width: '48px', height: '48px', borderRadius: '13px', display: 'grid', placeItems: 'center', background: 'rgba(240,137,42,0.12)', border: '1px solid rgba(240,137,42,0.28)', marginBottom: '24px' }
}, svg('<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M19 8v6M22 11h-6"/>', 23, ACCENT, 1.9)),
React.createElement('h3', { key: 'title', style: { margin: '0 0 8px', fontSize: '21px', fontWeight: 800, letterSpacing: '-0.4px', color: '#f4f4f6' } }, 'Become a partner'),
React.createElement('p', { key: 'desc', style: { margin: 0, fontSize: '14.5px', lineHeight: 1.6, color: '#8a8a92' } }, "Building privacy hardware or infrastructure? Let's integrate SecureBit.")
React.createElement('h3', { key: 'title', style: { margin: '0 0 8px', fontSize: '21px', fontWeight: 800, letterSpacing: '-0.4px', color: '#f4f4f6' } }, t('partners.inviteTitle')),
React.createElement('p', { key: 'desc', style: { margin: 0, fontSize: '14.5px', lineHeight: 1.6, color: '#8a8a92' } }, t('partners.inviteDesc'))
]),
React.createElement('span', {
key: 'btn',
@@ -120,7 +122,7 @@ const BecomePartner = () => {
transition: 'background .2s cubic-bezier(.2,.7,.3,1), transform .2s cubic-bezier(.2,.7,.3,1)'
}
}, [
'Start a conversation',
t('partners.inviteCta'),
svg('<path d="M5 12h14M13 6l6 6-6 6"/>', 17, 'currentColor', 2.2)
])
]);
@@ -134,20 +136,13 @@ const BecomePartner = () => {
React.createElement('div', {
key: 'eyebrow',
style: { fontFamily: MONO, fontSize: '11px', fontWeight: 600, color: '#6b6b73', textTransform: 'uppercase', letterSpacing: '1.6px', marginBottom: '14px' }
}, 'Partners & ecosystem'),
React.createElement('div', {
key: 'row',
style: { display: 'flex', alignItems: 'flex-end', justifyContent: 'space-between', gap: '32px', flexWrap: 'wrap' }
}, [
React.createElement('h2', {
key: 'h2',
style: { margin: 0, fontSize: isMobile ? '30px' : '40px', fontWeight: 800, letterSpacing: '-1.1px', lineHeight: 1.04, color: '#f4f4f6' }
}, 'Trusted by our partners'),
React.createElement('p', {
key: 'sub',
style: { margin: '0 0 4px', fontSize: '15px', lineHeight: 1.55, color: '#8a8a92', maxWidth: '360px' }
}, "A small, vetted circle — no pay-to-list logos and no badges we can't stand behind.")
])
}, t('partners.eyebrow')),
// The heading used to share a flex row with a strapline; with that gone the
// row had one child and nothing left to arrange.
React.createElement('h2', {
key: 'h2',
style: { margin: 0, fontSize: isMobile ? '30px' : '40px', fontWeight: 800, letterSpacing: '-1.1px', lineHeight: 1.04, color: '#f4f4f6' }
}, t('partners.heading'))
]),
// Cards