Skip to content

Commit 8cbefcf

Browse files
rahu1ramesharshiyaTW2021mohan-13
authored
BN-41 | Add. i18n Support (#10)
* BN-41.Add.Dependencies to integrate react-i18next. * BN-41.Add i18n configuration. * BN-41.Add translations for English and French and add README file * Add a language switcher to test translations. * Apply translations. * Merge local and standard-config changes. * Apply translations to PatientDetails.tsx * Change translation file structures. * BN-41 | Refactor. Remove LanguageSwitcher component * BN-41 | Fix. Merge translations function to be synchronous * BN-41 | Refactor. Extract cookies to constant file and enhance naming conventions * BN-41 | Refactor. Remove i18n http backend as we have custom logic for fetching and merging translation * BN-41 | Refactor. Extract translation service * BN-41 | WIP. Revert Translation keys to fix tests * BN-41 | Add. Test and mock configuration for i18n * BN-41 | Refactor. Remove unused variables and fix lint error for imports * BN-41 | Add. Tests for translation service * BN-41 | Fix. Prettier formatting errors * BN-41 | Fix. Remove Additional Locales * BN-41 | Add. Base Path Alias * BN-41 | Refactor. Clean i18n Initialization * BN-41 | Add. i18n Guide * BN-41 | Refactor. Save App Constants Together * BN-41 | Fix. Constant Casing * BN-41 | Refactor. Use TranslationProvider * BN-41 | Add. i18n Keys To ConditionsTable * BN-41 | Add. i18n Keys To AllergiesTable * BN-41 | Add. i18n Keys To ExpandableDataTable * BN-41 | Add. i18n Keys To PatientDetails * BN-41 | Add. i18n Tests For Components * BN-41 | Add. i18n Keys For Error Messages * BN-41 | Fix. Remove i18n Debug * BN-41 | Refactor. Use i18n For Tests * BN-41 | Fix. Circular Import * BN-41 | Add. Circular Dependency Footnote --------- Co-authored-by: arshiyaTW2021 <arshiya.shehzad@thoughtworks.com> Co-authored-by: MOHANKUMAR T <mohan13081999@gmail.com>
1 parent aa2159c commit 8cbefcf

33 files changed

+2668
-1643
lines changed

docs/i18n-guide.md

Lines changed: 326 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,326 @@
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)

jest.config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const config: Config.InitialOptions = {
1010
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
1111
moduleNameMapper: {
1212
'\\.(css|less|scss|sass)$': '<rootDir>/src/__mocks__/styleMock.ts',
13+
'@/(.*)$': ['<rootDir>/src/$1'],
1314
'@components/(.*)$': ['<rootDir>/src/components/$1'],
1415
'@contexts/(.*)$': ['<rootDir>/src/contexts/$1'],
1516
'@constants/(.*)$': ['<rootDir>/src/constants/$1'],
@@ -30,6 +31,8 @@ const config: Config.InitialOptions = {
3031
'<rootDir>/src/setupTests.ts',
3132
'<rootDir>/src/types',
3233
'<rootDir>/src/.*/stories/',
34+
'<rootDir>/src/i18n.ts',
35+
'<rootDir>/src/setupTests.i18n.ts',
3336
],
3437
coverageThreshold: {
3538
global: {

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,11 @@
2121
"@testing-library/react-hooks": "^8.0.1",
2222
"axios": "^1.8.4",
2323
"date-fns": "^4.1.0",
24+
"i18next": "^24.2.3",
25+
"i18next-browser-languagedetector": "^8.0.4",
2426
"react": "^19.0.0",
2527
"react-dom": "^19.0.0",
28+
"react-i18next": "^15.4.1",
2629
"react-router-dom": "^7.3.0",
2730
"workbox-core": "^7.0.0",
2831
"workbox-expiration": "^7.0.0",

0 commit comments

Comments
 (0)