Skip to main content
This page lives in the Developers section and is also referenced from Deployment. If you arrived from there, your sidebar has switched to Developers.

Internationalization

CDT supports multiple interface languages using next-intl. The current baseline includes English, French, and Spanish, and additional locales can be added without major architectural changes.

How It Works

UI strings are stored in locale message files (e.g. en.json) instead of being hardcoded in components. Components read translated strings at runtime through next-intl hooks, with English as the fallback locale. Numbers and dates use locale-aware formatters so language-specific conventions are applied automatically.

This keeps presentation text separate from business logic and makes translation updates mostly a content change rather than a code change.

For data coming from the backend (not UI labels), separate API and database changes are needed — next-intl only handles UI strings.


Message Catalogs

@collabdt/core ships one message catalog per locale under src/core/i18n/messages/ (en.json, fr.json, es.json), exported together as coreMessages from @collabdt/core/messages:

import { coreMessages } from '@collabdt/core/messages';
// { en: { ... }, fr: { ... }, es: { ... } }

English is the source of truth and the universal fallback: en.json defines the full key set, and any key a locale hasn't translated yet falls back to English rather than crashing. So fr.json and es.json must mirror en.json exactly — same namespaces, same keys.

To translate or fix a string, edit the relevant catalog in src/core/i18n/messages/ and republish @collabdt/core.


Basic Usage

// Without i18n — avoid this
export default function Welcome() {
return <h1>Hello World</h1>;
}
// With next-intl
import { useTranslations } from 'next-intl';

export default function Welcome() {
const t = useTranslations('Welcome');
return <h1>{t('greeting')}</h1>;
}

Message files are keyed by namespace:

// en.json
{ "Welcome": { "greeting": "Hello" } }

// fr.json
{ "Welcome": { "greeting": "Bonjour" } }

When designing layouts, leave extra space for French — text is typically 15–30% longer than English.


Naming Convention

Use the component name as the namespace:

{
"Signin": { "title": "Login to your account" },
"ConfirmDialog": { "delete": "Delete", "cancel": "Cancel" }
}
const t = useTranslations('Signin');
return <h1>{t('title')}</h1>;

Dynamic Values

{ "message": "Hello {name}!" }
t('message', { name: 'Jane' }); // "Hello Jane!"

Pluralization

{
"followers": "You have {count, plural, =0 {no followers yet} =1 {one follower} other {# followers}}."
}
t('followers', { count: 3580 }); // "You have 3,580 followers."

{ "guidelines": "Please refer to <link>the guidelines</link>." }
t.rich('guidelines', {
link: (chunks) => <a href="/guidelines">{chunks}</a>,
});

Number Formatting

Use useFormatter for numbers outside of message strings:

import { useFormatter } from 'next-intl';

function Price() {
const format = useFormatter();
return <span>{format.number(499.9, { style: 'currency', currency: 'CAD' })}</span>;
// EN: "$499.90" FR: "499,90 $"
}
ConventionEnglishFrench
Thousands separator, (1,234)non-breaking space (1 234)
Decimal separator.,
Currency symbol positionbefore ($10.00)after (10,00 $)

Date and Time Formatting

import { useFormatter } from 'next-intl';

function EventDate({ date }: { date: Date }) {
const format = useFormatter();
return <span>{format.dateTime(date, { year: 'numeric', month: 'short', day: 'numeric' })}</span>;
}
ConventionEnglishFrench
Date orderMM/DD/YYYYDD/MM/YYYY
Month namesCapitalized ("April")Lowercase in text ("avril")
Time format12h with AM/PM24h ("16 h 30")

Adding a New Language

The steps below add Portuguese to @collabdt/core. Locale codes are capitalized in the Language enum (Pt) and lowercase as catalog keys and filenames (pt).

1. Add the core catalog

Copy src/core/i18n/messages/en.json to src/core/i18n/messages/pt.json and translate every value. Keep the keys identical to en.json — an untranslated key falls back to English at runtime, and a missing key degrades to a visible key name rather than crashing.

2. Register it in coreMessages

// src/core/i18n/index.ts
import en from './messages/en.json'
import fr from './messages/fr.json'
import es from './messages/es.json'
import pt from './messages/pt.json' // new

export const coreMessages: Record<string, Record<string, unknown>> = { en, fr, es, pt }

3. Add the locale to the Language enum

// src/core/types/dbTypes.ts
export enum Language { En = 'En', Fr = 'Fr', Es = 'Es', Pt = 'Pt' }

4. Offer it in the language switcher

LanguageSwitch renders one dropdown entry per known language. Add Portuguese to its option list and label union:

// src/core/components/LanguageSwitch.tsx
const LANGUAGE_OPTIONS: { lang: Language; labelKey: 'english' | 'french' | 'spanish' | 'portuguese' }[] = [
{ lang: Language.En, labelKey: 'english' },
{ lang: Language.Fr, labelKey: 'french' },
{ lang: Language.Es, labelKey: 'spanish' },
{ lang: Language.Pt, labelKey: 'portuguese' }, // new
]

5. Add the language-name labels

The switcher label comes from the LanguageSwitch namespace, so add a portuguese key in every catalog, translated into that language:

// LanguageSwitch namespace, per file
"portuguese": "Portuguese" // en.json
"portuguese": "Portugais" // fr.json
"portuguese": "Portugués" // es.json
"portuguese": "Português" // pt.json

Language names also appear in the OrganizationConfig namespace (langEnglish, langFrench, langSpanish) for instances that let admins choose enabled languages. Add a langPortuguese there too if you use that flow.

6. Publish and enable

Rebuild and publish @collabdt/core. Verify layouts with the translated text — some languages need more space.


Reference Documentation