@zohodesk/i18n 1.0.0-beta.34 → 1.0.0-beta.36-murphy
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/murphy/01-MURPHY_OVERVIEW.md +148 -0
- package/docs/murphy/02-MURPHY_ARCHITECTURE.md +283 -0
- package/docs/murphy/03-MURPHY_BACKEND_CONFIG.md +337 -0
- package/docs/murphy/04-MURPHY_FRONTEND_INIT.md +437 -0
- package/docs/murphy/05-MURPHY_DESK_CLIENT_USAGE.md +467 -0
- package/docs/murphy/06-MURPHY_I18N_INTEGRATION.md +402 -0
- package/docs/murphy/07-MURPHY_WHY_I18N_APPROACH.md +391 -0
- package/es/components/DateTimeDiffFormat.js +5 -19
- package/es/components/FormatText.js +2 -2
- package/es/components/HOCI18N.js +32 -43
- package/es/components/I18N.js +2 -13
- package/es/components/I18NProvider.js +0 -9
- package/es/components/PluralFormat.js +3 -5
- package/es/components/UserTimeDiffFormat.js +5 -9
- package/es/components/__tests__/DateTimeDiffFormat.spec.js +157 -221
- package/es/components/__tests__/FormatText.spec.js +2 -2
- package/es/components/__tests__/HOCI18N.spec.js +2 -4
- package/es/components/__tests__/I18N.spec.js +6 -4
- package/es/components/__tests__/I18NProvider.spec.js +4 -4
- package/es/components/__tests__/PluralFormat.spec.js +2 -2
- package/es/components/__tests__/UserTimeDiffFormat.spec.js +249 -348
- package/es/index.js +1 -0
- package/es/utils/__tests__/jsxTranslations.spec.js +3 -7
- package/es/utils/errorReporter.js +35 -0
- package/es/utils/index.js +42 -92
- package/es/utils/jsxTranslations.js +34 -52
- package/lib/I18NContext.js +2 -7
- package/lib/components/DateTimeDiffFormat.js +46 -87
- package/lib/components/FormatText.js +18 -41
- package/lib/components/HOCI18N.js +24 -59
- package/lib/components/I18N.js +27 -64
- package/lib/components/I18NProvider.js +27 -63
- package/lib/components/PluralFormat.js +24 -50
- package/lib/components/UserTimeDiffFormat.js +43 -72
- package/lib/components/__tests__/DateTimeDiffFormat.spec.js +95 -165
- package/lib/components/__tests__/FormatText.spec.js +3 -10
- package/lib/components/__tests__/HOCI18N.spec.js +3 -14
- package/lib/components/__tests__/I18N.spec.js +4 -12
- package/lib/components/__tests__/I18NProvider.spec.js +8 -23
- package/lib/components/__tests__/PluralFormat.spec.js +3 -11
- package/lib/components/__tests__/UserTimeDiffFormat.spec.js +157 -225
- package/lib/index.js +25 -23
- package/lib/utils/__tests__/jsxTranslations.spec.js +1 -12
- package/lib/utils/errorReporter.js +44 -0
- package/lib/utils/index.js +49 -125
- package/lib/utils/jsxTranslations.js +61 -100
- package/package.json +1 -1
- package/src/index.js +5 -0
- package/src/utils/errorReporter.js +41 -0
- package/src/utils/index.js +8 -1
- package/src/utils/jsxTranslations.js +8 -1
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
# Murphy Integration in i18n Library
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The i18n library (`@zohodesk/i18n`) integrates with Murphy to track translation failures - when users see fallback text instead of proper translations.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Why Track i18n Errors?
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
+-------------------------------------------------------------------+
|
|
13
|
+
| THE PROBLEM: SILENT TRANSLATION FAILURES |
|
|
14
|
+
+-------------------------------------------------------------------+
|
|
15
|
+
| |
|
|
16
|
+
| Without Tracking: |
|
|
17
|
+
| |
|
|
18
|
+
| User sees: "desk.tickets.save.button" |
|
|
19
|
+
| Developer: Has no idea this is happening! |
|
|
20
|
+
| |
|
|
21
|
+
| Causes: |
|
|
22
|
+
| +-- Missing translation key |
|
|
23
|
+
| +-- i18n object not initialized |
|
|
24
|
+
| +-- Wrong key name used |
|
|
25
|
+
| +-- Bundle not loaded |
|
|
26
|
+
| |
|
|
27
|
+
| With Murphy Tracking: |
|
|
28
|
+
| |
|
|
29
|
+
| User sees: "desk.tickets.save.button" |
|
|
30
|
+
| Murphy Dashboard: "I18N_MISSING_KEY: desk.tickets.save.button" |
|
|
31
|
+
| Developer: Can see and fix immediately! |
|
|
32
|
+
| |
|
|
33
|
+
+-------------------------------------------------------------------+
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Implementation Architecture
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
+-------------------------------------------------------------------+
|
|
42
|
+
| I18N LIBRARY MURPHY INTEGRATION |
|
|
43
|
+
+-------------------------------------------------------------------+
|
|
44
|
+
| |
|
|
45
|
+
| +-------------------------------------------------------------+ |
|
|
46
|
+
| | errorReporter.js (NEW FILE) | |
|
|
47
|
+
| | Path: /jsapps/i18n/src/utils/errorReporter.js | |
|
|
48
|
+
| | | |
|
|
49
|
+
| | Error Types: | |
|
|
50
|
+
| | +--------------------------------------------------------+ | |
|
|
51
|
+
| | | I18N_UNDEFINED_OBJECT | i18n object not initialized | | |
|
|
52
|
+
| | | I18N_MISSING_KEY | Translation key not found | | |
|
|
53
|
+
| | | I18N_NUMERIC_FALLBACK | Numeric ID showing fallback | | |
|
|
54
|
+
| | +--------------------------------------------------------+ | |
|
|
55
|
+
| | | |
|
|
56
|
+
| | Deduplication: | |
|
|
57
|
+
| | +-- Uses Set() to track reported keys | |
|
|
58
|
+
| | +-- Each key reported only once per session | |
|
|
59
|
+
| | | |
|
|
60
|
+
| | Safety Check: | |
|
|
61
|
+
| | +-- isMurphyAvailable() checks if global murphy exists | |
|
|
62
|
+
| | +-- Silently skips if Murphy not present | |
|
|
63
|
+
| +-------------------------------------------------------------+ |
|
|
64
|
+
| | |
|
|
65
|
+
| v |
|
|
66
|
+
| +-------------------------------------------------------------+ |
|
|
67
|
+
| | Integration Points | |
|
|
68
|
+
| | | |
|
|
69
|
+
| | getI18NValue() (utils/index.js) | |
|
|
70
|
+
| | +-- Called for text translations | |
|
|
71
|
+
| | +-- Reports MISSING_KEY or NUMERIC_FALLBACK | |
|
|
72
|
+
| | | |
|
|
73
|
+
| | getI18NComponent() (utils/jsxTranslations.js) | |
|
|
74
|
+
| | +-- Called for JSX translations with components | |
|
|
75
|
+
| | +-- Reports UNDEFINED_OBJECT or MISSING_KEY | |
|
|
76
|
+
| +-------------------------------------------------------------+ |
|
|
77
|
+
| |
|
|
78
|
+
+-------------------------------------------------------------------+
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Code Implementation
|
|
84
|
+
|
|
85
|
+
### 1. errorReporter.js
|
|
86
|
+
|
|
87
|
+
```javascript
|
|
88
|
+
// Path: /jsapps/i18n/src/utils/errorReporter.js
|
|
89
|
+
|
|
90
|
+
// Error types for categorization in Murphy dashboard
|
|
91
|
+
export const I18N_ERROR_TYPES = {
|
|
92
|
+
UNDEFINED_OBJECT: 'I18N_UNDEFINED_OBJECT', // i18n not initialized
|
|
93
|
+
MISSING_KEY: 'I18N_MISSING_KEY', // Key not found
|
|
94
|
+
NUMERIC_FALLBACK: 'I18N_NUMERIC_FALLBACK' // Numeric ID as fallback
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// Deduplication - report each unique key only once per session
|
|
98
|
+
const reportedKeys = new Set();
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Check if Murphy SDK is available globally
|
|
102
|
+
*/
|
|
103
|
+
function isMurphyAvailable() {
|
|
104
|
+
return typeof murphy !== 'undefined' && typeof murphy.error === 'function';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Report i18n error to Murphy
|
|
109
|
+
* @param {string} type - Error type from I18N_ERROR_TYPES
|
|
110
|
+
* @param {string} key - The i18n key that failed
|
|
111
|
+
*/
|
|
112
|
+
export function reportI18NError(type, key) {
|
|
113
|
+
// Deduplicate: only report each unique key once per session
|
|
114
|
+
const dedupeKey = `${type}:${key}`;
|
|
115
|
+
if (reportedKeys.has(dedupeKey)) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
reportedKeys.add(dedupeKey);
|
|
119
|
+
|
|
120
|
+
// Report to Murphy if available
|
|
121
|
+
if (isMurphyAvailable()) {
|
|
122
|
+
const error = new Error(`i18n ${type}: ${key}`);
|
|
123
|
+
error.name = type;
|
|
124
|
+
|
|
125
|
+
murphy.error(error, undefined, {
|
|
126
|
+
customTags: {
|
|
127
|
+
errorType: type,
|
|
128
|
+
i18nKey: key,
|
|
129
|
+
category: 'i18n'
|
|
130
|
+
},
|
|
131
|
+
preventClientGrouping: true
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Clear reported keys (useful for testing)
|
|
138
|
+
*/
|
|
139
|
+
export function clearReportedKeys() {
|
|
140
|
+
reportedKeys.clear();
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### 2. Integration in getI18NValue()
|
|
145
|
+
|
|
146
|
+
```javascript
|
|
147
|
+
// Path: /jsapps/i18n/src/utils/index.js
|
|
148
|
+
|
|
149
|
+
import { reportI18NError, I18N_ERROR_TYPES } from './errorReporter';
|
|
150
|
+
|
|
151
|
+
export const getI18NValue = (i18n) => (key, values) => {
|
|
152
|
+
// Check if i18n object exists
|
|
153
|
+
if (typeof i18n === 'undefined') {
|
|
154
|
+
reportI18NError(I18N_ERROR_TYPES.UNDEFINED_OBJECT, 'i18n_object');
|
|
155
|
+
return key;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Look up translation
|
|
159
|
+
let i18nStr = i18n[key];
|
|
160
|
+
|
|
161
|
+
// If not found, report error and return fallback
|
|
162
|
+
if (i18nStr === undefined) {
|
|
163
|
+
const isNumeric = /^\d+$/.test(key);
|
|
164
|
+
reportI18NError(
|
|
165
|
+
isNumeric ? I18N_ERROR_TYPES.NUMERIC_FALLBACK : I18N_ERROR_TYPES.MISSING_KEY,
|
|
166
|
+
key
|
|
167
|
+
);
|
|
168
|
+
return getFallbackText(key);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Process and return translation
|
|
172
|
+
return replaceI18NValuesWithRegex(unescapeUnicode(i18nStr), values);
|
|
173
|
+
};
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### 3. Integration in getI18NComponent()
|
|
177
|
+
|
|
178
|
+
```javascript
|
|
179
|
+
// Path: /jsapps/i18n/src/utils/jsxTranslations.js
|
|
180
|
+
|
|
181
|
+
import { reportI18NError, I18N_ERROR_TYPES } from './errorReporter';
|
|
182
|
+
|
|
183
|
+
export function getI18NComponent(i18n) {
|
|
184
|
+
// Check if i18n object exists
|
|
185
|
+
if (typeof i18n === 'undefined') {
|
|
186
|
+
reportI18NError(I18N_ERROR_TYPES.UNDEFINED_OBJECT, 'i18n_object_jsx');
|
|
187
|
+
return (key) => key;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return (key) => {
|
|
191
|
+
let i18nStr = i18n[key];
|
|
192
|
+
|
|
193
|
+
// If not found, report error and return fallback
|
|
194
|
+
if (i18nStr === undefined) {
|
|
195
|
+
const isNumeric = /^\d+$/.test(key);
|
|
196
|
+
reportI18NError(
|
|
197
|
+
isNumeric ? I18N_ERROR_TYPES.NUMERIC_FALLBACK : I18N_ERROR_TYPES.MISSING_KEY,
|
|
198
|
+
key
|
|
199
|
+
);
|
|
200
|
+
return getFallbackText(key);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Process JSX translation
|
|
204
|
+
if (typeof i18nStr === 'string') {
|
|
205
|
+
i18nStr = unescapeUnicode(i18nStr);
|
|
206
|
+
const value = prepareI18NFunc({ i18nKey: i18nStr });
|
|
207
|
+
// Cache processed value
|
|
208
|
+
window.loadI18nChunk && window.loadI18nChunk({ [key]: value });
|
|
209
|
+
i18nStr = value;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return i18nStr;
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### 4. Exports from index.js
|
|
218
|
+
|
|
219
|
+
```javascript
|
|
220
|
+
// Path: /jsapps/i18n/src/index.js
|
|
221
|
+
|
|
222
|
+
export {
|
|
223
|
+
reportI18NError,
|
|
224
|
+
clearReportedKeys,
|
|
225
|
+
I18N_ERROR_TYPES
|
|
226
|
+
} from './utils/errorReporter';
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## Error Types Explained
|
|
232
|
+
|
|
233
|
+
```
|
|
234
|
+
+-------------------------------------------------------------------+
|
|
235
|
+
| I18N ERROR TYPES |
|
|
236
|
+
+-------------------------------------------------------------------+
|
|
237
|
+
| |
|
|
238
|
+
| I18N_UNDEFINED_OBJECT |
|
|
239
|
+
| +---------------------------------------------------------------+ |
|
|
240
|
+
| | When: i18n object is undefined | |
|
|
241
|
+
| | Cause: i18n not initialized before component render | |
|
|
242
|
+
| | Example: getI18NValue(undefined)('key') | |
|
|
243
|
+
| | Action: Check i18n initialization order | |
|
|
244
|
+
| +---------------------------------------------------------------+ |
|
|
245
|
+
| |
|
|
246
|
+
| I18N_MISSING_KEY |
|
|
247
|
+
| +---------------------------------------------------------------+ |
|
|
248
|
+
| | When: Translation key doesn't exist in i18n object | |
|
|
249
|
+
| | Cause: Key typo, missing translation, wrong bundle | |
|
|
250
|
+
| | Example: i18n['tickets.wrong.key'] === undefined | |
|
|
251
|
+
| | Action: Add translation or fix key name | |
|
|
252
|
+
| +---------------------------------------------------------------+ |
|
|
253
|
+
| |
|
|
254
|
+
| I18N_NUMERIC_FALLBACK |
|
|
255
|
+
| +---------------------------------------------------------------+ |
|
|
256
|
+
| | When: Key is purely numeric (e.g., "12345") | |
|
|
257
|
+
| | Cause: Likely using ID instead of i18n key | |
|
|
258
|
+
| | Example: getI18NValue(i18n)('12345') | |
|
|
259
|
+
| | Action: Check if ID was passed instead of key | |
|
|
260
|
+
| +---------------------------------------------------------------+ |
|
|
261
|
+
| |
|
|
262
|
+
+-------------------------------------------------------------------+
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## Deduplication Strategy
|
|
268
|
+
|
|
269
|
+
```
|
|
270
|
+
+-------------------------------------------------------------------+
|
|
271
|
+
| DEDUPLICATION - WHY IT MATTERS |
|
|
272
|
+
+-------------------------------------------------------------------+
|
|
273
|
+
| |
|
|
274
|
+
| Without Deduplication: |
|
|
275
|
+
| +---------------------------------------------------------------+ |
|
|
276
|
+
| | Component renders 100 times | |
|
|
277
|
+
| | Each render: getI18NValue(i18n)('missing.key') | |
|
|
278
|
+
| | Result: 100 identical errors sent to Murphy! | |
|
|
279
|
+
| | Murphy quota: Exceeded quickly | |
|
|
280
|
+
| +---------------------------------------------------------------+ |
|
|
281
|
+
| |
|
|
282
|
+
| With Deduplication: |
|
|
283
|
+
| +---------------------------------------------------------------+ |
|
|
284
|
+
| | const reportedKeys = new Set(); | |
|
|
285
|
+
| | | |
|
|
286
|
+
| | First render: | |
|
|
287
|
+
| | reportI18NError('MISSING_KEY', 'missing.key') | |
|
|
288
|
+
| | -> Adds to Set, reports to Murphy | |
|
|
289
|
+
| | | |
|
|
290
|
+
| | Subsequent renders: | |
|
|
291
|
+
| | reportI18NError('MISSING_KEY', 'missing.key') | |
|
|
292
|
+
| | -> Already in Set, skip | |
|
|
293
|
+
| | | |
|
|
294
|
+
| | Result: Only 1 error reported per unique key per session! | |
|
|
295
|
+
| +---------------------------------------------------------------+ |
|
|
296
|
+
| |
|
|
297
|
+
+-------------------------------------------------------------------+
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
---
|
|
301
|
+
|
|
302
|
+
## Murphy Dashboard View
|
|
303
|
+
|
|
304
|
+
When errors are reported, they appear in Murphy dashboard:
|
|
305
|
+
|
|
306
|
+
```
|
|
307
|
+
+-------------------------------------------------------------------+
|
|
308
|
+
| MURPHY DASHBOARD - I18N ERRORS |
|
|
309
|
+
+-------------------------------------------------------------------+
|
|
310
|
+
| |
|
|
311
|
+
| Filter by: category = 'i18n' |
|
|
312
|
+
| |
|
|
313
|
+
| +---------------------------------------------------------------+ |
|
|
314
|
+
| | Error: I18N_MISSING_KEY: desk.tickets.save.button | |
|
|
315
|
+
| | Tags: | |
|
|
316
|
+
| | errorType: I18N_MISSING_KEY | |
|
|
317
|
+
| | i18nKey: desk.tickets.save.button | |
|
|
318
|
+
| | category: i18n | |
|
|
319
|
+
| | Count: 1,234 (unique users) | |
|
|
320
|
+
| | First seen: 2024-01-15 | |
|
|
321
|
+
| +---------------------------------------------------------------+ |
|
|
322
|
+
| |
|
|
323
|
+
| +---------------------------------------------------------------+ |
|
|
324
|
+
| | Error: I18N_NUMERIC_FALLBACK: 7890123 | |
|
|
325
|
+
| | Tags: | |
|
|
326
|
+
| | errorType: I18N_NUMERIC_FALLBACK | |
|
|
327
|
+
| | i18nKey: 7890123 | |
|
|
328
|
+
| | category: i18n | |
|
|
329
|
+
| | Count: 45 (unique users) | |
|
|
330
|
+
| | First seen: 2024-01-18 | |
|
|
331
|
+
| +---------------------------------------------------------------+ |
|
|
332
|
+
| |
|
|
333
|
+
+-------------------------------------------------------------------+
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
---
|
|
337
|
+
|
|
338
|
+
## Files Modified
|
|
339
|
+
|
|
340
|
+
| File | Change |
|
|
341
|
+
|------|--------|
|
|
342
|
+
| `src/utils/errorReporter.js` | NEW - Error reporting module |
|
|
343
|
+
| `src/utils/index.js` | Added reportI18NError calls in getI18NValue |
|
|
344
|
+
| `src/utils/jsxTranslations.js` | Added reportI18NError calls in getI18NComponent |
|
|
345
|
+
| `src/index.js` | Added exports for error reporting functions |
|
|
346
|
+
|
|
347
|
+
---
|
|
348
|
+
|
|
349
|
+
## Testing
|
|
350
|
+
|
|
351
|
+
```javascript
|
|
352
|
+
import {
|
|
353
|
+
reportI18NError,
|
|
354
|
+
clearReportedKeys,
|
|
355
|
+
I18N_ERROR_TYPES
|
|
356
|
+
} from '@zohodesk/i18n';
|
|
357
|
+
|
|
358
|
+
// Test: Manual error reporting
|
|
359
|
+
reportI18NError(I18N_ERROR_TYPES.MISSING_KEY, 'test.key');
|
|
360
|
+
|
|
361
|
+
// Test: Clear for fresh test
|
|
362
|
+
clearReportedKeys();
|
|
363
|
+
|
|
364
|
+
// Test: Deduplication
|
|
365
|
+
reportI18NError(I18N_ERROR_TYPES.MISSING_KEY, 'same.key');
|
|
366
|
+
reportI18NError(I18N_ERROR_TYPES.MISSING_KEY, 'same.key'); // Skipped
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
371
|
+
## Summary
|
|
372
|
+
|
|
373
|
+
```
|
|
374
|
+
+-------------------------------------------------------------------+
|
|
375
|
+
| I18N MURPHY INTEGRATION SUMMARY |
|
|
376
|
+
+-------------------------------------------------------------------+
|
|
377
|
+
| |
|
|
378
|
+
| What's Tracked: |
|
|
379
|
+
| +-- Missing translation keys |
|
|
380
|
+
| +-- Undefined i18n object |
|
|
381
|
+
| +-- Numeric fallback (potential ID misuse) |
|
|
382
|
+
| |
|
|
383
|
+
| Key Features: |
|
|
384
|
+
| +-- Automatic reporting on failure |
|
|
385
|
+
| +-- Deduplication per session |
|
|
386
|
+
| +-- Safe if Murphy not available |
|
|
387
|
+
| +-- Zero overhead on success |
|
|
388
|
+
| |
|
|
389
|
+
| Integration Points: |
|
|
390
|
+
| +-- getI18NValue() for text translations |
|
|
391
|
+
| +-- getI18NComponent() for JSX translations |
|
|
392
|
+
| |
|
|
393
|
+
+-------------------------------------------------------------------+
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
---
|
|
397
|
+
|
|
398
|
+
## Related Documents
|
|
399
|
+
|
|
400
|
+
- [01-MURPHY_OVERVIEW.md](./01-MURPHY_OVERVIEW.md) - What is Murphy
|
|
401
|
+
- [05-MURPHY_DESK_CLIENT_USAGE.md](./05-MURPHY_DESK_CLIENT_USAGE.md) - Desk client usage
|
|
402
|
+
- [07-MURPHY_WHY_I18N_APPROACH.md](./07-MURPHY_WHY_I18N_APPROACH.md) - Why i18n approach
|