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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user