@idealyst/translate 1.2.3

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,773 @@
1
+ # @idealyst/translate
2
+
3
+ Cross-platform internationalization for the Idealyst Framework. Wraps `react-i18next` with a unified API and includes a Babel plugin for static translation key analysis.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Features](#features)
8
+ - [Installation](#installation)
9
+ - [Quick Start](#quick-start)
10
+ - [Translation Files](#translation-files)
11
+ - [Runtime API](#runtime-api)
12
+ - [Babel Plugin](#babel-plugin)
13
+ - [CI/CD Integration](#cicd-integration)
14
+ - [Platform-Specific Setup](#platform-specific-setup)
15
+ - [Advanced Usage](#advanced-usage)
16
+ - [API Reference](#api-reference)
17
+
18
+ ## Features
19
+
20
+ - **Unified API** - Single API for React and React Native
21
+ - **Babel Plugin** - Static extraction of translation keys at build time
22
+ - **Missing Translation Detection** - Automatically detect keys missing translations
23
+ - **Unused Translation Detection** - Find translations not used in code
24
+ - **JSON Report** - Generate detailed reports for CI/CD integration
25
+ - **Namespace Support** - Organize translations with nested namespaces
26
+ - **Pluralization** - Full i18next pluralization support
27
+ - **Interpolation** - Variable interpolation in translations
28
+ - **Rich Text** - Component interpolation with the Trans component
29
+
30
+ ## Installation
31
+
32
+ ```bash
33
+ # npm
34
+ npm install @idealyst/translate react-i18next i18next
35
+
36
+ # yarn
37
+ yarn add @idealyst/translate react-i18next i18next
38
+
39
+ # pnpm
40
+ pnpm add @idealyst/translate react-i18next i18next
41
+ ```
42
+
43
+ ## Quick Start
44
+
45
+ ### 1. Create Translation Files
46
+
47
+ ```
48
+ locales/
49
+ ├── en/
50
+ │ └── common.json
51
+ └── es/
52
+ └── common.json
53
+ ```
54
+
55
+ **locales/en/common.json**
56
+ ```json
57
+ {
58
+ "welcome": {
59
+ "title": "Welcome to Our App",
60
+ "greeting": "Hello, {{name}}!"
61
+ },
62
+ "buttons": {
63
+ "submit": "Submit",
64
+ "cancel": "Cancel"
65
+ },
66
+ "items": "{{count}} item",
67
+ "items_plural": "{{count}} items"
68
+ }
69
+ ```
70
+
71
+ **locales/es/common.json**
72
+ ```json
73
+ {
74
+ "welcome": {
75
+ "title": "Bienvenido a Nuestra App",
76
+ "greeting": "¡Hola, {{name}}!"
77
+ },
78
+ "buttons": {
79
+ "submit": "Enviar",
80
+ "cancel": "Cancelar"
81
+ },
82
+ "items": "{{count}} artículo",
83
+ "items_plural": "{{count}} artículos"
84
+ }
85
+ ```
86
+
87
+ ### 2. Set Up the Provider
88
+
89
+ ```tsx
90
+ // App.tsx
91
+ import { TranslateProvider } from '@idealyst/translate';
92
+ import en from './locales/en/common.json';
93
+ import es from './locales/es/common.json';
94
+
95
+ const config = {
96
+ defaultLanguage: 'en',
97
+ languages: ['en', 'es'],
98
+ resources: {
99
+ en: { common: en },
100
+ es: { common: es },
101
+ },
102
+ defaultNamespace: 'common',
103
+ };
104
+
105
+ export function App() {
106
+ return (
107
+ <TranslateProvider config={config}>
108
+ <MyApp />
109
+ </TranslateProvider>
110
+ );
111
+ }
112
+ ```
113
+
114
+ ### 3. Use Translations
115
+
116
+ ```tsx
117
+ // MyComponent.tsx
118
+ import { useTranslation } from '@idealyst/translate';
119
+
120
+ function MyComponent() {
121
+ const { t } = useTranslation('common');
122
+
123
+ return (
124
+ <div>
125
+ {/* Simple translation */}
126
+ <h1>{t('welcome.title')}</h1>
127
+
128
+ {/* With interpolation */}
129
+ <p>{t('welcome.greeting', { name: 'John' })}</p>
130
+
131
+ {/* With pluralization */}
132
+ <p>{t('items', { count: 5 })}</p>
133
+
134
+ {/* Buttons */}
135
+ <button>{t('buttons.submit')}</button>
136
+ <button>{t('buttons.cancel')}</button>
137
+ </div>
138
+ );
139
+ }
140
+ ```
141
+
142
+ ### 4. Language Switching
143
+
144
+ ```tsx
145
+ import { useLanguage } from '@idealyst/translate';
146
+
147
+ function LanguageSwitcher() {
148
+ const { language, languages, setLanguage, getDisplayName } = useLanguage();
149
+
150
+ return (
151
+ <select value={language} onChange={(e) => setLanguage(e.target.value)}>
152
+ {languages.map((lang) => (
153
+ <option key={lang} value={lang}>
154
+ {getDisplayName(lang)}
155
+ </option>
156
+ ))}
157
+ </select>
158
+ );
159
+ }
160
+ ```
161
+
162
+ ## Translation Files
163
+
164
+ ### Directory Structure
165
+
166
+ Organize translations by language and namespace:
167
+
168
+ ```
169
+ locales/
170
+ ├── en/
171
+ │ ├── common.json # Common UI strings
172
+ │ ├── auth.json # Authentication strings
173
+ │ └── errors.json # Error messages
174
+ ├── es/
175
+ │ ├── common.json
176
+ │ ├── auth.json
177
+ │ └── errors.json
178
+ └── fr/
179
+ ├── common.json
180
+ ├── auth.json
181
+ └── errors.json
182
+ ```
183
+
184
+ ### JSON Format
185
+
186
+ ```json
187
+ {
188
+ "namespace.key": "value",
189
+
190
+ "nested": {
191
+ "keys": {
192
+ "work": "Like this"
193
+ }
194
+ },
195
+
196
+ "interpolation": "Hello, {{name}}!",
197
+
198
+ "plural": "{{count}} item",
199
+ "plural_plural": "{{count}} items",
200
+
201
+ "context_male": "He liked it",
202
+ "context_female": "She liked it",
203
+
204
+ "richText": "Read our <terms>Terms</terms> and <privacy>Privacy Policy</privacy>"
205
+ }
206
+ ```
207
+
208
+ ### Key Formats
209
+
210
+ The plugin supports two key formats:
211
+
212
+ ```tsx
213
+ // Namespace:key format (i18next standard)
214
+ t('auth:login.title')
215
+
216
+ // Namespace.key format (first segment is namespace)
217
+ t('auth.login.title')
218
+
219
+ // Both resolve to:
220
+ // namespace: "auth"
221
+ // localKey: "login.title"
222
+ ```
223
+
224
+ ## Runtime API
225
+
226
+ ### TranslateProvider
227
+
228
+ Wrap your app with the provider:
229
+
230
+ ```tsx
231
+ import { TranslateProvider } from '@idealyst/translate';
232
+
233
+ <TranslateProvider
234
+ config={{
235
+ defaultLanguage: 'en',
236
+ languages: ['en', 'es', 'fr'],
237
+ resources: {
238
+ en: { common: enCommon, auth: enAuth },
239
+ es: { common: esCommon, auth: esAuth },
240
+ },
241
+ defaultNamespace: 'common',
242
+ fallbackLanguage: 'en',
243
+ debug: false,
244
+ }}
245
+ onInitialized={(i18n) => console.log('i18n ready')}
246
+ onLanguageChanged={(lang) => console.log('Language:', lang)}
247
+ >
248
+ <App />
249
+ </TranslateProvider>
250
+ ```
251
+
252
+ ### useTranslation Hook
253
+
254
+ ```tsx
255
+ import { useTranslation } from '@idealyst/translate';
256
+
257
+ function Component() {
258
+ const { t, language, languages, ready, i18n } = useTranslation('common');
259
+
260
+ // Simple translation
261
+ const title = t('welcome.title');
262
+
263
+ // With interpolation
264
+ const greeting = t('welcome.greeting', { name: 'World' });
265
+
266
+ // With default value
267
+ const fallback = t('missing.key', { defaultValue: 'Fallback text' });
268
+
269
+ // With pluralization
270
+ const items = t('items', { count: 5 });
271
+
272
+ // With context
273
+ const gendered = t('liked', { context: 'male' });
274
+
275
+ return <div>{title}</div>;
276
+ }
277
+ ```
278
+
279
+ ### useLanguage Hook
280
+
281
+ ```tsx
282
+ import { useLanguage } from '@idealyst/translate';
283
+
284
+ function LanguageControls() {
285
+ const {
286
+ language, // Current language: 'en'
287
+ languages, // Available: ['en', 'es', 'fr']
288
+ setLanguage, // Change language
289
+ isSupported, // Check if language is available
290
+ getDisplayName, // Get display name: 'English', 'Español'
291
+ } = useLanguage();
292
+
293
+ const handleChange = async (newLang: string) => {
294
+ if (isSupported(newLang)) {
295
+ await setLanguage(newLang);
296
+ }
297
+ };
298
+
299
+ return (
300
+ <div>
301
+ <p>Current: {getDisplayName(language)}</p>
302
+ <button onClick={() => handleChange('es')}>Switch to Spanish</button>
303
+ </div>
304
+ );
305
+ }
306
+ ```
307
+
308
+ ### Trans Component
309
+
310
+ For rich text with embedded components:
311
+
312
+ ```tsx
313
+ import { Trans } from '@idealyst/translate';
314
+
315
+ function RichText() {
316
+ return (
317
+ <Trans
318
+ i18nKey="common.richText"
319
+ components={{
320
+ terms: <a href="/terms" />,
321
+ privacy: <a href="/privacy" />,
322
+ bold: <strong />,
323
+ }}
324
+ values={{ name: 'User' }}
325
+ />
326
+ );
327
+ }
328
+
329
+ // With translation:
330
+ // "richText": "Read our <terms>Terms</terms> and <privacy>Privacy Policy</privacy>, <bold>{{name}}</bold>!"
331
+ //
332
+ // Renders:
333
+ // Read our <a href="/terms">Terms</a> and <a href="/privacy">Privacy Policy</a>, <strong>User</strong>!
334
+ ```
335
+
336
+ ## Babel Plugin
337
+
338
+ The Babel plugin extracts translation keys at build time and generates a report of missing/unused translations.
339
+
340
+ ### Configuration
341
+
342
+ **babel.config.js**
343
+ ```javascript
344
+ module.exports = {
345
+ presets: ['@babel/preset-react', '@babel/preset-typescript'],
346
+ plugins: [
347
+ ['@idealyst/translate/plugin', {
348
+ // Required: paths to translation JSON files (supports glob)
349
+ translationFiles: ['./locales/**/*.json'],
350
+
351
+ // Optional: output path for the report
352
+ reportPath: '.idealyst/translations-report.json',
353
+
354
+ // Optional: default namespace when not specified in key
355
+ defaultNamespace: 'common',
356
+
357
+ // Optional: emit console warnings for missing translations
358
+ emitWarnings: true,
359
+
360
+ // Optional: fail build if missing translations found
361
+ failOnMissing: false,
362
+
363
+ // Optional: verbose logging
364
+ verbose: false,
365
+ }],
366
+ ],
367
+ };
368
+ ```
369
+
370
+ **For Vite (vite.config.ts)**
371
+ ```typescript
372
+ import { defineConfig } from 'vite';
373
+ import react from '@vitejs/plugin-react';
374
+
375
+ export default defineConfig({
376
+ plugins: [
377
+ react({
378
+ babel: {
379
+ plugins: [
380
+ ['@idealyst/translate/plugin', {
381
+ translationFiles: ['./locales/**/*.json'],
382
+ reportPath: '.idealyst/translations-report.json',
383
+ defaultNamespace: 'common',
384
+ }],
385
+ ],
386
+ },
387
+ }),
388
+ ],
389
+ });
390
+ ```
391
+
392
+ **For React Native (babel.config.js)**
393
+ ```javascript
394
+ module.exports = {
395
+ presets: ['module:@react-native/babel-preset'],
396
+ plugins: [
397
+ ['@idealyst/translate/plugin', {
398
+ translationFiles: ['./locales/**/*.json'],
399
+ reportPath: '.idealyst/translations-report.json',
400
+ defaultNamespace: 'common',
401
+ emitWarnings: true,
402
+ }],
403
+ ],
404
+ };
405
+ ```
406
+
407
+ ### What the Plugin Extracts
408
+
409
+ The plugin statically analyzes your code for:
410
+
411
+ ```tsx
412
+ // t() function calls
413
+ t('common.key')
414
+ t('namespace:key')
415
+ t('key', { defaultValue: 'Default' })
416
+
417
+ // i18n.t() method calls
418
+ i18n.t('common.key')
419
+
420
+ // Trans component
421
+ <Trans i18nKey="common.richText" />
422
+ <Trans i18nKey={"common.richText"} />
423
+
424
+ // Dynamic keys are tracked but marked as such
425
+ const key = `common.${type}`;
426
+ t(key); // Marked as isDynamic: true
427
+ ```
428
+
429
+ ### Report Output
430
+
431
+ The plugin generates `.idealyst/translations-report.json`:
432
+
433
+ ```json
434
+ {
435
+ "timestamp": "2026-01-08T12:00:00.000Z",
436
+ "totalKeys": 45,
437
+ "dynamicKeys": [
438
+ {
439
+ "key": "<dynamic>",
440
+ "file": "src/DynamicComponent.tsx",
441
+ "line": 15,
442
+ "isDynamic": true
443
+ }
444
+ ],
445
+ "extractedKeys": [
446
+ {
447
+ "key": "common.buttons.submit",
448
+ "namespace": "common",
449
+ "localKey": "buttons.submit",
450
+ "file": "src/Form.tsx",
451
+ "line": 42,
452
+ "column": 12,
453
+ "defaultValue": "Submit",
454
+ "isDynamic": false
455
+ }
456
+ ],
457
+ "languages": ["en", "es", "fr"],
458
+ "missing": {
459
+ "en": [],
460
+ "es": [
461
+ {
462
+ "key": "common.buttons.submit",
463
+ "namespace": "common",
464
+ "usedIn": [
465
+ { "file": "src/Form.tsx", "line": 42, "column": 12 }
466
+ ],
467
+ "defaultValue": "Submit"
468
+ }
469
+ ],
470
+ "fr": []
471
+ },
472
+ "unused": {
473
+ "en": ["common.legacy.oldFeature"],
474
+ "es": [],
475
+ "fr": []
476
+ },
477
+ "summary": {
478
+ "totalMissing": 1,
479
+ "totalUnused": 1,
480
+ "coveragePercent": {
481
+ "en": 100,
482
+ "es": 98,
483
+ "fr": 100
484
+ }
485
+ }
486
+ }
487
+ ```
488
+
489
+ ## CI/CD Integration
490
+
491
+ ### GitHub Actions Example
492
+
493
+ ```yaml
494
+ name: Translation Check
495
+
496
+ on: [push, pull_request]
497
+
498
+ jobs:
499
+ check-translations:
500
+ runs-on: ubuntu-latest
501
+ steps:
502
+ - uses: actions/checkout@v3
503
+
504
+ - name: Setup Node
505
+ uses: actions/setup-node@v3
506
+ with:
507
+ node-version: '20'
508
+
509
+ - name: Install dependencies
510
+ run: yarn install
511
+
512
+ - name: Build (generates translation report)
513
+ run: yarn build
514
+
515
+ - name: Check for missing translations
516
+ run: |
517
+ MISSING=$(jq '.summary.totalMissing' .idealyst/translations-report.json)
518
+ if [ "$MISSING" -gt 0 ]; then
519
+ echo "Missing translations found: $MISSING"
520
+ jq '.missing' .idealyst/translations-report.json
521
+ exit 1
522
+ fi
523
+
524
+ - name: Check coverage threshold
525
+ run: |
526
+ jq -e '.summary.coveragePercent | to_entries | all(.value >= 95)' \
527
+ .idealyst/translations-report.json || \
528
+ (echo "Translation coverage below 95%" && exit 1)
529
+ ```
530
+
531
+ ### Shell Script
532
+
533
+ ```bash
534
+ #!/bin/bash
535
+
536
+ # Build and generate report
537
+ yarn build
538
+
539
+ # Check for missing translations
540
+ MISSING=$(jq '.summary.totalMissing' .idealyst/translations-report.json)
541
+
542
+ if [ "$MISSING" -gt 0 ]; then
543
+ echo "ERROR: $MISSING missing translation(s) found"
544
+ echo ""
545
+ echo "Missing translations:"
546
+ jq -r '.missing | to_entries[] | select(.value | length > 0) | "\(.key): \(.value | length) missing"' \
547
+ .idealyst/translations-report.json
548
+ exit 1
549
+ fi
550
+
551
+ echo "All translations present!"
552
+ ```
553
+
554
+ ## Platform-Specific Setup
555
+
556
+ ### React (Vite)
557
+
558
+ ```typescript
559
+ // vite.config.ts
560
+ import { defineConfig } from 'vite';
561
+ import react from '@vitejs/plugin-react';
562
+
563
+ export default defineConfig({
564
+ plugins: [
565
+ react({
566
+ babel: {
567
+ plugins: [
568
+ ['@idealyst/translate/plugin', {
569
+ translationFiles: ['./src/locales/**/*.json'],
570
+ reportPath: '.idealyst/translations-report.json',
571
+ }],
572
+ ],
573
+ },
574
+ }),
575
+ ],
576
+ });
577
+ ```
578
+
579
+ ### React Native
580
+
581
+ ```javascript
582
+ // babel.config.js
583
+ module.exports = {
584
+ presets: ['module:@react-native/babel-preset'],
585
+ plugins: [
586
+ ['@idealyst/translate/plugin', {
587
+ translationFiles: ['./src/locales/**/*.json'],
588
+ reportPath: '.idealyst/translations-report.json',
589
+ }],
590
+ ],
591
+ };
592
+ ```
593
+
594
+ ### Next.js
595
+
596
+ ```javascript
597
+ // next.config.js
598
+ module.exports = {
599
+ // ... other config
600
+ };
601
+
602
+ // babel.config.js
603
+ module.exports = {
604
+ presets: ['next/babel'],
605
+ plugins: [
606
+ ['@idealyst/translate/plugin', {
607
+ translationFiles: ['./locales/**/*.json'],
608
+ reportPath: '.idealyst/translations-report.json',
609
+ }],
610
+ ],
611
+ };
612
+ ```
613
+
614
+ ## Advanced Usage
615
+
616
+ ### Multiple Namespaces
617
+
618
+ ```tsx
619
+ // Load multiple namespaces
620
+ const { t } = useTranslation(['common', 'auth']);
621
+
622
+ // Use specific namespace
623
+ t('common:buttons.submit')
624
+ t('auth:login.title')
625
+ ```
626
+
627
+ ### Lazy Loading Namespaces
628
+
629
+ ```tsx
630
+ import { useTranslation } from '@idealyst/translate';
631
+
632
+ function LazyComponent() {
633
+ // Namespace loaded on demand
634
+ const { t, ready } = useTranslation('largeNamespace');
635
+
636
+ if (!ready) return <Loading />;
637
+
638
+ return <div>{t('content')}</div>;
639
+ }
640
+ ```
641
+
642
+ ### Detecting Language from Browser/Device
643
+
644
+ ```tsx
645
+ const config = {
646
+ defaultLanguage: navigator.language.split('-')[0] || 'en',
647
+ languages: ['en', 'es', 'fr'],
648
+ // ...
649
+ };
650
+ ```
651
+
652
+ ### Persisting Language Preference
653
+
654
+ ```tsx
655
+ import { useLanguage } from '@idealyst/translate';
656
+ import { useEffect } from 'react';
657
+
658
+ function LanguagePersistence() {
659
+ const { language, setLanguage } = useLanguage();
660
+
661
+ // Load saved preference on mount
662
+ useEffect(() => {
663
+ const saved = localStorage.getItem('language');
664
+ if (saved) setLanguage(saved);
665
+ }, []);
666
+
667
+ // Save preference when changed
668
+ useEffect(() => {
669
+ localStorage.setItem('language', language);
670
+ }, [language]);
671
+
672
+ return null;
673
+ }
674
+ ```
675
+
676
+ ## API Reference
677
+
678
+ ### TranslateConfig
679
+
680
+ ```typescript
681
+ interface TranslateConfig {
682
+ /** Default language code */
683
+ defaultLanguage: string;
684
+
685
+ /** Supported language codes */
686
+ languages: string[];
687
+
688
+ /** Pre-loaded translation resources */
689
+ resources?: Record<string, Record<string, object>>;
690
+
691
+ /** Default namespace */
692
+ defaultNamespace?: string;
693
+
694
+ /** Fallback language when key missing */
695
+ fallbackLanguage?: string;
696
+
697
+ /** Enable debug logging */
698
+ debug?: boolean;
699
+ }
700
+ ```
701
+
702
+ ### TranslatePluginOptions
703
+
704
+ ```typescript
705
+ interface TranslatePluginOptions {
706
+ /** Paths to translation files (glob patterns supported) */
707
+ translationFiles: string[];
708
+
709
+ /** Output path for report (default: '.idealyst/translations-report.json') */
710
+ reportPath?: string;
711
+
712
+ /** Languages to check (default: inferred from files) */
713
+ languages?: string[];
714
+
715
+ /** Default namespace (default: 'translation') */
716
+ defaultNamespace?: string;
717
+
718
+ /** Fail build on missing translations (default: false) */
719
+ failOnMissing?: boolean;
720
+
721
+ /** Emit console warnings (default: true) */
722
+ emitWarnings?: boolean;
723
+
724
+ /** Verbose logging (default: false) */
725
+ verbose?: boolean;
726
+ }
727
+ ```
728
+
729
+ ### useTranslation Return Value
730
+
731
+ ```typescript
732
+ interface UseTranslationResult {
733
+ /** Translation function */
734
+ t: (key: string, options?: TranslationOptions) => string;
735
+
736
+ /** Current language code */
737
+ language: string;
738
+
739
+ /** All available languages */
740
+ languages: string[];
741
+
742
+ /** Whether translations are loaded */
743
+ ready: boolean;
744
+
745
+ /** i18next instance for advanced usage */
746
+ i18n: i18n;
747
+ }
748
+ ```
749
+
750
+ ### useLanguage Return Value
751
+
752
+ ```typescript
753
+ interface UseLanguageResult {
754
+ /** Current language code */
755
+ language: string;
756
+
757
+ /** Available language codes */
758
+ languages: string[];
759
+
760
+ /** Change the current language */
761
+ setLanguage: (lang: string) => Promise<void>;
762
+
763
+ /** Check if a language is supported */
764
+ isSupported: (lang: string) => boolean;
765
+
766
+ /** Get display name for a language code */
767
+ getDisplayName: (lang: string) => string;
768
+ }
769
+ ```
770
+
771
+ ## License
772
+
773
+ MIT