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';
|
||||
|
||||
// "Join the future of privacy" — community / open-source call-to-action.
|
||||
// Translated from the Claude Design component (Community CTA.dc.html): a centered
|
||||
// glowing card with GitHub + Feedback actions on a full-bleed dark band.
|
||||
@@ -16,7 +18,6 @@ const CommunityCTA = () => {
|
||||
}, []);
|
||||
|
||||
const ACCENT = '#f0892a';
|
||||
const MONO = "'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace";
|
||||
const SANS = "'Manrope', system-ui, -apple-system, sans-serif";
|
||||
|
||||
const githubUrl = 'https://github.com/SecureBitChat/securebit-chat/';
|
||||
@@ -41,7 +42,7 @@ const CommunityCTA = () => {
|
||||
key: 'i', width: 20, height: 20, viewBox: '0 0 24 24', fill: 'currentColor',
|
||||
dangerouslySetInnerHTML: { __html: '<path d="M12 2C6.48 2 2 6.58 2 12.26c0 4.5 2.87 8.32 6.84 9.67.5.09.68-.22.68-.49 0-.24-.01-.87-.01-1.71-2.78.62-3.37-1.36-3.37-1.36-.46-1.18-1.11-1.5-1.11-1.5-.91-.63.07-.62.07-.62 1 .07 1.53 1.05 1.53 1.05.89 1.56 2.34 1.11 2.91.85.09-.66.35-1.11.63-1.36-2.22-.26-4.55-1.14-4.55-5.07 0-1.12.39-2.03 1.03-2.75-.1-.26-.45-1.3.1-2.71 0 0 .84-.27 2.75 1.05a9.3 9.3 0 0 1 5 0c1.91-1.32 2.75-1.05 2.75-1.05.55 1.41.2 2.45.1 2.71.64.72 1.03 1.63 1.03 2.75 0 3.94-2.34 4.81-4.57 5.06.36.32.68.94.68 1.9 0 1.37-.01 2.47-.01 2.81 0 .27.18.59.69.49A10.02 10.02 0 0 0 22 12.26C22 6.58 17.52 2 12 2z"/>' }
|
||||
}),
|
||||
'GitHub Repository'
|
||||
t('community.github')
|
||||
]);
|
||||
|
||||
const feedbackBtn = React.createElement('a', {
|
||||
@@ -63,15 +64,7 @@ const CommunityCTA = () => {
|
||||
strokeWidth: 1.9, strokeLinecap: 'round', strokeLinejoin: 'round',
|
||||
dangerouslySetInnerHTML: { __html: '<path d="M21 11.5a8 8 0 0 1-11.6 7.1L4 20l1.4-5.3A8 8 0 1 1 21 11.5z"/><path d="M8.5 11h7M8.5 14h4.5"/>' }
|
||||
}),
|
||||
'Feedback'
|
||||
]);
|
||||
|
||||
const chip = (label) => React.createElement('span', {
|
||||
key: label,
|
||||
style: { display: 'inline-flex', alignItems: 'center', gap: '7px' }
|
||||
}, [
|
||||
React.createElement('span', { key: 'd', style: { width: '5px', height: '5px', borderRadius: '50%', background: '#3ecf8e' } }),
|
||||
label
|
||||
t('community.feedback')
|
||||
]);
|
||||
|
||||
const card = React.createElement('div', {
|
||||
@@ -97,31 +90,21 @@ const CommunityCTA = () => {
|
||||
alt: 'SecureBit',
|
||||
style: { display: 'inline-block', width: '64px', height: '64px', objectFit: 'contain', marginBottom: '22px', animation: 'ccUp .4s cubic-bezier(.2,.7,.3,1)' }
|
||||
}),
|
||||
// eyebrow
|
||||
React.createElement('div', {
|
||||
key: 'eyebrow',
|
||||
style: { fontFamily: MONO, fontSize: '11px', fontWeight: 600, color: '#6b6b73', textTransform: 'uppercase', letterSpacing: '1.8px', marginBottom: '14px' }
|
||||
}, 'Open source · community-driven'),
|
||||
// title
|
||||
React.createElement('h2', {
|
||||
key: 'title',
|
||||
style: { margin: '0 0 16px', fontSize: isMobile ? '28px' : '36px', fontWeight: 800, letterSpacing: '-1px', lineHeight: 1.05, color: '#f4f4f6' }
|
||||
}, 'Join the future of privacy'),
|
||||
}, t('community.title')),
|
||||
// description
|
||||
React.createElement('p', {
|
||||
key: 'desc',
|
||||
style: { margin: '0 auto 32px', maxWidth: '560px', fontSize: '16px', lineHeight: 1.65, color: '#9a9aa2' }
|
||||
}, 'SecureBit grows thanks to its community. Your ideas and feedback shape the future of secure communication — built in the open, with complete ASN.1 validation end‑to‑end.'),
|
||||
}, t('community.description')),
|
||||
// buttons
|
||||
React.createElement('div', {
|
||||
key: 'btns',
|
||||
style: { display: 'flex', gap: '14px', justifyContent: 'center', flexWrap: 'wrap' }
|
||||
}, [githubBtn, feedbackBtn]),
|
||||
// trust chips
|
||||
React.createElement('div', {
|
||||
key: 'chips',
|
||||
style: { display: 'flex', gap: '10px 22px', justifyContent: 'center', flexWrap: 'wrap', marginTop: '30px', fontFamily: MONO, fontSize: '11px', fontWeight: 500, color: '#56565e', textTransform: 'uppercase', letterSpacing: '1px' }
|
||||
}, [chip('MIT licensed'), chip('No tracking'), chip('Auditable cryptography')])
|
||||
}, [githubBtn, feedbackBtn])
|
||||
]);
|
||||
|
||||
return React.createElement('section', {
|
||||
|
||||
Reference in New Issue
Block a user