@ttoss/react-i18n 2.0.14 → 2.0.15
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/README.md +621 -51
- package/dist/esm/index.js +20 -20
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -1,108 +1,678 @@
|
|
|
1
1
|
# @ttoss/react-i18n
|
|
2
2
|
|
|
3
|
-
**@ttoss/react-i18n** is a library that provides
|
|
3
|
+
**@ttoss/react-i18n** is a React internationalization library built on [FormatJS](https://formatjs.io/) that seamlessly integrates with the ttoss ecosystem. It provides component-level i18n capabilities for translating text elements like buttons, labels, and headings, making your React applications accessible to global audiences.
|
|
4
|
+
|
|
5
|
+
## Key Features
|
|
6
|
+
|
|
7
|
+
- **FormatJS Integration**: Built on industry-standard FormatJS with full ICU message format support
|
|
8
|
+
- **Dynamic Locale Loading**: Async loading of translation files with automatic caching
|
|
9
|
+
- **TypeScript Support**: Full TypeScript definitions for type-safe internationalization
|
|
10
|
+
- **Developer Experience**: Simple hooks and components for easy integration
|
|
11
|
+
- **ttoss Ecosystem**: Works seamlessly with other ttoss packages and tooling
|
|
12
|
+
- **Performance Optimized**: Efficient message loading and rendering with minimal overhead
|
|
13
|
+
|
|
14
|
+
## When to Use
|
|
15
|
+
|
|
16
|
+
Choose `@ttoss/react-i18n` for **component-level internationalization** when you need to:
|
|
17
|
+
|
|
18
|
+
- Translate UI text elements (buttons, labels, form fields, notifications)
|
|
19
|
+
- Support multiple languages within React components
|
|
20
|
+
- Implement user language switching functionality
|
|
21
|
+
- Handle pluralization, number formatting, and date localization
|
|
22
|
+
|
|
23
|
+
For **routing-level internationalization** (SEO, CMS content, URL localization), consider Next.js built-in i18n features alongside this library.
|
|
4
24
|
|
|
5
25
|
:::note
|
|
6
|
-
|
|
26
|
+
Declare your messages following [FormatJS message declaration guidelines](https://formatjs.io/docs/getting-started/message-declaration) for optimal extraction and compilation.
|
|
7
27
|
:::
|
|
8
28
|
|
|
9
|
-
##
|
|
10
|
-
|
|
11
|
-
### Install @ttoss/react-i18n
|
|
29
|
+
## Installation
|
|
12
30
|
|
|
13
31
|
```shell
|
|
14
32
|
pnpm add @ttoss/react-i18n
|
|
15
33
|
pnpm add -D @ttoss/i18n-cli
|
|
16
34
|
```
|
|
17
35
|
|
|
18
|
-
|
|
36
|
+
The `@ttoss/i18n-cli` package handles message extraction and compilation. See the [i18n-cli documentation](https://ttoss.dev/docs/modules/packages/i18n-cli/) for complete workflow setup.
|
|
19
37
|
|
|
20
|
-
##
|
|
38
|
+
## Quick Start
|
|
21
39
|
|
|
22
|
-
###
|
|
40
|
+
### 1. Provider Setup
|
|
23
41
|
|
|
24
|
-
|
|
42
|
+
Wrap your application with `I18nProvider` and configure locale data loading:
|
|
25
43
|
|
|
26
|
-
```tsx title="src/
|
|
44
|
+
```tsx title="src/main.tsx"
|
|
45
|
+
import React from 'react';
|
|
46
|
+
import ReactDOM from 'react-dom/client';
|
|
27
47
|
import { I18nProvider, LoadLocaleData } from '@ttoss/react-i18n';
|
|
48
|
+
import App from './App';
|
|
28
49
|
|
|
29
50
|
const loadLocaleData: LoadLocaleData = async (locale) => {
|
|
30
51
|
switch (locale) {
|
|
31
52
|
case 'pt-BR':
|
|
32
53
|
return import('../i18n/compiled/pt-BR.json');
|
|
54
|
+
case 'es':
|
|
55
|
+
return import('../i18n/compiled/es.json');
|
|
33
56
|
default:
|
|
34
57
|
return import('../i18n/compiled/en.json');
|
|
35
58
|
}
|
|
36
59
|
};
|
|
37
60
|
|
|
38
|
-
ReactDOM.render(
|
|
39
|
-
<I18nProvider
|
|
40
|
-
locale={window.navigator.language}
|
|
41
|
-
loadLocaleData={loadLocaleData}
|
|
42
|
-
>
|
|
61
|
+
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
62
|
+
<I18nProvider locale={navigator.language} loadLocaleData={loadLocaleData}>
|
|
43
63
|
<App />
|
|
44
|
-
</I18nProvider
|
|
45
|
-
document.getElementById('root')
|
|
64
|
+
</I18nProvider>
|
|
46
65
|
);
|
|
47
66
|
```
|
|
48
67
|
|
|
49
|
-
###
|
|
68
|
+
### 2. Component Usage
|
|
50
69
|
|
|
51
|
-
|
|
70
|
+
Use the `useI18n` hook to access internationalization features:
|
|
52
71
|
|
|
53
72
|
```tsx title="src/App.tsx"
|
|
54
|
-
import {
|
|
73
|
+
import React, { useState } from 'react';
|
|
74
|
+
import { useI18n } from '@ttoss/react-i18n';
|
|
55
75
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
76
|
+
export default function App() {
|
|
77
|
+
const { intl, setLocale, locale } = useI18n();
|
|
78
|
+
const [name, setName] = useState('User');
|
|
79
|
+
|
|
80
|
+
const toggleLanguage = () => {
|
|
81
|
+
setLocale(locale === 'en' ? 'pt-BR' : 'en');
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<div>
|
|
86
|
+
<h1>
|
|
87
|
+
{intl.formatMessage(
|
|
88
|
+
{
|
|
89
|
+
description: 'Welcome message',
|
|
90
|
+
defaultMessage: 'Welcome, {name}!',
|
|
91
|
+
},
|
|
92
|
+
{ name }
|
|
93
|
+
)}
|
|
94
|
+
</h1>
|
|
95
|
+
<input
|
|
96
|
+
value={name}
|
|
97
|
+
onChange={(e) => setName(e.target.value)}
|
|
98
|
+
placeholder="Enter your name"
|
|
99
|
+
/>
|
|
100
|
+
<button onClick={toggleLanguage}>
|
|
101
|
+
{intl.formatMessage({
|
|
102
|
+
description: 'Change language button',
|
|
103
|
+
defaultMessage: 'Change Language',
|
|
104
|
+
})}
|
|
105
|
+
</button>
|
|
106
|
+
<p>Current locale: {locale}</p>
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Framework Integration
|
|
113
|
+
|
|
114
|
+
### Vite Configuration
|
|
115
|
+
|
|
116
|
+
Configure Vite to properly handle message extraction. Choose between Babel or SWC based on your setup:
|
|
117
|
+
|
|
118
|
+
#### Option 1: Using Babel (with @ttoss/config)
|
|
119
|
+
|
|
120
|
+
```ts title="vite.config.ts"
|
|
121
|
+
import { defineConfig } from 'vite';
|
|
122
|
+
import react from '@vitejs/plugin-react';
|
|
123
|
+
import { babelConfig } from '@ttoss/config';
|
|
124
|
+
|
|
125
|
+
export default defineConfig({
|
|
126
|
+
plugins: [
|
|
127
|
+
react({
|
|
128
|
+
babel: {
|
|
129
|
+
plugins: babelConfig().plugins,
|
|
130
|
+
},
|
|
131
|
+
}),
|
|
132
|
+
],
|
|
133
|
+
});
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
#### Option 2: Using Babel (manual configuration)
|
|
137
|
+
|
|
138
|
+
First, install the Babel plugin:
|
|
139
|
+
|
|
140
|
+
```shell
|
|
141
|
+
pnpm add -D babel-plugin-formatjs
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Then configure Vite:
|
|
145
|
+
|
|
146
|
+
```ts title="vite.config.ts"
|
|
147
|
+
import { defineConfig } from 'vite';
|
|
148
|
+
import react from '@vitejs/plugin-react';
|
|
149
|
+
|
|
150
|
+
export default defineConfig({
|
|
151
|
+
plugins: [
|
|
152
|
+
react({
|
|
153
|
+
babel: {
|
|
154
|
+
plugins: [
|
|
155
|
+
[
|
|
156
|
+
'formatjs',
|
|
157
|
+
{
|
|
158
|
+
idInterpolationPattern: '[sha512:contenthash:base64:6]',
|
|
159
|
+
ast: true,
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
],
|
|
163
|
+
},
|
|
164
|
+
}),
|
|
165
|
+
],
|
|
166
|
+
});
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
#### Option 3: Using SWC
|
|
170
|
+
|
|
171
|
+
First, install the SWC plugin:
|
|
172
|
+
|
|
173
|
+
```shell
|
|
174
|
+
pnpm add -D @swc/plugin-formatjs
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Then configure Vite with SWC:
|
|
178
|
+
|
|
179
|
+
```ts title="vite.config.ts"
|
|
180
|
+
import react from '@vitejs/plugin-react-swc';
|
|
181
|
+
import { defineConfig } from 'vite';
|
|
182
|
+
|
|
183
|
+
export default defineConfig({
|
|
184
|
+
plugins: [
|
|
185
|
+
react({
|
|
186
|
+
plugins: [
|
|
187
|
+
[
|
|
188
|
+
'@swc/plugin-formatjs',
|
|
189
|
+
{
|
|
190
|
+
idInterpolationPattern: '[sha512:contenthash:base64:6]',
|
|
191
|
+
ast: true,
|
|
192
|
+
},
|
|
193
|
+
],
|
|
194
|
+
],
|
|
195
|
+
}),
|
|
196
|
+
],
|
|
61
197
|
});
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
#### Configuration Options
|
|
201
|
+
|
|
202
|
+
- **`idInterpolationPattern`**: Generates unique, deterministic IDs for messages using content hash
|
|
203
|
+
- **`ast`**: Enables AST-based extraction for better performance and accuracy
|
|
204
|
+
|
|
205
|
+
For more configuration options, see:
|
|
206
|
+
|
|
207
|
+
- [Babel plugin documentation](https://formatjs.github.io/docs/tooling/babel-plugin/)
|
|
208
|
+
- [SWC plugin documentation](https://www.npmjs.com/package/@swc/plugin-formatjs)
|
|
209
|
+
|
|
210
|
+
### Next.js Setup
|
|
211
|
+
|
|
212
|
+
For Next.js applications, configure the provider in `_app.tsx`:
|
|
213
|
+
|
|
214
|
+
```tsx title="pages/_app.tsx"
|
|
215
|
+
import { I18nProvider, LoadLocaleData } from '@ttoss/react-i18n';
|
|
216
|
+
import { useRouter } from 'next/router';
|
|
217
|
+
import type { AppProps } from 'next/app';
|
|
62
218
|
|
|
63
|
-
const
|
|
64
|
-
|
|
219
|
+
const loadLocaleData: LoadLocaleData = async (locale) => {
|
|
220
|
+
switch (locale) {
|
|
221
|
+
case 'pt-BR':
|
|
222
|
+
return import('../../i18n/compiled/pt-BR.json');
|
|
223
|
+
default:
|
|
224
|
+
return import('../../i18n/compiled/en.json');
|
|
225
|
+
}
|
|
226
|
+
};
|
|
65
227
|
|
|
66
|
-
|
|
228
|
+
export default function MyApp({ Component, pageProps }: AppProps) {
|
|
229
|
+
const { locale } = useRouter();
|
|
67
230
|
|
|
68
231
|
return (
|
|
69
|
-
<
|
|
70
|
-
<
|
|
71
|
-
|
|
232
|
+
<I18nProvider locale={locale} loadLocaleData={loadLocaleData}>
|
|
233
|
+
<Component {...pageProps} />
|
|
234
|
+
</I18nProvider>
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## Advanced Usage
|
|
240
|
+
|
|
241
|
+
### FormattedMessage Component
|
|
242
|
+
|
|
243
|
+
Use `FormattedMessage` for declarative message rendering:
|
|
72
244
|
|
|
73
|
-
|
|
74
|
-
|
|
245
|
+
```tsx
|
|
246
|
+
import { FormattedMessage } from '@ttoss/react-i18n';
|
|
75
247
|
|
|
76
|
-
|
|
248
|
+
function UserStats({ userCount, lastSeen }) {
|
|
249
|
+
return (
|
|
250
|
+
<div>
|
|
251
|
+
<FormattedMessage
|
|
252
|
+
description="Number of users"
|
|
253
|
+
defaultMessage="{count, plural, =0 {No users} =1 {One user} other {# users}}"
|
|
254
|
+
values={{ count: userCount }}
|
|
255
|
+
/>
|
|
256
|
+
<FormattedMessage
|
|
257
|
+
description="Last seen timestamp"
|
|
258
|
+
defaultMessage="Last seen: {timestamp, date, short}"
|
|
259
|
+
values={{ timestamp: lastSeen }}
|
|
260
|
+
/>
|
|
261
|
+
</div>
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
:::warning Library Development
|
|
267
|
+
When creating **reusable libraries** that use this package, prefer `intl.formatMessage` over `FormattedMessage` component. During the build process, `FormattedMessage` gets transformed into JSX syntax like `return /* @__PURE__ */jsx(FormattedMessage, { defaultMessage: "..." })`, which prevents the message extraction tool from properly detecting and generating IDs for the messages.
|
|
77
268
|
|
|
78
|
-
|
|
269
|
+
```tsx
|
|
270
|
+
// ✅ Good for libraries - extracts correctly
|
|
271
|
+
function MyLibraryComponent() {
|
|
272
|
+
const { intl } = useI18n();
|
|
273
|
+
return (
|
|
274
|
+
<div>
|
|
275
|
+
{intl.formatMessage({
|
|
276
|
+
description: 'Library message',
|
|
277
|
+
defaultMessage: 'This message will be extracted correctly',
|
|
278
|
+
})}
|
|
79
279
|
</div>
|
|
80
280
|
);
|
|
81
|
-
}
|
|
281
|
+
}
|
|
82
282
|
|
|
83
|
-
|
|
283
|
+
// ❌ Avoid in libraries - extraction may fail
|
|
284
|
+
function MyLibraryComponent() {
|
|
285
|
+
return (
|
|
286
|
+
<FormattedMessage
|
|
287
|
+
description="Library message"
|
|
288
|
+
defaultMessage="This may not extract properly in built libraries"
|
|
289
|
+
/>
|
|
290
|
+
);
|
|
291
|
+
}
|
|
84
292
|
```
|
|
85
293
|
|
|
86
|
-
|
|
294
|
+
:::
|
|
87
295
|
|
|
88
|
-
|
|
296
|
+
### Error Handling
|
|
89
297
|
|
|
90
|
-
|
|
91
|
-
import { babelConfig } from '@ttoss/config';
|
|
298
|
+
Handle translation errors gracefully:
|
|
92
299
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
// ...
|
|
300
|
+
```tsx
|
|
301
|
+
import { I18nProvider } from '@ttoss/react-i18n';
|
|
96
302
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
303
|
+
function App() {
|
|
304
|
+
const handleTranslationError = (error: Error) => {
|
|
305
|
+
console.warn('Translation error:', error.message);
|
|
306
|
+
// Log to error tracking service
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
return (
|
|
310
|
+
<I18nProvider
|
|
311
|
+
locale="en"
|
|
312
|
+
loadLocaleData={loadLocaleData}
|
|
313
|
+
onError={handleTranslationError}
|
|
314
|
+
>
|
|
315
|
+
<MyApp />
|
|
316
|
+
</I18nProvider>
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### Rich Text Formatting
|
|
322
|
+
|
|
323
|
+
Format messages with embedded components:
|
|
324
|
+
|
|
325
|
+
```tsx
|
|
326
|
+
import { useI18n } from '@ttoss/react-i18n';
|
|
327
|
+
|
|
328
|
+
function SignupForm() {
|
|
329
|
+
const { intl } = useI18n();
|
|
330
|
+
|
|
331
|
+
return (
|
|
332
|
+
<p>
|
|
333
|
+
{intl.formatMessage(
|
|
334
|
+
{
|
|
335
|
+
description: 'Terms and conditions',
|
|
336
|
+
defaultMessage:
|
|
337
|
+
'By signing up, you agree to our {termsLink} and {privacyLink}.',
|
|
101
338
|
},
|
|
102
|
-
|
|
103
|
-
|
|
339
|
+
{
|
|
340
|
+
termsLink: <a href="/terms">Terms of Service</a>,
|
|
341
|
+
privacyLink: <a href="/privacy">Privacy Policy</a>,
|
|
342
|
+
}
|
|
343
|
+
)}
|
|
344
|
+
</p>
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
### Dynamic Locale Loading
|
|
350
|
+
|
|
351
|
+
Load locales based on user preferences:
|
|
352
|
+
|
|
353
|
+
```tsx
|
|
354
|
+
import { useI18n } from '@ttoss/react-i18n';
|
|
355
|
+
|
|
356
|
+
function LanguageSelector() {
|
|
357
|
+
const { setLocale, locale } = useI18n();
|
|
358
|
+
|
|
359
|
+
const languages = [
|
|
360
|
+
{ code: 'en', name: 'English' },
|
|
361
|
+
{ code: 'pt-BR', name: 'Português' },
|
|
362
|
+
{ code: 'es', name: 'Español' },
|
|
363
|
+
];
|
|
364
|
+
|
|
365
|
+
const handleLanguageChange = async (newLocale: string) => {
|
|
366
|
+
// Persist user choice
|
|
367
|
+
localStorage.setItem('preferredLocale', newLocale);
|
|
368
|
+
setLocale(newLocale);
|
|
104
369
|
};
|
|
105
370
|
|
|
106
|
-
|
|
371
|
+
return (
|
|
372
|
+
<select
|
|
373
|
+
value={locale}
|
|
374
|
+
onChange={(e) => handleLanguageChange(e.target.value)}
|
|
375
|
+
>
|
|
376
|
+
{languages.map((lang) => (
|
|
377
|
+
<option key={lang.code} value={lang.code}>
|
|
378
|
+
{lang.name}
|
|
379
|
+
</option>
|
|
380
|
+
))}
|
|
381
|
+
</select>
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
## API Reference
|
|
387
|
+
|
|
388
|
+
### I18nProvider
|
|
389
|
+
|
|
390
|
+
Main provider component that configures internationalization context.
|
|
391
|
+
|
|
392
|
+
**Props:**
|
|
393
|
+
|
|
394
|
+
- `locale?: string` - Initial locale (defaults to browser language)
|
|
395
|
+
- `loadLocaleData?: LoadLocaleData` - Function to load translation data
|
|
396
|
+
- `onError?: (error: Error) => void` - Error handler for translation issues
|
|
397
|
+
- `children: ReactNode` - Child components
|
|
398
|
+
|
|
399
|
+
```tsx
|
|
400
|
+
type LoadLocaleData = (locale: string) => Promise<Messages> | Messages;
|
|
401
|
+
type Messages = Record<string, string> | Record<string, MessageFormatElement[]>;
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
### useI18n Hook
|
|
405
|
+
|
|
406
|
+
Returns internationalization utilities and state.
|
|
407
|
+
|
|
408
|
+
**Returns:**
|
|
409
|
+
|
|
410
|
+
```tsx
|
|
411
|
+
{
|
|
412
|
+
intl: IntlShape; // FormatJS intl object
|
|
413
|
+
locale: string; // Current locale
|
|
414
|
+
defaultLocale: string; // Default locale ('en')
|
|
415
|
+
setLocale: (locale: string) => void; // Change locale function
|
|
416
|
+
messages?: Messages; // Current translation messages
|
|
417
|
+
}
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
**Example:**
|
|
421
|
+
|
|
422
|
+
```tsx
|
|
423
|
+
const { intl, locale, setLocale } = useI18n();
|
|
424
|
+
|
|
425
|
+
// Format messages
|
|
426
|
+
const greeting = intl.formatMessage({
|
|
427
|
+
description: 'Simple greeting',
|
|
428
|
+
defaultMessage: 'Hello!',
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
// Format with variables
|
|
432
|
+
const welcome = intl.formatMessage(
|
|
433
|
+
{
|
|
434
|
+
description: 'Welcome message with name',
|
|
435
|
+
defaultMessage: 'Welcome, {name}!',
|
|
436
|
+
},
|
|
437
|
+
{ name: 'John' }
|
|
438
|
+
);
|
|
439
|
+
|
|
440
|
+
// Format numbers and dates
|
|
441
|
+
const price = intl.formatNumber(29.99, { style: 'currency', currency: 'USD' });
|
|
442
|
+
const date = intl.formatDate(new Date(), { dateStyle: 'medium' });
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
### defineMessages / defineMessage
|
|
446
|
+
|
|
447
|
+
Type-safe message definition utilities from FormatJS.
|
|
448
|
+
|
|
449
|
+
```tsx
|
|
450
|
+
import { defineMessages, defineMessage } from '@ttoss/react-i18n';
|
|
451
|
+
|
|
452
|
+
// Single message (recommended pattern)
|
|
453
|
+
const errorMessage = defineMessage({
|
|
454
|
+
description: 'Generic error message',
|
|
455
|
+
defaultMessage: 'Something went wrong. Please try again.',
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
// Multiple messages (alternative approach)
|
|
459
|
+
const messages = defineMessages({
|
|
460
|
+
title: {
|
|
461
|
+
description: 'Page title',
|
|
462
|
+
defaultMessage: 'My Application',
|
|
463
|
+
},
|
|
464
|
+
subtitle: {
|
|
465
|
+
description: 'Page subtitle',
|
|
466
|
+
defaultMessage: 'Welcome to our platform',
|
|
467
|
+
},
|
|
107
468
|
});
|
|
469
|
+
|
|
470
|
+
// Usage in component
|
|
471
|
+
function MyComponent() {
|
|
472
|
+
const { intl } = useI18n();
|
|
473
|
+
return <div>{intl.formatMessage(errorMessage)}</div>;
|
|
474
|
+
}
|
|
108
475
|
```
|
|
476
|
+
|
|
477
|
+
:::note
|
|
478
|
+
The current ttoss pattern is to define messages inline within components rather than using `defineMessages`. This approach provides better co-location and reduces the need for separate message definitions.
|
|
479
|
+
:::
|
|
480
|
+
|
|
481
|
+
### FormattedMessage
|
|
482
|
+
|
|
483
|
+
Component for declarative message rendering.
|
|
484
|
+
|
|
485
|
+
**Props:**
|
|
486
|
+
|
|
487
|
+
- `id?: string` - Message ID (auto-generated if using defineMessage)
|
|
488
|
+
- `defaultMessage: string` - Default message text
|
|
489
|
+
- `description?: string` - Message description for translators
|
|
490
|
+
- `values?: Record<string, any>` - Interpolation values
|
|
491
|
+
|
|
492
|
+
```tsx
|
|
493
|
+
<FormattedMessage
|
|
494
|
+
description="Item count message"
|
|
495
|
+
defaultMessage="You have {count, plural, =0 {no items} =1 {one item} other {# items}}"
|
|
496
|
+
values={{ count: itemCount }}
|
|
497
|
+
/>
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
## Best Practices
|
|
501
|
+
|
|
502
|
+
### Message Organization
|
|
503
|
+
|
|
504
|
+
Structure your messages for maintainability:
|
|
505
|
+
|
|
506
|
+
```tsx
|
|
507
|
+
// Inline messages with components (recommended ttoss pattern)
|
|
508
|
+
function AuthComponent() {
|
|
509
|
+
const { intl } = useI18n();
|
|
510
|
+
|
|
511
|
+
return (
|
|
512
|
+
<div>
|
|
513
|
+
<h1>
|
|
514
|
+
{intl.formatMessage({
|
|
515
|
+
description: 'Login page title',
|
|
516
|
+
defaultMessage: 'Sign In',
|
|
517
|
+
})}
|
|
518
|
+
</h1>
|
|
519
|
+
<button>
|
|
520
|
+
{intl.formatMessage({
|
|
521
|
+
description: 'Login submit button',
|
|
522
|
+
defaultMessage: 'Log In',
|
|
523
|
+
})}
|
|
524
|
+
</button>
|
|
525
|
+
<a href="/forgot-password">
|
|
526
|
+
{intl.formatMessage({
|
|
527
|
+
description: 'Forgot password link',
|
|
528
|
+
defaultMessage: 'Forgot your password?',
|
|
529
|
+
})}
|
|
530
|
+
</a>
|
|
531
|
+
</div>
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// For reusable messages across components, you can still use defineMessage
|
|
536
|
+
import { defineMessage } from '@ttoss/react-i18n';
|
|
537
|
+
|
|
538
|
+
const successMessage = defineMessage({
|
|
539
|
+
description: 'Success message when user profile is updated',
|
|
540
|
+
defaultMessage: 'Your profile has been successfully updated.',
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
function ProfileForm() {
|
|
544
|
+
const { intl } = useI18n();
|
|
545
|
+
|
|
546
|
+
const handleSuccess = () => {
|
|
547
|
+
toast.success(intl.formatMessage(successMessage));
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
// ... rest of component
|
|
551
|
+
}
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
### Performance Optimization
|
|
555
|
+
|
|
556
|
+
Optimize loading and rendering:
|
|
557
|
+
|
|
558
|
+
```tsx
|
|
559
|
+
// Lazy load locale data to reduce initial bundle size
|
|
560
|
+
const loadLocaleData: LoadLocaleData = async (locale) => {
|
|
561
|
+
const localeModule = await import(`../i18n/compiled/${locale}.json`);
|
|
562
|
+
return localeModule.default;
|
|
563
|
+
};
|
|
564
|
+
|
|
565
|
+
// Use React.memo for components with many translated strings
|
|
566
|
+
const TranslatedComponent = React.memo(function TranslatedComponent({ data }) {
|
|
567
|
+
const { intl } = useI18n();
|
|
568
|
+
|
|
569
|
+
return (
|
|
570
|
+
<div>
|
|
571
|
+
{data.map((item) => (
|
|
572
|
+
<div key={item.id}>
|
|
573
|
+
{intl.formatMessage(
|
|
574
|
+
{
|
|
575
|
+
description: 'Item title in list',
|
|
576
|
+
defaultMessage: 'Title: {title}',
|
|
577
|
+
},
|
|
578
|
+
{ title: item.title }
|
|
579
|
+
)}
|
|
580
|
+
</div>
|
|
581
|
+
))}
|
|
582
|
+
</div>
|
|
583
|
+
);
|
|
584
|
+
});
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
### Locale Detection
|
|
588
|
+
|
|
589
|
+
Implement intelligent locale detection:
|
|
590
|
+
|
|
591
|
+
```tsx
|
|
592
|
+
function getInitialLocale(): string {
|
|
593
|
+
// 1. Check user preference from localStorage
|
|
594
|
+
const saved = localStorage.getItem('preferredLocale');
|
|
595
|
+
if (saved) return saved;
|
|
596
|
+
|
|
597
|
+
// 2. Check URL parameters
|
|
598
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
599
|
+
const urlLocale = urlParams.get('locale');
|
|
600
|
+
if (urlLocale) return urlLocale;
|
|
601
|
+
|
|
602
|
+
// 3. Use browser language with fallback
|
|
603
|
+
const browserLocale = navigator.language;
|
|
604
|
+
const supportedLocales = ['en', 'pt-BR', 'es'];
|
|
605
|
+
|
|
606
|
+
return supportedLocales.includes(browserLocale) ? browserLocale : 'en';
|
|
607
|
+
}
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
## Troubleshooting
|
|
611
|
+
|
|
612
|
+
### Common Issues
|
|
613
|
+
|
|
614
|
+
**Missing translation warnings:**
|
|
615
|
+
|
|
616
|
+
```
|
|
617
|
+
MessageError: MISSING_TRANSLATION
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
- Ensure all locales have corresponding translation files
|
|
621
|
+
- Check that message IDs match between code and translation files
|
|
622
|
+
- Use `onError` prop to handle gracefully
|
|
623
|
+
|
|
624
|
+
**Babel plugin not working:**
|
|
625
|
+
|
|
626
|
+
```
|
|
627
|
+
Messages not being extracted during build
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
- Verify `@ttoss/config` babel plugins are configured correctly
|
|
631
|
+
- Check that message definitions use inline `intl.formatMessage` calls
|
|
632
|
+
- Ensure babel configuration is applied to your build process
|
|
633
|
+
|
|
634
|
+
**Locale not switching:**
|
|
635
|
+
|
|
636
|
+
```
|
|
637
|
+
setLocale called but UI doesn't update
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
- Verify `loadLocaleData` function returns correct translation data
|
|
641
|
+
- Check browser console for loading errors
|
|
642
|
+
- Ensure `I18nProvider` is at the correct level in component tree
|
|
643
|
+
|
|
644
|
+
### Development Tips
|
|
645
|
+
|
|
646
|
+
Enable verbose logging during development:
|
|
647
|
+
|
|
648
|
+
```tsx
|
|
649
|
+
const loadLocaleData: LoadLocaleData = async (locale) => {
|
|
650
|
+
console.log(`Loading locale: ${locale}`);
|
|
651
|
+
try {
|
|
652
|
+
const data = await import(`../i18n/compiled/${locale}.json`);
|
|
653
|
+
console.log(
|
|
654
|
+
`Loaded ${Object.keys(data.default).length} messages for ${locale}`
|
|
655
|
+
);
|
|
656
|
+
return data.default;
|
|
657
|
+
} catch (error) {
|
|
658
|
+
console.error(`Failed to load locale ${locale}:`, error);
|
|
659
|
+
throw error;
|
|
660
|
+
}
|
|
661
|
+
};
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
## Related Resources
|
|
665
|
+
|
|
666
|
+
### ttoss Ecosystem
|
|
667
|
+
|
|
668
|
+
- **[@ttoss/i18n-cli](https://ttoss.dev/docs/modules/packages/i18n-cli/)** - Extract and compile translations using FormatJS workflow
|
|
669
|
+
- **[@ttoss/forms](https://ttoss.dev/docs/modules/packages/forms/)** - Form components with built-in i18n support
|
|
670
|
+
- **[@ttoss/ui](https://ttoss.dev/docs/modules/packages/ui/)** - UI components that work seamlessly with react-i18n
|
|
671
|
+
|
|
672
|
+
### External Resources
|
|
673
|
+
|
|
674
|
+
- **[FormatJS Documentation](https://formatjs.io/)** - Complete guide to ICU message format and FormatJS features
|
|
675
|
+
- **[ICU Message Format](https://unicode-org.github.io/icu/userguide/format_parse/messages/)** - Specification for message formatting
|
|
676
|
+
- **[Building a Multilingual Blog with Next.js and @ttoss/react-i18n](/blog/building-a-multilingual-blog-site-with-next.js-and-@ttoss-react-i18n)** - Comprehensive tutorial
|
|
677
|
+
|
|
678
|
+
This library enables efficient internationalization following FormatJS standards while integrating seamlessly with the ttoss development ecosystem. For complete workflow setup including message extraction and compilation, see the [@ttoss/i18n-cli documentation](https://ttoss.dev/docs/modules/packages/i18n-cli/).
|
package/dist/esm/index.js
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
/** Powered by @ttoss/config. https://ttoss.dev/docs/modules/packages/config/ */
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __name = (target, value) => __defProp(target, "name", {
|
|
4
|
+
value,
|
|
5
|
+
configurable: true
|
|
6
|
+
});
|
|
2
7
|
|
|
3
8
|
// src/index.ts
|
|
4
9
|
import { defineMessages, defineMessage, FormattedMessage } from "react-intl";
|
|
@@ -6,16 +11,15 @@ import { defineMessages, defineMessage, FormattedMessage } from "react-intl";
|
|
|
6
11
|
// src/i18Provider.tsx
|
|
7
12
|
import * as React from "react";
|
|
8
13
|
import { IntlProvider } from "react-intl";
|
|
9
|
-
import { Fragment, jsx } from "react/jsx-runtime";
|
|
10
14
|
var DEFAULT_LOCALE = "en";
|
|
11
|
-
var I18nConfigContext = React.createContext({
|
|
15
|
+
var I18nConfigContext = /* @__PURE__ */React.createContext({
|
|
12
16
|
defaultLocale: DEFAULT_LOCALE,
|
|
13
17
|
messages: {},
|
|
14
|
-
setLocale: () => {
|
|
18
|
+
setLocale: /* @__PURE__ */__name(() => {
|
|
15
19
|
return null;
|
|
16
|
-
}
|
|
20
|
+
}, "setLocale")
|
|
17
21
|
});
|
|
18
|
-
var I18nProvider = ({
|
|
22
|
+
var I18nProvider = /* @__PURE__ */__name(({
|
|
19
23
|
children,
|
|
20
24
|
locale: initialLocale,
|
|
21
25
|
loadLocaleData,
|
|
@@ -35,35 +39,31 @@ var I18nProvider = ({
|
|
|
35
39
|
});
|
|
36
40
|
}
|
|
37
41
|
}, [loadLocaleData, locale]);
|
|
38
|
-
return /* @__PURE__ */
|
|
42
|
+
return /* @__PURE__ */React.createElement(I18nConfigContext.Provider, {
|
|
39
43
|
value: {
|
|
40
44
|
locale,
|
|
41
45
|
defaultLocale: DEFAULT_LOCALE,
|
|
42
46
|
messages: messagesAndLocale.messages,
|
|
43
47
|
setLocale,
|
|
44
48
|
...intlConfig
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
})
|
|
54
|
-
})
|
|
55
|
-
});
|
|
56
|
-
};
|
|
49
|
+
}
|
|
50
|
+
}, /* @__PURE__ */React.createElement(IntlProvider, {
|
|
51
|
+
defaultLocale: DEFAULT_LOCALE,
|
|
52
|
+
locale: messagesAndLocale.locale,
|
|
53
|
+
messages: messagesAndLocale.messages,
|
|
54
|
+
...intlConfig
|
|
55
|
+
}, /* @__PURE__ */React.createElement(React.Fragment, null, children)));
|
|
56
|
+
}, "I18nProvider");
|
|
57
57
|
|
|
58
58
|
// src/useI18n.ts
|
|
59
59
|
import * as React2 from "react";
|
|
60
60
|
import { useIntl } from "react-intl";
|
|
61
|
-
var useI18n = () => {
|
|
61
|
+
var useI18n = /* @__PURE__ */__name(() => {
|
|
62
62
|
const intl = useIntl();
|
|
63
63
|
const config = React2.useContext(I18nConfigContext);
|
|
64
64
|
return {
|
|
65
65
|
...config,
|
|
66
66
|
intl
|
|
67
67
|
};
|
|
68
|
-
};
|
|
68
|
+
}, "useI18n");
|
|
69
69
|
export { DEFAULT_LOCALE, FormattedMessage, I18nProvider, defineMessage, defineMessages, useI18n };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ttoss/react-i18n",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.15",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "ttoss",
|
|
6
6
|
"contributors": [
|
|
@@ -35,9 +35,9 @@
|
|
|
35
35
|
"jest": "^30.0.4",
|
|
36
36
|
"react": "^19.1.0",
|
|
37
37
|
"tsup": "^8.5.0",
|
|
38
|
-
"@ttoss/
|
|
39
|
-
"@ttoss/
|
|
40
|
-
"@ttoss/
|
|
38
|
+
"@ttoss/i18n-cli": "^0.7.32",
|
|
39
|
+
"@ttoss/test-utils": "^2.1.26",
|
|
40
|
+
"@ttoss/config": "^1.35.6"
|
|
41
41
|
},
|
|
42
42
|
"keywords": [
|
|
43
43
|
"React",
|