Skip to content

Commit 5c37b6f

Browse files
authored
Add translations for French, German, Portuguese, Italian, Basque and Greek. (#77)
* fix(i18n): align English copy with Catalan source and sync JSX defaults - Realign locales/en/common.json against locales/ca/common.json (source of truth). Notable changes: about_us brand statement and tabs values, FAQ free/setup/gdpr answers, hero subtitle/trusted_by, target_users description/list, comparison and footer sentence-case, Òmnium diacritic, swapped New Belarus value/label, "impugnments" -> "disputes", "catalan" -> "Catalan". - Unify the primary CTA to "Start for free" across hero, app_landing, use_cases_page, and FAQ. - Sync stale JSX t() defaults with the updated JSON across components/, lib/page-meta.ts, and pages/*/+title.ts so fallbacks no longer drift. * fix(i18n): polish Spanish locale and align with EN source - Replace wrong items.8 testimonial in es (was a fictitious "Federación Deportiva"; now the proper BLOOCK quote matching en/ca). - Fix mistranslations: "Confianza Probable" -> "Confianza Demostrable", "entidad civil" -> "entidad cívica", "Cumple con GDPR" / "GDPR compliant" -> "Cumple con el RGPD". - Sync about_us mission/values, faq answers, and target_users copy with the polished EN source. - Unify primary CTA to imperative "Empieza gratis" and secondary to "Habla con un experto" across hero, app_landing, use_cases_page, FAQ. - Sentence-case and proper-noun fixes (Òmnium Cultural, Bisbalenc/a de l'Any, "Directora ejecutiva", "¡Seguiremos"); replace double-spaced em-dash leftovers with " - "; "Compañía" -> "Empresa". * feat(i18n): write fresh German translation aligned with source - Replace stale, key-mismatched de/common.json with a full translation mirroring en's current key structure (0 missing, 0 extra keys). - Use formal "Sie" address (German B2B convention), DSGVO instead of GDPR, German number formatting (200.000, 77,12 %, 50 €), and naturalized idioms ("gleiche Ausgangsbedingungen", "manipulationssicher", "Allgemeine Geschäftsbedingungen"). * feat(i18n): write fresh Greek translation aligned with EN source - Replace stale, key-mismatched el/common.json with a full translation mirroring en's current key structure (0 missing, 0 extra keys). - Use formal "εσείς" address (B2B convention), Greek acronyms (ΓΚΠΔ, ΟΟΣΑ, ΕΕ), Greek number formatting (200.000, 77,12%, 50 €), and naturalized terms ("ανοιχτός κώδικας", "απαραβίαστος", "Όροι και προϋποθέσεις"). * feat(i18n): write fresh Italian translation aligned with EN source - Replace stale, key-mismatched it/common.json with a full translation mirroring en's current key structure (0 missing, 0 extra keys). - Use informal "tu" address (modern Italian SaaS convention), keep GDPR as the standard term, render numbers Italian-style (200.000, 77,12%, 50 €), and naturalize phrasing ("inalterabile", "ente civico", "Ordine Professionale degli Infermieri"). * feat(i18n): write fresh Portuguese translation aligned with EN source - Replace stale, key-mismatched pt/common.json with a full translation mirroring en's current key structure (0 missing, 0 extra keys). - Use European Portuguese (pt-PT) given the European brand context: "registo", "câmara municipal", "folha de cálculo", "contacto", "separador", and pt-PT progressive forms ("a enviar"). - Use formal address (3rd-person imperatives, "a sua"), Portuguese acronyms (RGPD, OCDE, UE), Portuguese number formatting (200.000, 77,12%, 50 €), and naturalized terms ("código aberto", "inviolável", "Presidente da Câmara"). * feat(i18n): add support for French and Basque (Euskera) - Register 'fr' and 'eu' in locales/index.ts (Locale type, locales array, availableLocales) - both commented out in the active list pending review. - Add locales/fr/common.json: full professional French translation with formal "vous" address, French typography (« » guillemets, narrow spaces, RGPD, OCDE, "200 000", "77,12 %"), and naturalized idioms ("égaliser les chances", "Conditions générales"). - Add locales/eu/common.json: full Basque (Euskera) translation with formal "zu" address, Basque acronyms (RGPD, ELGA), and pre-number percent style (%30). Native Basque review recommended before activation. * feat(i18n): activate de, el, fr, it, pt locales - Add German, Greek, French, Italian, and Portuguese to the active locales array and uncomment them in availableLocales.
1 parent dd0666f commit 5c37b6f

20 files changed

Lines changed: 6326 additions & 930 deletions

components/CookieConsent.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ export function CookieConsent() {
8888
<Alert className='bg-background shadow-xl border-2'>
8989
<div className='flex flex-col lg:flex-row lg:items-center gap-4'>
9090
<div className='flex-1 space-y-2'>
91-
<AlertTitle className='text-base font-semibold'>{t('cookies.title', 'Cookie Consent')}</AlertTitle>
91+
<AlertTitle className='text-base font-semibold'>{t('cookies.title', 'Cookie consent')}</AlertTitle>
9292
<AlertDescription className='text-sm text-muted-foreground'>
9393
{t(
9494
'cookies.description',

components/HomeFAQ.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export default function HomeFAQ() {
5858
<h3 className='text-2xl font-bold mb-4'>{t('faq.cta_title', 'Ready to see it for yourself?')}</h3>
5959
<Button asChild size='lg' className='rounded-full px-8 h-12 text-base'>
6060
<a href='https://app.vocdoni.io' target='_blank' rel='noreferrer'>
61-
{t('faq.cta_button', 'Start your first vote free')}
61+
{t('faq.cta_button', 'Start for free')}
6262
</a>
6363
</Button>
6464
</div>

components/TrustedBySection.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ export default function TrustedBySection() {
1919

2020
return (
2121
<div className='w-full max-w-full'>
22-
<p className='text-sm text-muted-foreground mb-4 font-medium'>{t('hero.trusted_by', 'Trusted by')}</p>
22+
<p className='text-sm text-muted-foreground mb-4 font-medium'>
23+
{t('hero.trusted_by', 'Trusted by organizations of all sizes')}
24+
</p>
2325
<div className='relative w-full max-w-full overflow-hidden mask-gradient-x'>
2426
<div className='absolute left-0 top-0 bottom-0 w-20 bg-gradient-to-r from-background to-transparent z-10 pointer-events-none' />
2527
<div className='absolute right-0 top-0 bottom-0 w-20 bg-gradient-to-l from-background to-transparent z-10 pointer-events-none' />

components/app/AppHeroWithVideo.tsx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export default function AppHeroWithVideo() {
2626
<h1 className='text-4xl font-black tracking-tight sm:text-5xl lg:text-6xl xl:text-6xl max-w-2xl break-words leading-none text-foreground text-balance'>
2727
{t('vocdoni_app.app_hero.title', 'Run your vote online,')}{' '}
2828
<span className='text-primary lg:block lg:mt-1'>
29-
{t('vocdoni_app.app_hero.title_highlight', 'with confidence.')}
29+
{t('vocdoni_app.app_hero.title_highlight', 'with confidence')}
3030
</span>
3131
</h1>
3232
</MotionPreset>
@@ -35,7 +35,7 @@ export default function AppHeroWithVideo() {
3535
<p className='text-base sm:text-lg lg:text-xl text-muted-foreground/90 max-w-2xl leading-relaxed break-words font-medium'>
3636
{t(
3737
'vocdoni_app.app_hero.subtitle',
38-
'Set up, send, and collect votes in minutes, from any device, with full legal validity and audit trail.'
38+
'Set up, send, and collect votes in minutes - from any device, with full legal validity and audit trail.'
3939
)}
4040
</p>
4141
</MotionPreset>
@@ -56,13 +56,13 @@ export default function AppHeroWithVideo() {
5656
asChild
5757
>
5858
<Link href='https://app.vocdoni.io' target='_blank' rel='noopener noreferrer' variant='inlineIcon'>
59-
{t('vocdoni_app.app_hero.cta_primary', 'Start for Free')}
59+
{t('vocdoni_app.app_hero.cta_primary', 'Start for free')}
6060
<ArrowRight className='h-5 w-5 transition-transform duration-200 group-hover:translate-x-0.5' />
6161
</Link>
6262
</Button>
6363
<Button variant='outline' size='lg' className='w-full sm:w-auto' onClick={() => setPlaying(true)}>
6464
<PlayCircleIcon />
65-
{t('vocdoni_app.app_hero.cta_secondary', 'Watch the Demo')}
65+
{t('vocdoni_app.app_hero.cta_secondary', 'Watch the demo')}
6666
</Button>
6767
</MotionPreset>
6868

@@ -78,14 +78,14 @@ export default function AppHeroWithVideo() {
7878
<div className='flex items-center gap-2'>
7979
<ShieldCheckIcon className='size-5' />
8080
<span className='text-xs font-semibold tracking-wide uppercase'>
81-
{t('vocdoni_app.app_hero.trust_gdpr', 'GDPR Compliant')}
81+
{t('vocdoni_app.app_hero.trust_gdpr', 'GDPR compliant')}
8282
</span>
8383
</div>
8484
<div className='hidden sm:block w-1 h-1 rounded-full bg-border' />
8585
<div className='flex items-center gap-2'>
8686
<ScaleIcon className='size-5' />
8787
<span className='text-xs font-semibold tracking-wide uppercase'>
88-
{t('vocdoni_app.app_hero.trust_legal', 'Legal Evidence')}
88+
{t('vocdoni_app.app_hero.trust_legal', 'Legal evidence')}
8989
</span>
9090
</div>
9191
</MotionPreset>
@@ -105,9 +105,9 @@ export default function AppHeroWithVideo() {
105105
<div className='absolute inset-0 w-full h-full plyr-clean'>
106106
<CleanYoutubePlayer
107107
videoId={VIDEO_ID}
108-
title={t('vocdoni_app.app_hero.cta_secondary', 'Watch the Demo')}
108+
title={t('vocdoni_app.app_hero.cta_secondary', 'Watch the demo')}
109109
coverUrl={THUMBNAIL_URL}
110-
coverAlt={t('vocdoni_app.app_hero.cta_secondary', 'Watch the Demo')}
110+
coverAlt={t('vocdoni_app.app_hero.cta_secondary', 'Watch the demo')}
111111
/>
112112
</div>
113113
) : (
@@ -118,7 +118,7 @@ export default function AppHeroWithVideo() {
118118
>
119119
<img
120120
src={THUMBNAIL_URL}
121-
alt={t('vocdoni_app.app_hero.cta_secondary', 'Watch the Demo')}
121+
alt={t('vocdoni_app.app_hero.cta_secondary', 'Watch the demo')}
122122
className='w-full h-full object-cover group-hover:scale-105 transition-transform duration-700 ease-out'
123123
/>
124124
<div className='absolute inset-0 bg-black/10 group-hover:bg-black/20 transition-colors duration-300' />

components/app/Services.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export default function Services() {
3636
<div className='mt-8 flex flex-col gap-3 sm:flex-row sm:items-center'>
3737
<Button asChild size='lg' className='has-[>svg]:px-6'>
3838
<Link href='https://app.vocdoni.io' variant='inlineIcon'>
39-
{t('app_landing.cta.primary', 'Start free')}
39+
{t('app_landing.cta.primary', 'Start for free')}
4040
<ArrowRightIcon />
4141
</Link>
4242
</Button>

components/app/TargetUsers.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export default function TargetUsersV3() {
1717
icon: <UsersIcon className='size-5' />,
1818
},
1919
{
20-
label: t('vocdoni_app.target_users.list.political_orgs', 'Political parties'),
20+
label: t('vocdoni_app.target_users.list.political_orgs', 'Political organizations and parties'),
2121
icon: <LandmarkIcon className='size-5' />,
2222
},
2323
{

lib/page-meta.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ import { localeDefault } from '@/locales'
33
const t = (_key: string, defaultValue: string) => defaultValue
44

55
const metaDefaults = {
6-
'meta.index.title': t('meta.index.title', 'Vocdoni - secure digital voting you can trust'),
6+
'meta.index.title': t('meta.index.title', 'Vocdoni - Secure, verifiable online voting'),
77
'meta.index.description': t(
88
'meta.index.description',
9-
'Cutting-edge blockchain technology powering the future of democratic participation with transparent, secure, and accessible voting infrastructure.'
9+
'Run secure, legally valid elections for your organization with the most verifiable voting technology. Start free, set up in minutes, no credit card needed.'
1010
),
1111
'meta.contact.title': t('meta.contact.title', 'Contact - Vocdoni'),
1212
'meta.contact.description': t(

locales/ca/common.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@
114114
},
115115
{
116116
"question": "Quant es tarda a configurar una votació?",
117-
"answer": "Una sola persona pot crear i publicar una votació en menys d'una hora."
117+
"answer": "Una sola persona pot crear i publicar una votació en menys de 30 minuts."
118118
},
119119
{
120120
"question": "Els votants han d'instal·lar una app o crear un compte?",
@@ -442,7 +442,7 @@
442442
"question": "Vocdoni és gratis?"
443443
},
444444
"gdpr": {
445-
"answer": "Sí. Totes les dades s'allotgen en infraestructura de la UE. Els vots individuals s'encripten i no s'exposen mai, ni a l'organitzador ni a Vocdoni. Complim el RGPD i disposem de certificació ISO 27001.",
445+
"answer": "Sí. Totes les dades s'allotgen en infraestructura de la UE. Els vots individuals s'encripten i no s'exposen mai, ni a l'organitzador ni a Vocdoni. Complim el RGPD.",
446446
"question": "Compleix el RGPD?"
447447
},
448448
"hybrid": {
@@ -458,7 +458,7 @@
458458
"question": "Vocdoni és codi obert?"
459459
},
460460
"setup": {
461-
"answer": "Una sola persona pot configurar una elecció completa - llista de votants, papereta, calendari i ajustos de seguretat - en menys d'una hora. No cal equip tècnic, ni projecte d'implantació, ni especialistes externs. Els vostres membres reben un enllaç, hi fan clic, voten i ja està.",
461+
"answer": "Una sola persona pot configurar una elecció completa - llista de votants, papereta, calendari i ajustos de seguretat - en menys de 30 minuts. No cal equip tècnic, ni projecte d'implantació, ni especialistes externs. Els vostres membres reben un enllaç, hi fan clic, voten i ja està.",
462462
"question": "Quant de temps es tarda a configurar una elecció?"
463463
},
464464
"switch": {

0 commit comments

Comments
 (0)