@jsarc/intl 0.0.0

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 ADDED
@@ -0,0 +1,596 @@
1
+ # @jsarc/intl
2
+
3
+ [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
4
+ ![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-007ACC)
5
+ ![React](https://img.shields.io/badge/React-19+-61DAFB)
6
+ ![Vite](https://img.shields.io/badge/Vite-6+-646CFF)
7
+
8
+ **@jsarc/intl** est un système de gestion d'internationalisation (i18n) modulaire et performant pour les applications React avec TypeScript/JavaScript. Il fournit une gestion avancée des traductions, un chargement dynamique des modules, et une intégration transparente avec l'écosystème Arc.
9
+
10
+ ## ✨ Fonctionnalités Principales
11
+
12
+ ### 🌍 Gestion Multi-Langue Avancée
13
+ - **Support de plusieurs locales** avec détection automatique
14
+ - **Chargement dynamique** des fichiers de traduction
15
+ - **Gestion des pluriels** basée sur les règles Intl
16
+ - **Interpolation de variables** dans les traductions
17
+
18
+ ### 📦 Architecture Modulaire
19
+ - **Traductions par module** avec isolation complète
20
+ - **Chargement à la demande** des traductions des modules
21
+ - **Fusion intelligente** des traductions hiérarchiques
22
+ - **Support des namespaces** pour une organisation claire
23
+
24
+ ### ⚡ Performance Optimisée
25
+ - **Chargement paresseux** des fichiers de traduction
26
+ - **Mémoire cache** des traductions chargées
27
+ - **Minimal bundle size** grâce au code splitting
28
+ - **Hot reload** pendant le développement
29
+
30
+ ### 🔧 Intégration Facile
31
+ - **Provider React** simple à configurer
32
+ - **Hooks personnalisés** pour une utilisation intuitive
33
+ - **Compatibilité totale** avec TypeScript
34
+ - **Intégration avec @jsarc/cooks** pour la persistance
35
+
36
+ ## 📦 Installation
37
+
38
+ ### Via npm/yarn/pnpm
39
+
40
+ ```bash
41
+ npm install @jsarc/intl @jsarc/cooks react
42
+ # ou
43
+ yarn add @jsarc/intl @jsarc/cooks react
44
+ # ou
45
+ pnpm add @jsarc/intl @jsarc/cooks react
46
+ ```
47
+
48
+ ### Dépendances requises
49
+ - React 19+
50
+ - @jsarc/cooks 1.0.0+
51
+ - TypeScript 5.0+ (recommandé)
52
+ - Vite (pour le chargement dynamique)
53
+
54
+ ## 🚀 Démarrage Rapide
55
+
56
+ ### Structure de projet recommandée
57
+
58
+ ```
59
+ src/
60
+ ├── locales/
61
+ │ ├── en.json # Traductions anglaises de base
62
+ │ └── fr.json # Traductions françaises de base
63
+ ├── modules/
64
+ │ ├── admin/
65
+ │ │ └── locales/
66
+ │ │ ├── en.json # Traductions admin anglaises
67
+ │ │ └── fr.json # Traductions admin françaises
68
+ │ └── dashboard/
69
+ │ └── locales/
70
+ │ ├── en.json # Traductions dashboard anglaises
71
+ │ └── fr.json # Traductions dashboard françaises
72
+ └── main.tsx
73
+ ```
74
+
75
+ ### Configuration de base
76
+
77
+ ```typescript
78
+ // main.tsx
79
+ import React from 'react';
80
+ import ReactDOM from 'react-dom/client';
81
+ import { ArcIntlProvider } from '@jsarc/intl';
82
+
83
+ const App = () => {
84
+ return (
85
+ <div>
86
+ <h1>Mon Application Internationalisée</h1>
87
+ {/* Votre contenu ici */}
88
+ </div>
89
+ );
90
+ };
91
+
92
+ ReactDOM.createRoot(document.getElementById('root')!).render(
93
+ <React.StrictMode>
94
+ <ArcIntlProvider
95
+ translations={{
96
+ base: {
97
+ en: async () => (await import('./locales/en.json')).default,
98
+ fr: async () => (await import('./locales/fr.json')).default
99
+ }
100
+ }}
101
+ supportedLocales={['en', 'fr', 'es']}
102
+ >
103
+ <App />
104
+ </ArcIntlProvider>
105
+ </React.StrictMode>
106
+ );
107
+ ```
108
+
109
+ ### Fichiers de traduction
110
+
111
+ ```json
112
+ // locales/en.json
113
+ {
114
+ "common": {
115
+ "welcome": "Welcome to our application",
116
+ "login": "Sign in",
117
+ "logout": "Sign out",
118
+ "save": "Save changes"
119
+ },
120
+ "errors": {
121
+ "required": "This field is required",
122
+ "email": "Please enter a valid email address"
123
+ }
124
+ }
125
+
126
+ // modules/admin/locales/en.json
127
+ {
128
+ "admin": {
129
+ "dashboard": {
130
+ "title": "Admin Dashboard",
131
+ "users": "Manage Users",
132
+ "settings": "System Settings"
133
+ }
134
+ }
135
+ }
136
+ ```
137
+
138
+ ## 📚 Documentation API
139
+
140
+ ### Hook useTranslation
141
+
142
+ ```typescript
143
+ import { useTranslation } from '@jsarc/intl';
144
+
145
+ const MyComponent = () => {
146
+ const {
147
+ t, // Fonction de traduction principale
148
+ changeLocale, // Changer la locale
149
+ currentLocale, // Locale actuelle
150
+ isLoading // État de chargement
151
+ } = useTranslation('admin'); // Optionnel: nom du module
152
+
153
+ // Exemple d'utilisation
154
+ const handleChangeLanguage = () => {
155
+ changeLocale(currentLocale === 'en' ? 'fr' : 'en');
156
+ };
157
+
158
+ return (
159
+ <div>
160
+ <h1>{t('admin.dashboard.title')}</h1>
161
+ <p>{t('common.welcome')}</p>
162
+ <button onClick={handleChangeLanguage}>
163
+ Switch to {currentLocale === 'en' ? 'French' : 'English'}
164
+ </button>
165
+ </div>
166
+ );
167
+ };
168
+ ```
169
+
170
+ ### ArcIntlProvider
171
+
172
+ ```typescript
173
+ import { ArcIntlProvider } from '@jsarc/intl';
174
+
175
+ // Configuration complète avec modules
176
+ <ArcIntlProvider
177
+ translations={{
178
+ base: {
179
+ en: async () => (await import('./locales/en.json')).default,
180
+ fr: async () => (await import('./locales/fr.json')).default
181
+ },
182
+ modules: {
183
+ admin: {
184
+ en: async () => (await import('./modules/admin/locales/en.json')).default,
185
+ fr: async () => (await import('./modules/admin/locales/fr.json')).default
186
+ },
187
+ dashboard: {
188
+ en: async () => (await import('./modules/dashboard/locales/en.json')).default,
189
+ fr: async () => (await import('./modules/dashboard/locales/fr.json')).default
190
+ }
191
+ }
192
+ }}
193
+ supportedLocales={['en', 'fr', 'es']}
194
+ >
195
+ {children}
196
+ </ArcIntlProvider>
197
+ ```
198
+
199
+ ## 🔧 Utilisation Avancée
200
+
201
+ ### Traductions avec paramètres
202
+
203
+ ```typescript
204
+ // locales/en.json
205
+ {
206
+ "greeting": "Hello, {name}!",
207
+ "items_count": "You have {count} item{count, plural, one {} other {s}}"
208
+ }
209
+
210
+ // Utilisation dans le composant
211
+ const Greeting = ({ userName, itemCount }) => {
212
+ const { t } = useTranslation();
213
+
214
+ return (
215
+ <div>
216
+ <p>{t('greeting', { name: userName })}</p>
217
+ <p>{t('items_count', { count: itemCount })}</p>
218
+ </div>
219
+ );
220
+ };
221
+ ```
222
+
223
+ ### Pluriels et contextes
224
+
225
+ ```typescript
226
+ // locales/en.json
227
+ {
228
+ "messages": {
229
+ "unread": {
230
+ "one": "You have one unread message",
231
+ "other": "You have {count} unread messages"
232
+ }
233
+ }
234
+ }
235
+
236
+ // Utilisation
237
+ const Notification = ({ unreadCount }) => {
238
+ const { t } = useTranslation();
239
+
240
+ return (
241
+ <div>
242
+ {/* Pluriel automatique */}
243
+ <p>{t('messages.unread', { count: unreadCount })}</p>
244
+ </div>
245
+ );
246
+ };
247
+ ```
248
+
249
+ ### Chargement dynamique de modules
250
+
251
+ ```typescript
252
+ import { useEffect } from 'react';
253
+ import { useTranslation } from '@jsarc/intl';
254
+
255
+ const AdminModule = () => {
256
+ const { loadModule, t, isModuleLoaded } = useTranslation('admin');
257
+
258
+ useEffect(() => {
259
+ // Charger les traductions du module admin à la demande
260
+ if (!isModuleLoaded) {
261
+ loadModule('admin');
262
+ }
263
+ }, []);
264
+
265
+ if (!isModuleLoaded) return <div>Loading translations...</div>;
266
+
267
+ return (
268
+ <div>
269
+ <h1>{t('admin.dashboard.title')}</h1>
270
+ </div>
271
+ );
272
+ };
273
+ ```
274
+
275
+ ## 🎯 Exemples Complets
276
+
277
+ ### Exemple 1 : Sélecteur de langue
278
+
279
+ ```typescript
280
+ import { useTranslation } from '@jsarc/intl';
281
+
282
+ const LanguageSwitcher = () => {
283
+ const { currentLocale, changeLocale, t } = useTranslation();
284
+
285
+ const languages = [
286
+ { code: 'en', name: 'English' },
287
+ { code: 'fr', name: 'Français' },
288
+ { code: 'es', name: 'Español' },
289
+ { code: 'de', name: 'Deutsch' }
290
+ ];
291
+
292
+ return (
293
+ <div className="language-switcher">
294
+ <span>{t('common.language')}:</span>
295
+ <select
296
+ value={currentLocale}
297
+ onChange={(e) => changeLocale(e.target.value)}
298
+ >
299
+ {languages.map(lang => (
300
+ <option key={lang.code} value={lang.code}>
301
+ {lang.name}
302
+ </option>
303
+ ))}
304
+ </select>
305
+ </div>
306
+ );
307
+ };
308
+ ```
309
+
310
+ ### Exemple 2 : Formulaire internationalisé
311
+
312
+ ```typescript
313
+ import { useTranslation } from '@jsarc/intl';
314
+ import { useState } from 'react';
315
+
316
+ const ContactForm = () => {
317
+ const { t } = useTranslation();
318
+ const [formData, setFormData] = useState({
319
+ name: '',
320
+ email: '',
321
+ message: ''
322
+ });
323
+
324
+ const handleSubmit = (e) => {
325
+ e.preventDefault();
326
+ // Soumettre le formulaire
327
+ };
328
+
329
+ return (
330
+ <form onSubmit={handleSubmit} className="contact-form">
331
+ <div className="form-group">
332
+ <label>{t('form.name')}</label>
333
+ <input
334
+ value={formData.name}
335
+ onChange={e => setFormData({...formData, name: e.target.value})}
336
+ placeholder={t('form.name_placeholder')}
337
+ />
338
+ {!formData.name && (
339
+ <span className="error">{t('errors.required')}</span>
340
+ )}
341
+ </div>
342
+
343
+ <div className="form-group">
344
+ <label>{t('form.email')}</label>
345
+ <input
346
+ type="email"
347
+ value={formData.email}
348
+ onChange={e => setFormData({...formData, email: e.target.value})}
349
+ placeholder={t('form.email_placeholder')}
350
+ />
351
+ </div>
352
+
353
+ <div className="form-group">
354
+ <label>{t('form.message')}</label>
355
+ <textarea
356
+ value={formData.message}
357
+ onChange={e => setFormData({...formData, message: e.target.value})}
358
+ placeholder={t('form.message_placeholder')}
359
+ rows={4}
360
+ />
361
+ </div>
362
+
363
+ <button type="submit">
364
+ {t('form.submit')}
365
+ </button>
366
+ </form>
367
+ );
368
+ };
369
+ ```
370
+
371
+ ### Exemple 3 : Dashboard avec modules multiples
372
+
373
+ ```typescript
374
+ import { useTranslation } from '@jsarc/intl';
375
+ import { useEffect } from 'react';
376
+
377
+ const Dashboard = () => {
378
+ const { t, loadModule, currentLocale, isLoading } = useTranslation('admin');
379
+
380
+ // Charger les traductions de plusieurs modules
381
+ useEffect(() => {
382
+ const loadModules = async () => {
383
+ await loadModule('admin');
384
+ await loadModule('analytics');
385
+ await loadModule('reports');
386
+ };
387
+ loadModules();
388
+ }, [currentLocale]);
389
+
390
+ if (isLoading) return <div>Loading translations...</div>;
391
+
392
+ return (
393
+ <div className="dashboard">
394
+ <header>
395
+ <h1>{t('admin.dashboard.title')}</h1>
396
+ <p>{t('common.welcome', { user: 'John Doe' })}</p>
397
+ </header>
398
+
399
+ <section className="stats">
400
+ <div className="stat-card">
401
+ <h3>{t('analytics.visitors', {}, { moduleName: 'analytics' })}</h3>
402
+ <p>1,234</p>
403
+ </div>
404
+
405
+ <div className="stat-card">
406
+ <h3>{t('reports.monthly', {}, { moduleName: 'reports' })}</h3>
407
+ <p>{t('reports.growth', { percent: '15%' })}</p>
408
+ </div>
409
+ </section>
410
+
411
+ <footer>
412
+ <p>{t('common.last_updated', { date: new Date().toLocaleDateString(currentLocale) })}</p>
413
+ </footer>
414
+ </div>
415
+ );
416
+ };
417
+ ```
418
+
419
+ ## 📋 API Reference
420
+
421
+ ### ArcIntlProvider
422
+ | Prop | Type | Description | Required |
423
+ |------|------|-------------|----------|
424
+ | `translations` | `TranslationsConfig` | Configuration des chargeurs de traductions | Oui |
425
+ | `supportedLocales` | `string[]` | Locales supportées (défaut: ['en', 'fr']) | Non |
426
+ | `children` | `React.ReactNode` | Composants enfants | Oui |
427
+
428
+ ### Hook useTranslation
429
+ Retourne un objet avec:
430
+ - `t(key: string, params?: Record<string, any>, options?: TranslationOptions)`: Fonction de traduction
431
+ - `changeLocale(locale: string)`: Changer la locale actuelle
432
+ - `currentLocale`: Locale actuelle
433
+ - `isLoading`: État de chargement global
434
+ - `isModuleLoaded`: Indique si le module demandé est chargé
435
+ - `loadModule(moduleName: string)`: Charge un module spécifique
436
+
437
+ ### Options de Traduction
438
+ ```typescript
439
+ interface TranslationOptions {
440
+ moduleName?: string; // Nom du module (défaut: 'core')
441
+ defaultValue?: string; // Valeur par défaut si clé non trouvée
442
+ count?: number; // Pour la gestion des pluriels
443
+ context?: string; // Contexte spécifique
444
+ }
445
+ ```
446
+
447
+ ### Structure de configuration
448
+ ```typescript
449
+ interface TranslationsConfig {
450
+ base: {
451
+ [locale: string]: () => Promise<Record<string, any>>;
452
+ };
453
+ modules?: {
454
+ [moduleName: string]: {
455
+ [locale: string]: () => Promise<Record<string, any>>;
456
+ };
457
+ };
458
+ }
459
+ ```
460
+
461
+ ## 🛡️ Gestion des Erreurs
462
+
463
+ ### Fallback sécurisé
464
+ ```typescript
465
+ const SafeComponent = () => {
466
+ const { t } = useTranslation();
467
+
468
+ // Utilisation sécurisée avec valeur par défaut
469
+ const title = t('nonexistent.key', {}, {
470
+ defaultValue: 'Default Title'
471
+ });
472
+
473
+ // Si pas de valeur par défaut, retourne la clé
474
+ const subtitle = t('another.missing.key'); // Retourne: "another.missing.key"
475
+
476
+ return (
477
+ <div>
478
+ <h1>{title}</h1>
479
+ <p>{subtitle}</p>
480
+ </div>
481
+ );
482
+ };
483
+ ```
484
+
485
+ ### Logs en développement
486
+ ```typescript
487
+ // En mode développement, les clés manquantes sont automatiquement loggées
488
+ const MissingTranslations = () => {
489
+ const { t } = useTranslation();
490
+
491
+ // Ceci loggue un avertissement en développement
492
+ const missingKey = t('non.existent.key');
493
+
494
+ return <div>{missingKey}</div>;
495
+ };
496
+ ```
497
+
498
+ ## 🔧 Configuration TypeScript
499
+
500
+ ```json
501
+ {
502
+ "compilerOptions": {
503
+ "target": "ES2020",
504
+ "lib": ["DOM", "DOM.Iterable", "ES2020"],
505
+ "module": "ESNext",
506
+ "moduleResolution": "bundler",
507
+ "allowImportingTsExtensions": true,
508
+ "resolveJsonModule": true,
509
+ "isolatedModules": true,
510
+ "noEmit": true,
511
+ "jsx": "react-jsx",
512
+ "strict": true,
513
+ "types": ["vite/client"]
514
+ },
515
+ "include": ["src", "node_modules/@jsarc/intl/**/*"]
516
+ }
517
+ ```
518
+
519
+ ## 📋 Table des Conventions
520
+
521
+ ### Structure des fichiers JSON
522
+
523
+ | Chemin | Description | Exemple |
524
+ |--------|-------------|---------|
525
+ | `locales/{locale}.json` | Traductions de base | `locales/en.json` |
526
+ | `modules/{module}/locales/{locale}.json` | Traductions du module | `modules/admin/locales/fr.json` |
527
+ | `public/locales/{locale}.json` | Traductions statiques (optionnel) | `public/locales/es.json` |
528
+
529
+ ### Clés de traduction
530
+
531
+ | Format | Description | Exemple |
532
+ |--------|-------------|---------|
533
+ | `namespace.key` | Clé simple | `common.save` |
534
+ | `namespace.nested.key` | Clé imbriquée | `form.contact.email` |
535
+ | `key_{context}` | Avec contexte | `save_draft`, `save_final` |
536
+
537
+ ### Paramètres supportés
538
+
539
+ | Paramètre | Type | Description |
540
+ |-----------|------|-------------|
541
+ | `count` | number | Pour la gestion des pluriels |
542
+ | `context` | string | Contexte spécifique (draft, final, etc.) |
543
+ | `moduleName` | string | Module cible pour la traduction |
544
+ | `defaultValue` | string | Valeur par défaut si la clé n'existe pas |
545
+
546
+ ## 🔧 Build et Développement
547
+
548
+ ### Scripts recommandés
549
+
550
+ ```json
551
+ {
552
+ "scripts": {
553
+ "dev": "vite",
554
+ "build": "tsc && vite build",
555
+ "preview": "vite preview",
556
+ "type-check": "tsc --noEmit",
557
+ "extract-translations": "node scripts/extract-keys.js",
558
+ "validate-translations": "node scripts/validate-translations.js"
559
+ }
560
+ }
561
+ ```
562
+
563
+ ### Configuration Vite
564
+
565
+ ```typescript
566
+ // vite.config.ts
567
+ import { defineConfig } from 'vite';
568
+ import react from '@vitejs/plugin-react';
569
+
570
+ export default defineConfig({
571
+ plugins: [react()],
572
+ resolve: {
573
+ alias: {
574
+ '@jsarc/intl': '@jsarc/intl/index.js'
575
+ }
576
+ }
577
+ });
578
+ ```
579
+
580
+ ## 📄 Licence
581
+
582
+ MIT License - Voir le fichier [LICENSE](LICENSE) pour plus de détails.
583
+
584
+ ## 🐛 Signaler un Bug
585
+
586
+ Envoyez-nous un mail à l'adresse `contact.inicode@gmail.com` pour :
587
+ - Signaler un bug
588
+ - Proposer une amélioration
589
+ - Poser une question sur l'utilisation
590
+ - Demander une nouvelle fonctionnalité
591
+
592
+ ---
593
+
594
+ **@jsarc/intl** - La solution d'internationalisation modulaire pour React et TypeScript.
595
+
596
+ *Développé par l'équipe INICODE*
package/config.ts ADDED
@@ -0,0 +1,36 @@
1
+ import { Cooks } from '@jsarc/cooks';
2
+
3
+ export const DEFAULT_LOCALE = 'en';
4
+ export const SUPPORTED_LOCALES: string[] = ['en', 'fr'] as const;
5
+ export const COOKIE_NAME = 'app_locale';
6
+
7
+ export type Locale = typeof SUPPORTED_LOCALES[number];
8
+
9
+ export interface TranslationMap {
10
+ [locale: string]: () => Promise<Record<string, any>>;
11
+ }
12
+
13
+ export interface ModuleTranslations {
14
+ [moduleName: string]: TranslationMap;
15
+ }
16
+
17
+ export interface TranslationsConfig {
18
+ base: {
19
+ [locale: string]: () => Promise<Record<string, any>>;
20
+ };
21
+ modules?: ModuleTranslations;
22
+ }
23
+
24
+ export const getSavedLocale = (supportedLocales: string[] = SUPPORTED_LOCALES): string => {
25
+ const saved = Cooks.get(COOKIE_NAME);
26
+ const supportedLocalesRes = (
27
+ typeof supportedLocales === 'object' &&
28
+ !!Array.isArray(supportedLocales) &&
29
+ supportedLocales.length > 0
30
+ ) ? supportedLocales : SUPPORTED_LOCALES;
31
+ return supportedLocalesRes.includes(saved as string) ? saved as string : DEFAULT_LOCALE;
32
+ };
33
+
34
+ export const saveLocale = (locale: string): void => {
35
+ Cooks.set(COOKIE_NAME, locale, { expires: 365 * 24 * 3600, sameSite: 'strict' });
36
+ };