Skip to content

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:

  1. i18next -- Core translation engine. Handles key lookup, interpolation, pluralization, and fallback logic.
  2. react-i18next -- React integration. Provides the useTranslation hook and Trans component for JSX interpolation.
  3. 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 i18n

This runs the configuration in src/i18n/index.ts before the React tree renders, ensuring translations are available from the first paint.

Configuration details:

SettingValuePurpose
fallbackLng'en'English is the default if detection fails or a key is missing
escapeValuefalseReact already escapes rendered values; double-escaping would break display
debugimport.meta.env.DEVLogs missing keys and detection results in development only
Detection orderlocalStorage > navigator > htmlTagUser's explicit choice wins, then browser setting, then page lang attribute
CachelocalStorage key docurious-languagePersists 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:

CodeNameFlagLocale FileKeys
enEnglishUS flagsrc/i18n/locales/en.json~130
esEspanolSpain flagsrc/i18n/locales/es.json~130
frFrancaisFrance flagsrc/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 UI

Key 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: bestFitSubtitleDefault not just subtitle2

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):

PrioritySpec (Doc 16 Section 4.1)Current Implementation
1stUser's explicit setting (stored in profile)localStorage (docurious-language)
2ndBrowser/device localenavigator.language
3rdGeoIP-based suggestionNot implemented
4thDefault: en-USfallbackLng: '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 supportedLanguages array
  • 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 useEffect with mousedown listener)

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:

AreaComponents Using t()Key Namespace
NavigationuseNavigationItemsnav.*
Explore pageExplore, ExploreCategoryViewexplore.*
SearchSearchFilterBarsearch.*
FiltersFilterModal, FilterSidebar, FilterButton, AppliedFiltersfilters.*
LanguageLanguageSwitcherlanguage.*

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.json

The 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.json namespace when an admin page is visited
  • Smaller initial bundle -- The common namespace 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 TypeTranslatable?Method
UI strings (buttons, labels, headings)YesTranslation files
Platform emailsYesLocalized templates
Error messagesYesTranslation files
Legal documentsYesSeparate translated versions
Help center articlesYesCMS-based translation
Challenge content (vendor-created)NoVendor creates in their language; translation support planned
Track Records (user-generated)NoUser's language
Community posts (user-generated)NoUser's language
Reflection responses (user-generated)NoUser'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.

ActionGeneral UserStudentParentTeacherSchool AdminPlatform Admin
Set personal language preferenceYesYesYesYesYesYes
See UI in selected languageYesYesYesYesYesYes
Switch language at any timeYesYesYesYesYesYes
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

ConstraintValueSource
Supported languages at launch1 (English)Spec Doc 16 Section 1
Supported languages in prototype3 (en, es, fr)src/i18n/index.ts
Translation keys per language~130Current locale file size
Namespace count1 (translation)Current; spec recommends 10
Locale formatBCP 47 (e.g., en-US, es-MX)Spec Doc 16 Section 4.1
User profile locale fieldvarchar(10), default 'en-US'Spec Doc 16 Section 4.3
Challenge locale fieldvarchar(10), default 'en'Spec Doc 16 Section 4.3
Database text encodingUTF-8PostgreSQL default, verified
Text expansion buffer40%Spec Doc 16 Section 5.1 (pseudolocalization target)
Max string length assumptionGerman ~30% longer, French ~20% longerSpec design guidelines
Detection fallbackEnglish (en)fallbackLng in i18n config
Persistence mechanismlocalStorage key docurious-languagei18n detection config

Design Decisions

These decisions have been resolved and are documented in the spec or established through implementation:

DecisionResolutionRationale
Translation libraryreact-i18next (i18next core)Industry standard for React. Supports namespaces, lazy loading, pluralization, interpolation, and has a large ecosystem of plugins.
Single namespace vs. splitSingle namespace for now; split when feature count growsSimpler to maintain during prototype stage. Migration path is clear and non-breaking.
URL strategy for localePath prefix (/en/, /es/) recommended by specNot yet implemented in routing. Simpler infrastructure than subdomains; good SEO. Routes should support adding a prefix later.
Language detection orderlocalStorage > navigator > htmlTagSpec calls for user profile > browser > GeoIP > default. localStorage is the frontend equivalent of user profile until backend integration.
RTL supportLTR only at launchSpec recommends using logical CSS properties (margin-inline-start instead of margin-left) now to ease future RTL support.
Text expansion handlingNo fixed widths on text containersSpec requires all containers to handle 40% text expansion. Buttons use min-width, not width. Overflow uses ellipsis with tooltip.
Hardcoded string policyZero tolerance; all user-facing strings through t()Enforced by convention. The spec recommends a lint rule and CI check (not yet implemented).
PseudolocalizationNot yet implementedSpec recommends running pseudolocale testing before any real translation work begins. Would replace ASCII with accented equivalents and pad by 40%.
Content directionLTR only; logical CSS properties where possibleFuture RTL support (Arabic, Hebrew) becomes significantly easier with logical properties.
Calendar start dayLocale-awareSunday for US, Monday for most of world. Not yet implemented but specified for when calendar features are built.

Technical Implementation

Key Files

PurposeFile Path
i18n configurationsrc/i18n/index.ts
English translationssrc/i18n/locales/en.json
Spanish translationssrc/i18n/locales/es.json
French translationssrc/i18n/locales/fr.json
App initializationsrc/main.tsx (imports ./i18n)

Components

ComponentFile PathDescription
LanguageSwitchersrc/components/common/LanguageSwitcher.tsxDropdown for selecting app language. Supports compact (icon-only) and full (flag + name) modes.

Consuming Components

These components actively use useTranslation for localized UI:

ComponentFile PathKeys Used
Exploresrc/pages/Explore.tsxexplore.*
ExploreCategoryViewsrc/pages/explore/ExploreCategoryView.tsxexplore.*, search.*
useNavigationItemssrc/components/layout/navigation/useNavigationItems.tsnav.*
SearchFilterBarsrc/components/search/SearchFilterBar.tsxsearch.*
FilterModalsrc/components/filters/FilterModal.tsxfilters.*
FilterSidebarsrc/components/filters/FilterSidebar.tsxfilters.*
FilterButtonsrc/components/filters/FilterButton.tsxfilters.*
AppliedFilterssrc/components/filters/AppliedFilters.tsxfilters.*

Dependencies

PackageVersionPurpose
i18next^23.11.5Core translation engine
react-i18next^14.1.2React bindings (hooks, components)
i18next-browser-languagedetector^7.2.1Automatic language detection from browser

Adding a New Translation Key

  1. Add the key to src/i18n/locales/en.json under the appropriate section
  2. Add the corresponding translated value to es.json and fr.json
  3. Use the key in a component via const { t } = useTranslation() and t('section.keyName')
  4. If the key might not exist in all locales yet, provide a fallback: t('section.keyName', 'Fallback text')

Adding a New Language

  1. Create a new locale file (e.g., src/i18n/locales/de.json) with all keys translated
  2. Import it in src/i18n/index.ts and add it to the resources object
  3. Add an entry to the supportedLanguages array with code, name, and flag
  4. The LanguageCode type updates automatically via the as const assertion
  5. The LanguageSwitcher component 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/YYYY for US, DD/MM/YYYY for 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 PropertyLogical Replacement
margin-leftmargin-inline-start
margin-rightmargin-inline-end
padding-leftpadding-inline-start
text-align: lefttext-align: start
float: leftfloat: 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:

AreaStatusNotes
Common UI (buttons, labels)Covered~20 keys in common.*
NavigationCovered~35 keys in nav.*
Explore pageCovered~12 keys in explore.*
Search UICovered~14 keys in search.*
FiltersCovered~45 keys in filters.* (including all sub-categories)
Challenge detailCovered~8 keys in challenge.*
Language UICovered2 keys in language.*
Auth (login, register)Not coveredKeys needed for auth flow strings
DashboardNot coveredHardcoded strings in dashboard components
Track RecordsNot coveredAll TR-related strings are hardcoded
CommunitiesNot coveredCommunity strings are hardcoded
GamificationNot coveredXP, badges, levels, streaks all hardcoded
School adminNot coveredSchool management strings are hardcoded
VendorNot coveredVendor dashboard strings are hardcoded
Platform adminNot coveredAdmin panel strings are hardcoded
NotificationsNot coveredNotification templates are hardcoded
Error messagesNot coveredError strings are hardcoded
SettingsNot coveredSettings 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.


  • 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.

DoCurious Platform Documentation