Skip to main content

Internationalization (i18n)

This page documents the multi-language support of the PCH-SIG application.

Overview​

PCH-SIG supports multiple languages to accommodate users from different regions:

LanguageCodeStatus
FrenchfrPrimary
PortugueseptComplete
EnglishenComplete

Kriol (Guinea-Bissau Creole) is not an interface language: it is a language of the Bemba assistant only, with its own setting. Its former interface dictionary was removed on 12 October 2026. A browser set to Creole gets the interface in Portuguese.

Architecture​

Frontend (React)​

src/
├── i18n/
│ ├── index.ts # i18next configuration
│ └── locales/
│ ├── fr.json # French
│ ├── pt.json # Portuguese
│ ├── en.json # English

Backend (Symfony)​

translations/
├── messages.fr.yaml
├── messages.pt.yaml
├── messages.en.yaml
├── api_messages.{en,pt}.yaml # API response messages
└── vbg_analytics.{fr,en,pt}.yaml

Frontend​

i18next Configuration​

File: frontend/src/i18n/index.ts.

SettingValue
Languagesfr, pt, en (LANGUES_SUPPORTEES); fallback to fr
Detectionchoice saved in localStorage (key i18nextLng), otherwise the browser language
Variantspt-BR gives pt; a browser set to Creole (kea, pov) gives pt

The API client sends the current language in the Accept-Language header of every request.

Translation File Structure​

// fr.json
{
"common": {
"save": "Save",
"cancel": "Cancel",
"delete": "Delete",
"edit": "Edit",
"loading": "Loading..."
},
"menu": {
"dashboard": "Dashboard",
"menages": "Households",
"beneficiaires": "Beneficiaries",
"paiements": "Payments",
"plaintes": "Complaints"
},
"menages": {
"title": "Household List",
"create": "New Household",
"fields": {
"code": "Household Code",
"tailleMenage": "Household Size",
"region": "Region"
}
}
}

Usage in Components​

import { useTranslation } from 'react-i18next';

function MenagesList() {
const { t } = useTranslation();

return (
<div>
<h1>{t('menages.title')}</h1>
<button>{t('menages.create')}</button>
<table>
<thead>
<tr>
<th>{t('menages.fields.code')}</th>
<th>{t('menages.fields.tailleMenage')}</th>
</tr>
</thead>
</table>
</div>
);
}

Language Switching​

i18n.changeLanguage(lang) is enough: the detector saves the choice in localStorage (key i18nextLng). Do not write another key by hand.

Backend​

Symfony Configuration​

# config/packages/translation.yaml
framework:
default_locale: fr
translator:
default_path: '%kernel.project_dir%/translations'
fallbacks:
- fr

Translation Files​

# translations/messages.fr.yaml
menage:
created: "Household created successfully"
updated: "Household updated"
deleted: "Household deleted"

error:
not_found: "Element not found"
validation: "Validation error"

Usage in Controllers​

use Symfony\Contracts\Translation\TranslatorInterface;

class MenageController extends AbstractController
{
public function __construct(
private TranslatorInterface $translator
) {}

public function create(): JsonResponse
{
// ...
return $this->json([
'success' => true,
'message' => $this->translator->trans('menage.created')
]);
}
}

Language Detection​

backend/src/EventListener/RequestLocaleListener.php sets the language of each request, in order of priority:

  1. ?locale= parameter;
  2. X-Locale header;
  3. Accept-Language header (sent by the frontend);
  4. fr by default.

Only fr, en and pt are accepted.

Documentation (Docusaurus)​

i18n Configuration​

// docusaurus.config.js
module.exports = {
i18n: {
defaultLocale: 'fr',
locales: ['fr', 'en'],
localeConfigs: {
fr: {
label: 'Français',
},
en: {
label: 'English',
},
},
},
};

File Structure​

documentation/
├── docs/ # French (default)
│ └── guide/
│ └── introduction.md
└── i18n/
└── en/
└── docusaurus-plugin-content-docs/
└── current/
└── guide/
└── introduction.md

Best Practices​

Key Organization​

{
"module": {
"action": {
"label": "Text"
}
}
}

Example:

{
"menages": {
"create": {
"title": "Create Household",
"submit": "Save"
}
}
}

Pluralization​

{
"menages": {
"count": "{{count}} household",
"count_plural": "{{count}} households"
}
}
t('menages.count', { count: 5 }) // "5 households"

Variables​

{
"welcome": "Welcome, {{name}}!"
}
t('welcome', { name: user.nom }) // "Welcome, John!"

Translation Workflow​

1. Add a New Key​

  1. Add the key in fr.json (primary language)
  2. Copy to other language files
  3. Translate each file

2. Helper Scripts​

# Inventory: missing keys, hard-coded text, orphan keys (from the repository root)
node frontend/scripts/i18n-audit.js . --json audit-i18n.json

# Dynamically built keys (prefix + variable)
node frontend/scripts/i18n-dynamic-check.js .
ToolUsage
i18next-parserAutomatic key extraction
BabelEditVisual JSON editing
CrowdinCollaborative translation platform

New Language Checklist​

  • Create frontend translation file
  • Create backend translation file
  • Add the code to LANGUES_SUPPORTEES (frontend) and to RequestLocaleListener::LOCALES (backend)
  • Add option in language selector
  • Update documentation
  • Test RTL display if necessary
  • Verify date/number formats

Regional Formats​

Dates​

// Use date-fns with locale
import { format } from 'date-fns';
import { fr, pt, enUS } from 'date-fns/locale';

const locales = { fr, pt, en: enUS };

format(new Date(), 'PPP', {
locale: locales[i18n.language]
});

Numbers​

new Intl.NumberFormat(i18n.language, {
style: 'currency',
currency: 'XOF'
}).format(15000);
// "15 000 XOF" (fr)
// "15,000 XOF" (en)