|
| 1 | +# Internationalization (i18n) Guide |
| 2 | + |
| 3 | +This guide provides comprehensive documentation for the internationalization (i18n) implementation in the Bahmni Clinical Frontend application. |
| 4 | + |
| 5 | +## Table of Contents |
| 6 | + |
| 7 | +1. [Overview & Architecture](#overview--architecture) |
| 8 | +2. [Configuration Constants](#configuration-constants) |
| 9 | +3. [Translation Management](#translation-management) |
| 10 | +4. [Development Guidelines](#development-guidelines) |
| 11 | +5. [Usage Examples](#usage-examples) |
| 12 | +6. [Configuration Guide](#configuration-guide) |
| 13 | + |
| 14 | +## Overview & Architecture |
| 15 | + |
| 16 | +The Bahmni Clinical Frontend application uses [i18next](https://www.i18next.com/) with [react-i18next](https://react.i18next.com/) for internationalization. The implementation includes: |
| 17 | + |
| 18 | +### Core Components |
| 19 | + |
| 20 | +- **i18next**: The core internationalization framework |
| 21 | +- **react-i18next**: React bindings for i18next |
| 22 | +- **i18next-browser-languagedetector**: Detects the user's preferred language from browser settings |
| 23 | + |
| 24 | +### Key Features |
| 25 | + |
| 26 | +- **Asynchronous Loading**: Translations are loaded asynchronously when the application starts |
| 27 | +- **Dual Source Strategy**: Translations can come from both bundled files and configuration files |
| 28 | +- **Fallback Mechanism**: English is used as a fallback when translations are missing |
| 29 | +- **Namespace Support**: Translations are organized by namespaces (e.g., 'clinical') |
| 30 | +- **Language Detection**: Automatically detects user's preferred language from localStorage |
| 31 | +- **Error Handling**: Comprehensive error handling for missing or invalid translations |
| 32 | + |
| 33 | +### Initialization Flow |
| 34 | + |
| 35 | +1. The `TranslationProvider` component initializes translations after the notification service is ready |
| 36 | +2. The `initI18n` function in `src/i18n.ts` is called to set up i18next |
| 37 | +3. User's preferred locale is determined from storage key or defaults to English |
| 38 | +4. Translations are fetched from both bundled and config sources |
| 39 | +5. i18next is initialized with the merged translations |
| 40 | + |
| 41 | +## Configuration Constants |
| 42 | + |
| 43 | +The i18n implementation relies on several constants defined in `src/constants/app.ts`: |
| 44 | + |
| 45 | +### Translation URL Templates |
| 46 | + |
| 47 | +These constants define the URL patterns for fetching translations: |
| 48 | + |
| 49 | +- **CONFIG_TRANSLATIONS_URL_TEMPLATE**: Points to configuration-specific translations that can be customized per deployment |
| 50 | +- **BUNDLED_TRANSLATIONS_URL_TEMPLATE**: Points to bundled translations that ship with the application |
| 51 | + |
| 52 | +### Locale Settings |
| 53 | + |
| 54 | +```typescript |
| 55 | +export const DEFAULT_LOCALE = "en"; |
| 56 | +export const LOCALE_STORAGE_KEY = "NG_TRANSLATE_LANG_KEY"; |
| 57 | +``` |
| 58 | + |
| 59 | +These constants define: |
| 60 | + |
| 61 | +- **DEFAULT_LOCALE**: The fallback locale (English) used when a translation is missing or when the user's preferred locale is invalid |
| 62 | +- **LOCALE_STORAGE_KEY**: The name of the cookie used to store the user's preferred locale |
| 63 | + |
| 64 | +### Namespace Configuration |
| 65 | + |
| 66 | +```typescript |
| 67 | +export const CLINICAL_NAMESPACE = "clinical"; |
| 68 | +``` |
| 69 | + |
| 70 | +This constant defines the default namespace for translations, which helps organize translations by application section. |
| 71 | + |
| 72 | +## Translation Management |
| 73 | + |
| 74 | +### File Structure and Naming Conventions |
| 75 | + |
| 76 | +Translation files follow a consistent naming pattern: |
| 77 | + |
| 78 | +```text |
| 79 | +locale_[language-code].json |
| 80 | +``` |
| 81 | + |
| 82 | +For example: |
| 83 | + |
| 84 | +- `locale_en.json` for English |
| 85 | +- `locale_es.json` for Spanish |
| 86 | +- `locale_fr.json` for French |
| 87 | + |
| 88 | +These files are stored in two locations: |
| 89 | + |
| 90 | +- **Bundled translations**: `/public/locales/locale_[lang].json` |
| 91 | +- **Config translations**: `<CONFIG_REPO>/openmrs/i18n/clinical/locale_[lang].json` |
| 92 | + |
| 93 | +### Bundled vs Config Translations |
| 94 | + |
| 95 | +The application implements a dual-source strategy for translations: |
| 96 | + |
| 97 | +1. **Bundled Translations**: These are packaged with the application and serve as the base translations. |
| 98 | +2. **Config Translations**: These are deployment-specific and can override bundled translations. |
| 99 | + |
| 100 | +When both sources provide a translation for the same key, the config translation takes precedence. This allows for customization without modifying the core application. |
| 101 | + |
| 102 | +### Translation Loading and Merging |
| 103 | + |
| 104 | +The `getTranslations` function in `translationService.ts` handles loading and merging translations: |
| 105 | + |
| 106 | +1. Fetches translations from both bundled and config sources using the `getMergedTranslations` function |
| 107 | +2. Merges them with config translations taking precedence over bundled translations |
| 108 | +3. For non-English locales, also loads English translations as fallback |
| 109 | +4. Organizes translations by language code and namespace following the i18next resource structure |
| 110 | + |
| 111 | +The merging process is handled by the `getMergedTranslations` function: |
| 112 | + |
| 113 | +```typescript |
| 114 | +const getMergedTranslations = async ( |
| 115 | + lang: string, |
| 116 | +): Promise<Record<string, string>> => { |
| 117 | + let bundledTranslations: Record<string, string> = {}; |
| 118 | + let configTranslations: Record<string, string> = {}; |
| 119 | + |
| 120 | + bundledTranslations = await get<Record<string, string>>( |
| 121 | + BUNDLED_TRANSLATIONS_URL_TEMPLATE(lang), |
| 122 | + ); |
| 123 | + |
| 124 | + configTranslations = await get<Record<string, string>>( |
| 125 | + CONFIG_TRANSLATIONS_URL_TEMPLATE(lang), |
| 126 | + ); |
| 127 | + |
| 128 | + return { ...bundledTranslations, ...configTranslations }; |
| 129 | +}; |
| 130 | +``` |
| 131 | + |
| 132 | +This function: |
| 133 | + |
| 134 | +- Fetches translations from both bundled and configuration sources |
| 135 | +- Uses the spread operator to merge them, with config translations overriding bundled ones |
| 136 | +- Either source can fail independently without affecting the other |
| 137 | + |
| 138 | +### Error Handling and Fallbacks |
| 139 | + |
| 140 | +The implementation includes robust error handling: |
| 141 | + |
| 142 | +- If a locale is invalid or not found in localStorage, it falls back to the default locale (English) |
| 143 | +- The `getUserPreferredLocale` function handles this fallback: |
| 144 | + ```typescript |
| 145 | + export const getUserPreferredLocale = (): string => { |
| 146 | + const localeStorageKey = localStorage.getItem(LOCALE_STORAGE_KEY); |
| 147 | + const userLocale = localeStorageKey || DEFAULT_LOCALE; |
| 148 | + return userLocale; |
| 149 | + }; |
| 150 | + ``` |
| 151 | +- For non-English locales, English translations are always loaded as fallback: |
| 152 | + ```typescript |
| 153 | + // Add English fallback for non-English languages |
| 154 | + if (lang !== "en") { |
| 155 | + translations.en = { |
| 156 | + [namespace]: await getMergedTranslations("en"), |
| 157 | + }; |
| 158 | + } |
| 159 | + ``` |
| 160 | +- This ensures that even if a translation is missing in the requested language, the English version will be displayed |
| 161 | + |
| 162 | +#### Translation File Fetching and Error Handling |
| 163 | + |
| 164 | +- Internationalization will only function after i18n is properly initialized. If initialization fails, the application will fall back to using keys instead of translated text. |
| 165 | +- A separate axios client is used to fetch translation files (see `getTranslationFile` function) rather than the main API service. |
| 166 | +- This separate client is necessary because the main API service has a dependency on the notification service, which would create a circular dependency issue if used for translation files. |
| 167 | +- If there's a failure in fetching a particular locale, errors will be logged to the console, but the notification service will not display any errors to the user. |
| 168 | +- The implementation gracefully handles missing translation files by returning an empty object, allowing the application to continue functioning with available translations or fallbacks. |
| 169 | + |
| 170 | +## Development Guidelines |
| 171 | + |
| 172 | +### Adding New Translations |
| 173 | + |
| 174 | +To add new translations: |
| 175 | + |
| 176 | +1. Identify the appropriate namespace (usually 'clinical') |
| 177 | +2. Add the new key-value pair to the relevant locale files |
| 178 | +3. For new features, add translations for all supported languages |
| 179 | + |
| 180 | +### Best Practices for Keys and Namespaces |
| 181 | + |
| 182 | +- **Be Consistent**: Use consistent naming patterns for similar concepts |
| 183 | +- **Be Descriptive**: Keys should be self-explanatory and indicate their purpose |
| 184 | +- **Avoid Hardcoding**: Never hardcode text that might need translation |
| 185 | +- **Context Comments**: Add comments for translators when context might be unclear |
| 186 | + |
| 187 | +### Testing Translations |
| 188 | + |
| 189 | +When adding new translations, consider adding tests to verify: |
| 190 | + |
| 191 | +- The translation key exists in all supported languages |
| 192 | +- The translation is correctly loaded and applied |
| 193 | +- The fallback mechanism works as expected |
| 194 | + |
| 195 | +### Handling Dynamic Content |
| 196 | + |
| 197 | +For dynamic content: |
| 198 | + |
| 199 | +- Use interpolation with the `{{variable}}` syntax |
| 200 | +- For pluralization, use i18next's plural features |
| 201 | +- For formatting (dates, numbers, etc.), use appropriate formatting utilities |
| 202 | +- Consider context when translating dynamic content |
| 203 | + |
| 204 | +## Usage Examples |
| 205 | + |
| 206 | +### Basic Translation Usage |
| 207 | + |
| 208 | +```tsx |
| 209 | +import { useTranslation } from "react-i18next"; |
| 210 | + |
| 211 | +function MyComponent() { |
| 212 | + const { t } = useTranslation(); |
| 213 | + |
| 214 | + return <h1>{t("greeting")}</h1>; // Renders "Hello" in English |
| 215 | +} |
| 216 | +``` |
| 217 | + |
| 218 | +### Handling Plurals and Interpolation |
| 219 | + |
| 220 | +```tsx |
| 221 | +import { useTranslation } from "react-i18next"; |
| 222 | + |
| 223 | +function ItemCount({ count }: { count: number }) { |
| 224 | + const { t } = useTranslation(); |
| 225 | + |
| 226 | + return ( |
| 227 | + <p> |
| 228 | + {t("items.count", { count })} |
| 229 | + {/* Can render "1 item" or "5 items" depending on count */} |
| 230 | + </p> |
| 231 | + ); |
| 232 | +} |
| 233 | +``` |
| 234 | + |
| 235 | +### Dynamic Language Switching |
| 236 | + |
| 237 | +```tsx |
| 238 | +import { useTranslation } from "react-i18next"; |
| 239 | +import Cookies from "js-cookie"; |
| 240 | +import { LOCALE_STORAGE_KEY } from "@constants/app"; |
| 241 | + |
| 242 | +function LanguageSwitcher() { |
| 243 | + const { i18n } = useTranslation(); |
| 244 | + |
| 245 | + const changeLanguage = (lang: string) => { |
| 246 | + i18n.changeLanguage(lang); |
| 247 | + Cookies.set(LOCALE_STORAGE_KEY, lang); |
| 248 | + }; |
| 249 | + |
| 250 | + return ( |
| 251 | + <div> |
| 252 | + <button onClick={() => changeLanguage("en")}>English</button> |
| 253 | + <button onClick={() => changeLanguage("es")}>Español</button> |
| 254 | + <button onClick={() => changeLanguage("fr")}>Français</button> |
| 255 | + </div> |
| 256 | + ); |
| 257 | +} |
| 258 | +``` |
| 259 | + |
| 260 | +## Configuration Guide |
| 261 | + |
| 262 | +### Setting Up New Locales |
| 263 | + |
| 264 | +To add support for a new locale: |
| 265 | + |
| 266 | +1. Create new translation files: |
| 267 | + |
| 268 | + - `/public/locales/locale_[lang].json` for bundled translations |
| 269 | + - `/<CONFIG_REPO>/openmrs/i18n/clinical/locale_[lang].json` for config translations |
| 270 | + |
| 271 | +2. Ensure the locale code is valid according to [BCP 47](https://tools.ietf.org/html/bcp47) |
| 272 | + |
| 273 | +3. Add translations for all existing keys in the default locale |
| 274 | + |
| 275 | +4. Update any language selection UI to include the new locale |
| 276 | + |
| 277 | +### Configuring URL Templates |
| 278 | + |
| 279 | +If you need to change the location of translation files: |
| 280 | + |
| 281 | +1. Update the URL templates in `src/constants/app.ts`: |
| 282 | + |
| 283 | + ```typescript |
| 284 | + export const CONFIG_TRANSLATIONS_URL_TEMPLATE = (lang: string) => |
| 285 | + `/your/custom/path/locale_${lang}.json`; |
| 286 | + export const BUNDLED_TRANSLATIONS_URL_TEMPLATE = (lang: string) => |
| 287 | + `/your/custom/bundled/path/locale_${lang}.json`; |
| 288 | + ``` |
| 289 | + |
| 290 | +2. Ensure the new paths are accessible and contain valid translation files |
| 291 | + |
| 292 | +### Managing Cookie Settings |
| 293 | + |
| 294 | +The application uses cookies to persist the user's language preference: |
| 295 | + |
| 296 | +1. The cookie name is defined by `LOCALE_STORAGE_KEY` in `src/constants/app.ts` |
| 297 | +2. The default value is `'NG_TRANSLATE_LANG_KEY'` for compatibility with AngularJS applications |
| 298 | +3. To change the cookie name, update this constant |
| 299 | + |
| 300 | +### Namespace Organization |
| 301 | + |
| 302 | +The application uses namespaces to organize translations: |
| 303 | + |
| 304 | +1. The default namespace is defined by `CLINICAL_NAMESPACE` in `src/constants/app.ts` |
| 305 | +2. To add a new namespace: |
| 306 | + - Update the `ns` array in the i18next initialization in `src/i18n.ts` |
| 307 | + - Create translation files for the new namespace |
| 308 | + - Use the namespace when accessing translations: `t('key', { ns: 'yourNamespace' })` |
| 309 | + |
| 310 | +### Environment-Specific Configurations |
| 311 | + |
| 312 | +For different environments (development, testing, production): |
| 313 | + |
| 314 | +1. Use environment variables to configure translation paths |
| 315 | +2. Consider using different fallback strategies for development vs. production |
| 316 | +3. In development, you might want to show missing translation keys |
| 317 | +4. In production, ensure all translations are available and fallbacks are in place |
| 318 | + |
| 319 | +--- |
| 320 | + |
| 321 | +## References |
| 322 | + |
| 323 | +- [i18next Documentation](https://www.i18next.com/) |
| 324 | +- [react-i18next Documentation](https://react.i18next.com/) |
| 325 | +- [i18next-browser-languagedetector](https://github.com/i18next/i18next-browser-languageDetector) |
| 326 | +- [BCP 47 Language Tags](https://tools.ietf.org/html/bcp47) |
0 commit comments