Appearance
Internationalization
DoCurious uses react-i18next with browser language detection to support multiple languages. The system is configured for English, Spanish, and French.
Setup
The i18n configuration lives in src/i18n/index.ts:
typescript
import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'
import LanguageDetector from 'i18next-browser-languagedetector'
import en from './locales/en.json'
import es from './locales/es.json'
import fr from './locales/fr.json'
i18n
.use(LanguageDetector)
.use(initReactI18next)
.init({
resources: {
en: { translation: en },
es: { translation: es },
fr: { translation: fr },
},
fallbackLng: 'en',
debug: import.meta.env.DEV, // Log missing keys in development
interpolation: {
escapeValue: false, // React already escapes values
},
detection: {
order: ['localStorage', 'navigator', 'htmlTag'],
caches: ['localStorage'],
lookupLocalStorage: 'docurious-language',
},
})Detection Order
- localStorage -- checks
docurious-languagekey - navigator -- uses browser language setting
- htmlTag -- reads
langattribute on<html>
The detected language is cached in localStorage so it persists across sessions.
Supported Languages
typescript
export const supportedLanguages = [
{ code: 'en', name: 'English', flag: '🇺🇸' },
{ code: 'es', name: 'Espanol', flag: '🇪🇸' },
{ code: 'fr', name: 'Francais', flag: '🇫🇷' },
] as const
export type LanguageCode = 'en' | 'es' | 'fr'Translation File Structure
Translation files are JSON objects in src/i18n/locales/. The English file (en.json) is the primary reference:
json
{
"common": {
"loading": "Loading...",
"error": "Error",
"save": "Save",
"cancel": "Cancel",
"delete": "Delete",
"edit": "Edit",
"done": "Done",
"close": "Close",
"back": "Back",
"next": "Next",
"submit": "Submit",
"search": "Search",
"clearAll": "Clear All",
"showMore": "Show More",
"showLess": "Show Less",
"seeAll": "See All",
"noResults": "No results found",
"points": "Points"
},
"nav": {
"home": "Home",
"dashboard": "Dashboard",
"explore": "Explore",
"myChallenges": "My Challenges",
"communities": "Communities",
"gifts": "Gifts",
"school": "School",
"admin": "Admin"
// ... 30+ navigation keys
},
"explore": {
"title": "Explore",
"searchPlaceholder": "Search challenges...",
"bestFit": "Best Fit",
"featured": "Featured",
"freeChallenges": "Free Challenges",
"popular": "Popular",
"new": "New"
// ...
},
"search": { /* location, type, category, suggestions */ },
"filters": {
"title": "Filters",
"categories": { /* 6 categories */ },
"formats": { /* hosted, digitallyGuided, kit */ },
"audiences": { /* kidOriented, family, allAges, adultOriented */ },
"costs": { /* free, paid */ },
"skillLevels": { /* beginner through expert */ },
"durations": { /* halfDay, oneDay, multiDay, ongoing */ },
"locations": { /* indoors, outdoors, combination */ },
"participantOptions": { /* solo, pair, smallGroup, largeGroup */ },
"seasons": { /* anySeason, spring, summer, fall, winter */ },
"equipmentOptions": { /* noEquipment, basicSupplies, specialized */ }
},
"challenge": { /* free, difficulty, duration, startChallenge, etc. */ },
"language": {
"title": "Language",
"select": "Select Language"
}
}Current Coverage
Translation coverage is currently focused on core navigation and the explore/search experience:
| Area | Coverage |
|---|---|
| Navigation labels | Translated |
| Explore page | Translated |
| Search UI | Translated |
| Filter panel | Translated |
| Challenge cards | Partially translated |
| Auth pages | Not yet translated |
| Admin pages | Not yet translated |
| School pages | Not yet translated |
| Vendor pages | Not yet translated |
| Error messages | Not yet translated |
| Form validation | Not yet translated |
Estimated overall coverage: ~6% of user-facing strings.
Using Translations in Components
With the useTranslation Hook
tsx
import { useTranslation } from 'react-i18next'
function ExploreHeader() {
const { t } = useTranslation()
return (
<div>
<h1>{t('explore.title')}</h1>
<input placeholder={t('explore.searchPlaceholder')} />
</div>
)
}With Interpolation
tsx
const { t } = useTranslation()
// If you add: "greeting": "Hello, {{name}}!"
t('greeting', { name: user.displayName })
// → "Hello, Alex!"With Plurals
tsx
// If you add: "items_one": "{{count}} item", "items_other": "{{count}} items"
t('items', { count: 5 })
// → "5 items"How to Add Translation Keys
- Add the key to
src/i18n/locales/en.json:
json
{
"mySection": {
"newKey": "English text"
}
}- Add translations to
es.jsonandfr.json:
json
{
"mySection": {
"newKey": "Texto en espanol"
}
}- Use in components:
tsx
const { t } = useTranslation()
return <span>{t('mySection.newKey')}</span>Missing keys fall back to English. In development mode, missing key warnings are logged to the console.
How to Add a New Language
Create a translation file at
src/i18n/locales/xx.json(copyen.jsonas a starting point).Import and register in
src/i18n/index.ts:
typescript
import xx from './locales/xx.json'
// Add to resources:
resources: {
en: { translation: en },
es: { translation: es },
fr: { translation: fr },
xx: { translation: xx }, // new language
},- Add to the
supportedLanguagesarray:
typescript
export const supportedLanguages = [
{ code: 'en', name: 'English', flag: '🇺🇸' },
{ code: 'es', name: 'Espanol', flag: '🇪🇸' },
{ code: 'fr', name: 'Francais', flag: '🇫🇷' },
{ code: 'xx', name: 'Language Name', flag: '🏳️' },
] as const- Update the
LanguageCodetype (derived automatically fromsupportedLanguages).
LanguageSwitcher Component
A LanguageSwitcher component exists in the UI for users to change their language preference. It reads supportedLanguages and calls i18n.changeLanguage(code) on selection. The selected language is automatically persisted to localStorage under the docurious-language key.
Key Dependencies
| Package | Version | Purpose |
|---|---|---|
i18next | 23.11.5 | Core i18n framework |
react-i18next | 14.1.2 | React bindings |
i18next-browser-languagedetector | 7.2.1 | Auto-detect browser language |
See Also
- i18n guide -- internationalization feature overview, supported languages, and content translation strategy