Complete multilingual support for Ackify frontend.
- 🇫🇷 Français (default)
- 🇬🇧 English (fallback)
- 🇪🇸 Español
- 🇩🇪 Deutsch
- 🇮🇹 Italiano
The frontend automatically detects language via:
- localStorage - Saved user choice
- navigator.language - Browser language
- Fallback - English if not supported
User interface with Unicode flags:
🇫🇷 FR | 🇬🇧 EN | 🇪🇸 ES | 🇩🇪 DE | 🇮🇹 IT
Click → Language changes + saves to localStorage
Located in /webapp/src/locales/:
locales/
├── fr.json # Français
├── en.json # English
├── es.json # Español
├── de.json # Deutsch
└── it.json # Italiano
{
"home": {
"title": "Ackify - Proof of Read",
"subtitle": "Cryptographic read signatures"
},
"document": {
"sign": "Sign this document",
"signed": "Document signed",
"signatures": "{count} confirmation | {count} confirmations"
}
}Pluralization:
"signatures": "{count} confirmation | {count} confirmations"Usage:
{{ $t('document.signatures', { count: 42 }) }}
// → "42 confirmations"Emails use multilingual templates in /backend/templates/emails/:
templates/emails/
├── fr/
│ ├── reminder.html
│ └── reminder.txt
├── en/
│ ├── reminder.html
│ └── reminder.txt
├── es/...
├── de/...
└── it/...
POST /api/v1/admin/documents/doc_id/reminders
Content-Type: application/json
{
"emails": ["user@company.com"],
"locale": "fr"
}Backend loads template fr/reminder.html.
# Default language for emails (default: en)
ACKIFY_MAIL_DEFAULT_LOCALE=fr- Create translation file:
cd webapp/src/locales
cp en.json pt.json # Portuguese- Translate:
{
"home": {
"title": "Ackify - Prova de Leitura",
"subtitle": "Assinaturas criptográficas de leitura"
}
}- Register in i18n:
// webapp/src/i18n.ts
import pt from './locales/pt.json'
const i18n = createI18n({
locale: 'fr',
fallbackLocale: 'en',
messages: {
fr, en, es, de, it,
pt // Add here
}
})- Add to selector:
<!-- components/LanguageSwitcher.vue -->
<button @click="changeLocale('pt')">🇵🇹 PT</button>- Create directory:
mkdir -p backend/templates/emails/pt- Create templates:
cp backend/templates/emails/en/reminder.html backend/templates/emails/pt/
cp backend/templates/emails/en/reminder.txt backend/templates/emails/pt/-
Translate templates
-
Rebuild:
docker compose up -d --force-recreate ackify-ce --buildProject includes a script to verify translation completeness:
cd webapp
npm run lint:i18nOutput:
✅ fr.json - 156 keys
✅ en.json - 156 keys
✅ es.json - 156 keys
✅ de.json - 156 keys
✅ it.json - 156 keys
All translations are complete!
Script automatically runs in GitHub Actions to block PRs with missing translations.
- ✅ Use structured keys:
feature.action.label - ✅ Group by page/component
- ✅ Avoid too generic keys (
button,title) - ✅ Use placeholders:
{count},{name}
Example:
{
"admin": {
"documents": {
"list": {
"title": "Document list",
"count": "{count} document | {count} documents"
}
}
}
}When adding new keys in French:
# Sync script (to create)
node scripts/sync-i18n-from-fr.jsAutomatically copies new keys to other languages with [TODO].
For long texts, use arrays:
{
"help": {
"intro": [
"Ackify allows creating cryptographic signatures.",
"Each signature is timestamped and non-repudiable.",
"Data is stored immutably."
]
}
}Usage:
<p v-for="line in $tm('help.intro')" :key="line">
{{ line }}
</p>// Format with current locale
import { useI18n } from 'vue-i18n'
const { locale } = useI18n()
const formatted = new Date().toLocaleDateString(locale.value, {
year: 'numeric',
month: 'long',
day: 'numeric'
})
// fr: "15 janvier 2025"
// en: "January 15, 2025"const formatted = (42000).toLocaleString(locale.value)
// fr: "42 000"
// en: "42,000"Meta tags are dynamically translated:
<script setup>
import { useI18n } from 'vue-i18n'
import { useHead } from '@vueuse/head'
const { t } = useI18n()
useHead({
title: t('home.title'),
meta: [
{ name: 'description', content: t('home.description') }
]
})
</script>For more details on frontend i18n implementation, see:
This file contains:
- Complete vue-i18n architecture
- Contribution guide
- Synchronization scripts
- Advanced examples