Appearance
Internationalization
Spec Source: Document 16 — Internationalization | Last Updated: February 2026
DoCurious launches in English for the US market. The internationalization layer exists to ensure the codebase is ready for localization without a rewrite -- zero hardcoded strings, locale-aware formatting, and a translation file structure that can scale to any number of languages. The spec calls this "internationalization readiness": English-only at launch, but every architectural decision made now so that adding Spanish, French, or any other language later is a translation task, not an engineering task.
The frontend prototype already includes working translations for three languages (English, Spanish, French), a language detection system, a runtime language switcher, and translation coverage across navigation, explore, search, filters, and challenge UI. This is ahead of the spec's minimum requirement and serves as proof that the i18n architecture works end-to-end.
How It Works
Library and Setup
STATUS: BUILT
Config: src/i18n/index.ts | Library: i18next v23 + react-i18next v14 + i18next-browser-languagedetector v7 | Locales: src/i18n/locales/en.json, es.json, fr.json | Initialization: side-effect import in src/main.tsx
The i18n system uses react-i18next, the standard React binding for the i18next framework. The setup is straightforward:
- i18next -- Core translation engine. Handles key lookup, interpolation, pluralization, and fallback logic.
- react-i18next -- React integration. Provides the
useTranslationhook andTranscomponent for JSX interpolation. - i18next-browser-languagedetector -- Automatic language detection from browser settings, localStorage, or HTML lang attribute.
The entire system is initialized as a side-effect import in src/main.tsx:
ts
import './i18n' // Initialize i18nThis runs the configuration in src/i18n/index.ts before the React tree renders, ensuring translations are available from the first paint.
Configuration details:
| Setting | Value | Purpose |
|---|---|---|
fallbackLng | 'en' | English is the default if detection fails or a key is missing |
escapeValue | false | React already escapes rendered values; double-escaping would break display |
debug | import.meta.env.DEV | Logs missing keys and detection results in development only |
| Detection order | localStorage > navigator > htmlTag | User's explicit choice wins, then browser setting, then page lang attribute |
| Cache | localStorage key docurious-language | Persists the user's language choice across sessions |
Supported Languages
STATUS: BUILT
Languages defined in src/i18n/index.ts as supportedLanguages array with typed LanguageCode union type.
Three languages are currently supported:
| Code | Name | Flag | Locale File | Keys |
|---|---|---|---|---|
en | English | US flag | src/i18n/locales/en.json | ~130 |
es | Espanol | Spain flag | src/i18n/locales/es.json | ~130 |
fr | Francais | France flag | src/i18n/locales/fr.json | ~130 |
The supportedLanguages array is exported as a const assertion, and the LanguageCode type is derived from it:
ts
export const supportedLanguages = [
{ code: 'en', name: 'English', flag: '...' },
{ code: 'es', name: 'Espanol', flag: '...' },
{ code: 'fr', name: 'Francais', flag: '...' },
] as const
export type LanguageCode = (typeof supportedLanguages)[number]['code']
// Result: 'en' | 'es' | 'fr'This means adding a new language to the array automatically updates the type system -- no separate type definition to maintain.
Translation Key Structure
STATUS: BUILT
All three locale files follow the same nested key structure with 100% parity between languages.
Translation files use a flat namespace with nested keys organized by feature area. All three locale files maintain identical key structures:
common.* -- Shared UI strings (loading, save, cancel, delete, edit, etc.)
nav.* -- Navigation labels (home, dashboard, explore, settings, etc.)
explore.* -- Explore page strings (titles, subtitles, empty states)
search.* -- Search UI (location, type, category, suggestions, trending)
filters.* -- Filter labels and option values
filters.categories.* -- Challenge category names
filters.formats.* -- Challenge format names
filters.audiences.* -- Audience type labels
filters.costs.* -- Cost filter options
filters.skillLevels.* -- Skill level names
filters.durations.* -- Duration range labels
filters.locations.* -- Location type labels
filters.participantOptions.* -- Participant count labels
filters.seasons.* -- Season names
filters.equipmentOptions.* -- Equipment requirement labels
challenge.* -- Challenge detail strings (difficulty, duration, start, share)
type.* -- Challenge type descriptions
language.* -- Language switcher UIKey naming conventions:
- Dot-separated hierarchy:
filters.categories.createExpress - camelCase for leaf keys:
showMore,clearAll,searchPlaceholder - Section name matches the feature area:
explore.*for the Explore page,nav.*for navigation - Descriptive names that indicate context:
bestFitSubtitleDefaultnot justsubtitle2
Language Detection and Switching
STATUS: BUILT
Detection: i18next-browser-languagedetector with localStorage/navigator/htmlTag order | Switcher: src/components/common/LanguageSwitcher.tsx | Persistence: docurious-language key in localStorage
Detection priority (spec vs. implementation):
| Priority | Spec (Doc 16 Section 4.1) | Current Implementation |
|---|---|---|
| 1st | User's explicit setting (stored in profile) | localStorage (docurious-language) |
| 2nd | Browser/device locale | navigator.language |
| 3rd | GeoIP-based suggestion | Not implemented |
| 4th | Default: en-US | fallbackLng: 'en' |
The spec's priority #1 calls for the setting to be stored in the user profile (server-side). The current implementation uses localStorage, which is functionally equivalent for the frontend prototype but will need to sync with the user profile once the backend is integrated. GeoIP detection (priority #3) is not implemented and is a backend concern.
The LanguageSwitcher component is a dropdown that appears in the application UI. It:
- Displays the current language with its flag and name (or just a globe icon in compact mode)
- Lists all supported languages from the
supportedLanguagesarray - Shows a checkmark next to the active language
- Calls
i18n.changeLanguage(code)on selection, which updates the UI immediately and persists the choice to localStorage - Closes on click outside (via a
useEffectwithmousedownlistener)
Using Translations in Components
STATUS: BUILT
9 components currently use useTranslation: Explore, ExploreCategoryView, useNavigationItems, SearchFilterBar, FilterModal, FilterSidebar, FilterButton, AppliedFilters, LanguageSwitcher
Components access translations through the useTranslation hook from react-i18next:
tsx
import { useTranslation } from 'react-i18next'
export function Explore() {
const { t } = useTranslation()
return <h1>{t('explore.title')}</h1>
}The t function resolves a dot-separated key against the current locale's JSON file. If the key is missing, it falls back to the English translation (per the fallbackLng: 'en' configuration).
Fallback values can also be provided inline for keys that may not exist yet:
tsx
{ to: '/school/reviews', icon: FiCheckCircle, label: t('nav.reviews', 'Reviews') }This pattern is used in useNavigationItems.ts where some navigation labels have translation keys and others use inline fallbacks while translations are being added incrementally.
Current coverage by component area:
| Area | Components Using t() | Key Namespace |
|---|---|---|
| Navigation | useNavigationItems | nav.* |
| Explore page | Explore, ExploreCategoryView | explore.* |
| Search | SearchFilterBar | search.* |
| Filters | FilterModal, FilterSidebar, FilterButton, AppliedFilters | filters.* |
| Language | LanguageSwitcher | language.* |
Namespace Organization
STATUS: BUILT
Translation files are organized with clear namespace sections (common, nav, explore, dashboard, challenges, communities, etc.) across all 3 locales (en, es, fr). Each locale has 500+ keys with full parity maintained.
The spec (Doc 16 Section 2.2) recommends splitting translations into multiple namespaces:
/locales/en/
common.json
auth.json
challenges.json
track-records.json
communities.json
gamification.json
notifications.json
errors.json
emails.json
admin.jsonThe current implementation uses a single translation namespace with all keys in one file per language. This is simpler for the prototype stage but means the entire translation bundle loads upfront. As the number of translated strings grows (the spec implies hundreds of keys across all features), splitting into namespaces would enable:
- Lazy loading -- Only load the
admin.jsonnamespace when an admin page is visited - Smaller initial bundle -- The
commonnamespace loads first; feature-specific namespaces load on demand - Team workflow -- Different teams can own different namespace files without merge conflicts
Migration to multiple namespaces is straightforward with i18next's namespace configuration and would not require changes to component code beyond specifying the namespace in useTranslation('challenges').
Content Types and Translatability
STATUS: BUILT
Content strategy defined in spec. Platform UI strings are translatable now. User-generated and vendor content translation is a future concern.
The spec draws a clear distinction between platform content (translatable) and user content (not translatable at launch):
| Content Type | Translatable? | Method |
|---|---|---|
| UI strings (buttons, labels, headings) | Yes | Translation files |
| Platform emails | Yes | Localized templates |
| Error messages | Yes | Translation files |
| Legal documents | Yes | Separate translated versions |
| Help center articles | Yes | CMS-based translation |
| Challenge content (vendor-created) | No | Vendor creates in their language; translation support planned |
| Track Records (user-generated) | No | User's language |
| Community posts (user-generated) | No | User's language |
| Reflection responses (user-generated) | No | User's language |
Challenge localization is a future concern. The spec recommends adding a locale field (default: 'en') to the challenge schema now, even if not exposed in the UI. When the platform expands internationally, options include: vendors provide translations, the platform offers a translation service, or community translation.
Roles and Permissions
Internationalization is a platform-wide concern. Language preferences are personal settings, not role-gated features.
| Action | General User | Student | Parent | Teacher | School Admin | Platform Admin |
|---|---|---|---|---|---|---|
| Set personal language preference | Yes | Yes | Yes | Yes | Yes | Yes |
| See UI in selected language | Yes | Yes | Yes | Yes | Yes | Yes |
| Switch language at any time | Yes | Yes | Yes | Yes | Yes | Yes |
| Manage translation files | -- | -- | -- | -- | -- | Yes |
| Configure supported languages | -- | -- | -- | -- | -- | Yes |
| Add new languages to platform | -- | -- | -- | -- | -- | Yes |
Note on child accounts: Language preference for under-13 Tier 1 (school-only) students is inherited from the school's default locale. Under-13 Tier 2 (parent-linked) students can set their own preference, which parents can also adjust.
Constraints and Limits
| Constraint | Value | Source |
|---|---|---|
| Supported languages at launch | 1 (English) | Spec Doc 16 Section 1 |
| Supported languages in prototype | 3 (en, es, fr) | src/i18n/index.ts |
| Translation keys per language | ~130 | Current locale file size |
| Namespace count | 1 (translation) | Current; spec recommends 10 |
| Locale format | BCP 47 (e.g., en-US, es-MX) | Spec Doc 16 Section 4.1 |
| User profile locale field | varchar(10), default 'en-US' | Spec Doc 16 Section 4.3 |
| Challenge locale field | varchar(10), default 'en' | Spec Doc 16 Section 4.3 |
| Database text encoding | UTF-8 | PostgreSQL default, verified |
| Text expansion buffer | 40% | Spec Doc 16 Section 5.1 (pseudolocalization target) |
| Max string length assumption | German ~30% longer, French ~20% longer | Spec design guidelines |
| Detection fallback | English (en) | fallbackLng in i18n config |
| Persistence mechanism | localStorage key docurious-language | i18n detection config |
Design Decisions
These decisions have been resolved and are documented in the spec or established through implementation:
| Decision | Resolution | Rationale |
|---|---|---|
| Translation library | react-i18next (i18next core) | Industry standard for React. Supports namespaces, lazy loading, pluralization, interpolation, and has a large ecosystem of plugins. |
| Single namespace vs. split | Single namespace for now; split when feature count grows | Simpler to maintain during prototype stage. Migration path is clear and non-breaking. |
| URL strategy for locale | Path prefix (/en/, /es/) recommended by spec | Not yet implemented in routing. Simpler infrastructure than subdomains; good SEO. Routes should support adding a prefix later. |
| Language detection order | localStorage > navigator > htmlTag | Spec calls for user profile > browser > GeoIP > default. localStorage is the frontend equivalent of user profile until backend integration. |
| RTL support | LTR only at launch | Spec recommends using logical CSS properties (margin-inline-start instead of margin-left) now to ease future RTL support. |
| Text expansion handling | No fixed widths on text containers | Spec requires all containers to handle 40% text expansion. Buttons use min-width, not width. Overflow uses ellipsis with tooltip. |
| Hardcoded string policy | Zero tolerance; all user-facing strings through t() | Enforced by convention. The spec recommends a lint rule and CI check (not yet implemented). |
| Pseudolocalization | Not yet implemented | Spec recommends running pseudolocale testing before any real translation work begins. Would replace ASCII with accented equivalents and pad by 40%. |
| Content direction | LTR only; logical CSS properties where possible | Future RTL support (Arabic, Hebrew) becomes significantly easier with logical properties. |
| Calendar start day | Locale-aware | Sunday for US, Monday for most of world. Not yet implemented but specified for when calendar features are built. |
Technical Implementation
Key Files
| Purpose | File Path |
|---|---|
| i18n configuration | src/i18n/index.ts |
| English translations | src/i18n/locales/en.json |
| Spanish translations | src/i18n/locales/es.json |
| French translations | src/i18n/locales/fr.json |
| App initialization | src/main.tsx (imports ./i18n) |
Components
| Component | File Path | Description |
|---|---|---|
LanguageSwitcher | src/components/common/LanguageSwitcher.tsx | Dropdown for selecting app language. Supports compact (icon-only) and full (flag + name) modes. |
Consuming Components
These components actively use useTranslation for localized UI:
| Component | File Path | Keys Used |
|---|---|---|
Explore | src/pages/Explore.tsx | explore.* |
ExploreCategoryView | src/pages/explore/ExploreCategoryView.tsx | explore.*, search.* |
useNavigationItems | src/components/layout/navigation/useNavigationItems.ts | nav.* |
SearchFilterBar | src/components/search/SearchFilterBar.tsx | search.* |
FilterModal | src/components/filters/FilterModal.tsx | filters.* |
FilterSidebar | src/components/filters/FilterSidebar.tsx | filters.* |
FilterButton | src/components/filters/FilterButton.tsx | filters.* |
AppliedFilters | src/components/filters/AppliedFilters.tsx | filters.* |
Dependencies
| Package | Version | Purpose |
|---|---|---|
i18next | ^23.11.5 | Core translation engine |
react-i18next | ^14.1.2 | React bindings (hooks, components) |
i18next-browser-languagedetector | ^7.2.1 | Automatic language detection from browser |
Adding a New Translation Key
- Add the key to
src/i18n/locales/en.jsonunder the appropriate section - Add the corresponding translated value to
es.jsonandfr.json - Use the key in a component via
const { t } = useTranslation()andt('section.keyName') - If the key might not exist in all locales yet, provide a fallback:
t('section.keyName', 'Fallback text')
Adding a New Language
- Create a new locale file (e.g.,
src/i18n/locales/de.json) with all keys translated - Import it in
src/i18n/index.tsand add it to theresourcesobject - Add an entry to the
supportedLanguagesarray with code, name, and flag - The
LanguageCodetype updates automatically via theas constassertion - The
LanguageSwitchercomponent picks up the new language automatically
Date, Time, and Number Formatting
STATUS: BUILT
The spec calls for locale-aware formatting of dates, times, numbers, and currencies. This is architecturally prepared but not yet implemented in the UI.
Spec requirements:
- All timestamps stored as UTC in the database, displayed in the user's timezone
- Date formats are locale-aware:
MM/DD/YYYYfor US,DD/MM/YYYYfor EU - Number formatting uses locale-aware separators (thousands, decimal)
- Currency formatting if/when monetization is added: store in smallest unit (cents) with currency code
- Calendar start day: locale-aware (Sunday for US, Monday for most of world)
Recommended approach (from the spec): Use the browser's Intl.DateTimeFormat and Intl.NumberFormat APIs, which handle locale-specific formatting natively. i18next also supports formatting via its interpolation system.
Pluralization
STATUS: BUILT
Pluralization is implemented using i18next's built-in _plural suffix convention. All countable strings in the translation files (en, es, fr) use proper pluralization forms.
The spec requires proper pluralization using ICU MessageFormat or equivalent. i18next supports this natively:
json
{
"challenges": {
"count_zero": "No challenges",
"count_one": "1 challenge",
"count_other": "{{count}} challenges"
}
}tsx
t('challenges.count', { count: 0 }) // "No challenges"
t('challenges.count', { count: 1 }) // "1 challenge"
t('challenges.count', { count: 7 }) // "7 challenges"This pattern should be adopted wherever counts are displayed (completions, notifications, community members, etc.).
RTL Support
STATUS: NOT STARTED
The spec calls for LTR only at launch. RTL readiness (logical CSS properties) is a codebase-wide concern that has not been systematically applied.
The spec recommends using logical CSS properties now to prepare for future RTL languages (Arabic, Hebrew):
| Physical Property | Logical Replacement |
|---|---|
margin-left | margin-inline-start |
margin-right | margin-inline-end |
padding-left | padding-inline-start |
text-align: left | text-align: start |
float: left | float: inline-start |
The current codebase uses Tailwind CSS utilities, which provide logical property variants (e.g., ms-4 for margin-inline-start). A systematic audit of all directional utilities would be needed before enabling RTL support.
Testing Strategy
STATUS: PARTIAL
The spec defines a testing strategy including pseudolocalization, string extraction audits, and CI checks. The strategy is documented but none of the actual test tools (pseudolocale generation, automated string extraction audits, CI translation coverage checks) are implemented yet.
Pseudolocalization (spec Section 6.1): Before any real translation work, create a pseudolocale that replaces ASCII with accented equivalents ("Welcome" becomes accented text), pads strings by 40%, and wraps in brackets to detect missing translations. Run automated UI tests with this pseudolocale to catch truncated text, overlapping elements, hardcoded strings (which appear un-transformed), and layout breaks.
String extraction audit (spec Section 6.2): Add an automated lint rule that flags hardcoded user-facing strings. Add a CI check that requires all new strings to use the t() function. Run quarterly audits to catch strings that slipped through.
Translation workflow (spec Section 6.3): When ready to add production-quality languages, extract all string files, send to a translation service (Crowdin, Phrase, Lokalise), have translators work in context with screenshots, review by native speakers, import translated strings, QA in target language.
Implementation Checklist
From the spec (Doc 16 Section 7), for every new feature developers should verify:
- [ ] All user-facing strings use the
t()translation function - [ ] No string concatenation for display text (use parameterized strings)
- [ ] Dates and times formatted with locale-aware functions
- [ ] Numbers formatted with locale-aware functions
- [ ] CSS uses logical properties (inline/block, not left/right)
- [ ] Text containers handle expansion (no fixed width)
- [ ] No text baked into images
- [ ] No assumptions about text direction (no LTR hardcoding)
- [ ] User-facing error messages use translation keys
- [ ] Email templates use the translation function
Coverage Status
The current translation coverage represents a foundation that proves the architecture works. Significant expansion is needed to cover the full platform:
| Area | Status | Notes |
|---|---|---|
| Common UI (buttons, labels) | Covered | ~20 keys in common.* |
| Navigation | Covered | ~35 keys in nav.* |
| Explore page | Covered | ~12 keys in explore.* |
| Search UI | Covered | ~14 keys in search.* |
| Filters | Covered | ~45 keys in filters.* (including all sub-categories) |
| Challenge detail | Covered | ~8 keys in challenge.* |
| Language UI | Covered | 2 keys in language.* |
| Auth (login, register) | Not covered | Keys needed for auth flow strings |
| Dashboard | Not covered | Hardcoded strings in dashboard components |
| Track Records | Not covered | All TR-related strings are hardcoded |
| Communities | Not covered | Community strings are hardcoded |
| Gamification | Not covered | XP, badges, levels, streaks all hardcoded |
| School admin | Not covered | School management strings are hardcoded |
| Vendor | Not covered | Vendor dashboard strings are hardcoded |
| Platform admin | Not covered | Admin panel strings are hardcoded |
| Notifications | Not covered | Notification templates are hardcoded |
| Error messages | Not covered | Error strings are hardcoded |
| Settings | Not covered | Settings page strings are hardcoded |
The nine components currently using useTranslation demonstrate the pattern. Expanding coverage is a matter of adding keys to locale files and replacing hardcoded strings in components -- no architectural changes needed.
Related Features
- Accounts & Authentication -- User profiles include a locale field. Language preference is a user setting that persists across sessions and devices.
- Explore & Discovery -- The Explore page is the most fully translated feature area, with all section titles, filter labels, and category names localized.
- Challenges -- Challenge content localization (vendor-created text, descriptions, instructions) is a future concern separate from UI translation.
- Notifications -- Notification templates are specified as translatable content. Email and push notification text should use translation keys.
- School Administration -- Schools may set a default locale for their students. School-context language settings interact with individual user preferences.