@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 +773 -0
- package/package.json +77 -0
- package/src/babel/__tests__/extractor.test.ts +224 -0
- package/src/babel/__tests__/plugin.test.ts +289 -0
- package/src/babel/__tests__/reporter.test.ts +314 -0
- package/src/babel/extractor.ts +179 -0
- package/src/babel/index.ts +21 -0
- package/src/babel/plugin.js +545 -0
- package/src/babel/reporter.ts +287 -0
- package/src/babel/types.ts +214 -0
- package/src/components/Trans.tsx +72 -0
- package/src/components/index.ts +2 -0
- package/src/components/types.ts +64 -0
- package/src/config/index.ts +2 -0
- package/src/config/types.ts +10 -0
- package/src/hooks/index.ts +8 -0
- package/src/hooks/types.ts +113 -0
- package/src/hooks/useLanguage.ts +73 -0
- package/src/hooks/useTranslation.ts +52 -0
- package/src/index.native.ts +2 -0
- package/src/index.ts +22 -0
- package/src/index.web.ts +24 -0
- package/src/provider/TranslateProvider.tsx +132 -0
- package/src/provider/index.ts +6 -0
- package/src/provider/types.ts +119 -0
- package/src/utils/__tests__/namespace.test.ts +211 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/namespace.ts +97 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import type {
|
|
4
|
+
TranslatePluginOptions,
|
|
5
|
+
ExtractedKey,
|
|
6
|
+
TranslationsReport,
|
|
7
|
+
MissingTranslation,
|
|
8
|
+
LoadedTranslations,
|
|
9
|
+
} from './types';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Check if an object has a nested key
|
|
13
|
+
*/
|
|
14
|
+
function hasNestedKey(obj: Record<string, unknown>, keyPath: string): boolean {
|
|
15
|
+
const parts = keyPath.split('.');
|
|
16
|
+
let current: unknown = obj;
|
|
17
|
+
|
|
18
|
+
for (const part of parts) {
|
|
19
|
+
if (current === undefined || current === null) return false;
|
|
20
|
+
if (typeof current !== 'object') return false;
|
|
21
|
+
current = (current as Record<string, unknown>)[part];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return current !== undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Flatten an object's keys into dot-notation
|
|
29
|
+
*/
|
|
30
|
+
function flattenKeys(obj: Record<string, unknown>, prefix: string = ''): string[] {
|
|
31
|
+
const keys: string[] = [];
|
|
32
|
+
|
|
33
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
34
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
35
|
+
|
|
36
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
37
|
+
keys.push(...flattenKeys(value as Record<string, unknown>, fullKey));
|
|
38
|
+
} else {
|
|
39
|
+
keys.push(fullKey);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return keys;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Parse a key into namespace and local key
|
|
48
|
+
*/
|
|
49
|
+
function parseKey(
|
|
50
|
+
fullKey: string,
|
|
51
|
+
defaultNamespace: string = 'translation'
|
|
52
|
+
): { namespace: string; localKey: string } {
|
|
53
|
+
if (fullKey.includes(':')) {
|
|
54
|
+
const [namespace, ...rest] = fullKey.split(':');
|
|
55
|
+
return { namespace, localKey: rest.join(':') };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const segments = fullKey.split('.');
|
|
59
|
+
if (segments.length > 1) {
|
|
60
|
+
return { namespace: segments[0], localKey: segments.slice(1).join('.') };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { namespace: defaultNamespace, localKey: fullKey };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Extract language code from file path
|
|
68
|
+
*/
|
|
69
|
+
function extractLanguageFromPath(filePath: string): string {
|
|
70
|
+
const parts = filePath.split(path.sep);
|
|
71
|
+
const filename = path.basename(filePath, '.json');
|
|
72
|
+
|
|
73
|
+
if (filename.match(/^[a-z]{2}(-[A-Z]{2})?$/)) {
|
|
74
|
+
return filename;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const parentDir = parts[parts.length - 2];
|
|
78
|
+
if (parentDir && parentDir.match(/^[a-z]{2}(-[A-Z]{2})?$/)) {
|
|
79
|
+
return parentDir;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return filename;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Extract namespace from file path
|
|
87
|
+
*/
|
|
88
|
+
function extractNamespaceFromPath(filePath: string): string {
|
|
89
|
+
const filename = path.basename(filePath, '.json');
|
|
90
|
+
|
|
91
|
+
if (filename.match(/^[a-z]{2}(-[A-Z]{2})?$/)) {
|
|
92
|
+
return 'translation';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return filename;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Load translation files from glob patterns
|
|
100
|
+
*/
|
|
101
|
+
export function loadTranslations(
|
|
102
|
+
patterns: string[],
|
|
103
|
+
verbose: boolean = false
|
|
104
|
+
): LoadedTranslations {
|
|
105
|
+
const result: LoadedTranslations = {};
|
|
106
|
+
|
|
107
|
+
// Dynamic import glob if available
|
|
108
|
+
let globSync: ((pattern: string) => string[]) | null = null;
|
|
109
|
+
try {
|
|
110
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
111
|
+
const glob = require('glob');
|
|
112
|
+
globSync = glob.sync;
|
|
113
|
+
} catch {
|
|
114
|
+
globSync = null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
for (const pattern of patterns) {
|
|
118
|
+
let files: string[] = [];
|
|
119
|
+
|
|
120
|
+
if (globSync) {
|
|
121
|
+
try {
|
|
122
|
+
files = globSync(pattern);
|
|
123
|
+
} catch (err) {
|
|
124
|
+
if (verbose) {
|
|
125
|
+
console.warn(`[@idealyst/translate] Failed to glob pattern: ${pattern}`, err);
|
|
126
|
+
}
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
} else if (fs.existsSync(pattern)) {
|
|
130
|
+
files = [pattern];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
for (const file of files) {
|
|
134
|
+
try {
|
|
135
|
+
const content = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
136
|
+
const lang = extractLanguageFromPath(file);
|
|
137
|
+
const namespace = extractNamespaceFromPath(file);
|
|
138
|
+
|
|
139
|
+
if (!result[lang]) result[lang] = {};
|
|
140
|
+
result[lang][namespace] = content;
|
|
141
|
+
|
|
142
|
+
if (verbose) {
|
|
143
|
+
console.log(`[@idealyst/translate] Loaded: ${file} -> ${lang}/${namespace}`);
|
|
144
|
+
}
|
|
145
|
+
} catch (err) {
|
|
146
|
+
if (verbose) {
|
|
147
|
+
console.warn(`[@idealyst/translate] Failed to load: ${file}`, err);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Generate a translations report from extracted keys
|
|
158
|
+
*/
|
|
159
|
+
export function generateReport(
|
|
160
|
+
extractedKeys: ExtractedKey[],
|
|
161
|
+
options: TranslatePluginOptions
|
|
162
|
+
): TranslationsReport {
|
|
163
|
+
const {
|
|
164
|
+
translationFiles,
|
|
165
|
+
languages: configLanguages,
|
|
166
|
+
defaultNamespace = 'translation',
|
|
167
|
+
verbose = false,
|
|
168
|
+
} = options;
|
|
169
|
+
|
|
170
|
+
const translations = loadTranslations(translationFiles, verbose);
|
|
171
|
+
const languages = configLanguages || Object.keys(translations);
|
|
172
|
+
|
|
173
|
+
const staticKeys = extractedKeys.filter((k) => !k.isDynamic);
|
|
174
|
+
const dynamicKeys = extractedKeys.filter((k) => k.isDynamic);
|
|
175
|
+
const uniqueKeys = [...new Set(staticKeys.map((k) => k.key))];
|
|
176
|
+
|
|
177
|
+
// Find missing translations per language
|
|
178
|
+
const missing: Record<string, MissingTranslation[]> = {};
|
|
179
|
+
for (const lang of languages) {
|
|
180
|
+
missing[lang] = [];
|
|
181
|
+
const langTranslations = translations[lang] || {};
|
|
182
|
+
|
|
183
|
+
for (const key of uniqueKeys) {
|
|
184
|
+
const { namespace, localKey } = parseKey(key, defaultNamespace);
|
|
185
|
+
const nsTranslations = langTranslations[namespace] || {};
|
|
186
|
+
|
|
187
|
+
if (!hasNestedKey(nsTranslations as Record<string, unknown>, localKey)) {
|
|
188
|
+
const usages = staticKeys.filter((k) => k.key === key);
|
|
189
|
+
missing[lang].push({
|
|
190
|
+
key,
|
|
191
|
+
namespace,
|
|
192
|
+
usedIn: usages.map((u) => ({
|
|
193
|
+
file: u.file,
|
|
194
|
+
line: u.line,
|
|
195
|
+
column: u.column,
|
|
196
|
+
})),
|
|
197
|
+
defaultValue: usages[0]?.defaultValue,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Find unused translations per language
|
|
204
|
+
const unused: Record<string, string[]> = {};
|
|
205
|
+
for (const lang of languages) {
|
|
206
|
+
unused[lang] = [];
|
|
207
|
+
const langTranslations = translations[lang] || {};
|
|
208
|
+
|
|
209
|
+
for (const namespace of Object.keys(langTranslations)) {
|
|
210
|
+
const keys = flattenKeys(
|
|
211
|
+
langTranslations[namespace] as Record<string, unknown>,
|
|
212
|
+
namespace
|
|
213
|
+
);
|
|
214
|
+
for (const translationKey of keys) {
|
|
215
|
+
if (!uniqueKeys.includes(translationKey)) {
|
|
216
|
+
unused[lang].push(translationKey);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Calculate coverage
|
|
223
|
+
const coveragePercent: Record<string, number> = {};
|
|
224
|
+
for (const lang of languages) {
|
|
225
|
+
const totalKeys = uniqueKeys.length;
|
|
226
|
+
const missingCount = missing[lang].length;
|
|
227
|
+
coveragePercent[lang] =
|
|
228
|
+
totalKeys > 0 ? Math.round(((totalKeys - missingCount) / totalKeys) * 100) : 100;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
timestamp: new Date().toISOString(),
|
|
233
|
+
totalKeys: uniqueKeys.length,
|
|
234
|
+
dynamicKeys,
|
|
235
|
+
extractedKeys: staticKeys,
|
|
236
|
+
languages,
|
|
237
|
+
missing,
|
|
238
|
+
unused,
|
|
239
|
+
summary: {
|
|
240
|
+
totalMissing: Object.values(missing).flat().length,
|
|
241
|
+
totalUnused: Object.values(unused).flat().length,
|
|
242
|
+
coveragePercent,
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Write a translations report to disk
|
|
249
|
+
*/
|
|
250
|
+
export function writeReport(report: TranslationsReport, outputPath: string): void {
|
|
251
|
+
const dir = path.dirname(outputPath);
|
|
252
|
+
if (!fs.existsSync(dir)) {
|
|
253
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
254
|
+
}
|
|
255
|
+
fs.writeFileSync(outputPath, JSON.stringify(report, null, 2), 'utf-8');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Print report summary to console
|
|
260
|
+
*/
|
|
261
|
+
export function printReportSummary(report: TranslationsReport): void {
|
|
262
|
+
console.log('\n[@idealyst/translate] Translation Report Summary');
|
|
263
|
+
console.log('='.repeat(50));
|
|
264
|
+
console.log(`Total keys: ${report.totalKeys}`);
|
|
265
|
+
console.log(`Dynamic keys: ${report.dynamicKeys.length}`);
|
|
266
|
+
console.log(`Languages: ${report.languages.join(', ')}`);
|
|
267
|
+
console.log('');
|
|
268
|
+
|
|
269
|
+
for (const lang of report.languages) {
|
|
270
|
+
const coverage = report.summary.coveragePercent[lang];
|
|
271
|
+
const missingCount = report.missing[lang]?.length || 0;
|
|
272
|
+
const unusedCount = report.unused[lang]?.length || 0;
|
|
273
|
+
|
|
274
|
+
console.log(`${lang}: ${coverage}% coverage`);
|
|
275
|
+
if (missingCount > 0) {
|
|
276
|
+
console.log(` - ${missingCount} missing translation(s)`);
|
|
277
|
+
}
|
|
278
|
+
if (unusedCount > 0) {
|
|
279
|
+
console.log(` - ${unusedCount} unused translation(s)`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
console.log('');
|
|
284
|
+
console.log(`Total missing: ${report.summary.totalMissing}`);
|
|
285
|
+
console.log(`Total unused: ${report.summary.totalUnused}`);
|
|
286
|
+
console.log('='.repeat(50));
|
|
287
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Options for the @idealyst/translate Babel plugin
|
|
3
|
+
*/
|
|
4
|
+
export interface TranslatePluginOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Paths to translation JSON files
|
|
7
|
+
* Supports glob patterns: ['./locales/*.json', './src/locales/**\/*.json']
|
|
8
|
+
*/
|
|
9
|
+
translationFiles: string[];
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Output path for the translation report
|
|
13
|
+
* @default '.idealyst/translations-report.json'
|
|
14
|
+
*/
|
|
15
|
+
reportPath?: string;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Languages to check against
|
|
19
|
+
* If not provided, inferred from translation file names/directories
|
|
20
|
+
*/
|
|
21
|
+
languages?: string[];
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Default namespace used when namespace is not specified in the key
|
|
25
|
+
* @default 'translation'
|
|
26
|
+
*/
|
|
27
|
+
defaultNamespace?: string;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Whether to fail the build if missing translations are found
|
|
31
|
+
* @default false
|
|
32
|
+
*/
|
|
33
|
+
failOnMissing?: boolean;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Whether to emit console warnings for missing translations
|
|
37
|
+
* @default true
|
|
38
|
+
*/
|
|
39
|
+
emitWarnings?: boolean;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Whether to include source locations in the report
|
|
43
|
+
* @default true
|
|
44
|
+
*/
|
|
45
|
+
includeLocations?: boolean;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Enable verbose logging
|
|
49
|
+
* @default false
|
|
50
|
+
*/
|
|
51
|
+
verbose?: boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* A translation key extracted from source code
|
|
56
|
+
*/
|
|
57
|
+
export interface ExtractedKey {
|
|
58
|
+
/**
|
|
59
|
+
* Full key path including namespace: "common.buttons.submit" or "common:buttons.submit"
|
|
60
|
+
*/
|
|
61
|
+
key: string;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Namespace portion: "common"
|
|
65
|
+
*/
|
|
66
|
+
namespace: string;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Key without namespace: "buttons.submit"
|
|
70
|
+
*/
|
|
71
|
+
localKey: string;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Source file path
|
|
75
|
+
*/
|
|
76
|
+
file: string;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Line number in source
|
|
80
|
+
*/
|
|
81
|
+
line: number;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Column number in source
|
|
85
|
+
*/
|
|
86
|
+
column: number;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Default value if provided in code
|
|
90
|
+
*/
|
|
91
|
+
defaultValue?: string;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Whether this is a dynamic key (contains variables, cannot be statically analyzed)
|
|
95
|
+
*/
|
|
96
|
+
isDynamic: boolean;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* A missing translation entry
|
|
101
|
+
*/
|
|
102
|
+
export interface MissingTranslation {
|
|
103
|
+
/**
|
|
104
|
+
* The missing key
|
|
105
|
+
*/
|
|
106
|
+
key: string;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Namespace
|
|
110
|
+
*/
|
|
111
|
+
namespace: string;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Files where this key is used
|
|
115
|
+
*/
|
|
116
|
+
usedIn: Array<{
|
|
117
|
+
file: string;
|
|
118
|
+
line: number;
|
|
119
|
+
column: number;
|
|
120
|
+
}>;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Default value if provided in code
|
|
124
|
+
*/
|
|
125
|
+
defaultValue?: string;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Translation coverage statistics
|
|
130
|
+
*/
|
|
131
|
+
export interface CoverageStats {
|
|
132
|
+
/**
|
|
133
|
+
* Total number of unique static keys found in code
|
|
134
|
+
*/
|
|
135
|
+
totalKeys: number;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Number of keys missing translation for this language
|
|
139
|
+
*/
|
|
140
|
+
missingCount: number;
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Percentage of keys that have translations (0-100)
|
|
144
|
+
*/
|
|
145
|
+
coveragePercent: number;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* The full translations report output
|
|
150
|
+
*/
|
|
151
|
+
export interface TranslationsReport {
|
|
152
|
+
/**
|
|
153
|
+
* Report generation timestamp (ISO 8601)
|
|
154
|
+
*/
|
|
155
|
+
timestamp: string;
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Total number of unique static keys extracted
|
|
159
|
+
*/
|
|
160
|
+
totalKeys: number;
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Keys that couldn't be statically analyzed (dynamic keys)
|
|
164
|
+
*/
|
|
165
|
+
dynamicKeys: ExtractedKey[];
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* All extracted static keys
|
|
169
|
+
*/
|
|
170
|
+
extractedKeys: ExtractedKey[];
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Languages analyzed
|
|
174
|
+
*/
|
|
175
|
+
languages: string[];
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Missing translations per language
|
|
179
|
+
* { "es": [...], "fr": [...] }
|
|
180
|
+
*/
|
|
181
|
+
missing: Record<string, MissingTranslation[]>;
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Unused translations per language (keys in JSON but not in code)
|
|
185
|
+
* { "en": ["legacy.oldKey"], ... }
|
|
186
|
+
*/
|
|
187
|
+
unused: Record<string, string[]>;
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Summary statistics
|
|
191
|
+
*/
|
|
192
|
+
summary: {
|
|
193
|
+
/**
|
|
194
|
+
* Total missing translations across all languages
|
|
195
|
+
*/
|
|
196
|
+
totalMissing: number;
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Total unused translations across all languages
|
|
200
|
+
*/
|
|
201
|
+
totalUnused: number;
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Coverage percentage per language
|
|
205
|
+
*/
|
|
206
|
+
coveragePercent: Record<string, number>;
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Loaded translations structure
|
|
212
|
+
* { [language]: { [namespace]: { nested: { key: "value" } } } }
|
|
213
|
+
*/
|
|
214
|
+
export type LoadedTranslations = Record<string, Record<string, Record<string, unknown>>>;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Trans as I18nextTrans } from 'react-i18next';
|
|
3
|
+
import type { TransProps } from './types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Component for rendering translations with rich text and interpolation
|
|
7
|
+
*
|
|
8
|
+
* @example Basic usage
|
|
9
|
+
* ```tsx
|
|
10
|
+
* import { Trans } from '@idealyst/translate';
|
|
11
|
+
*
|
|
12
|
+
* // Translation: "Hello, {{name}}!"
|
|
13
|
+
* <Trans i18nKey="greeting" values={{ name: 'World' }} />
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* @example With components
|
|
17
|
+
* ```tsx
|
|
18
|
+
* // Translation: "Click <bold>here</bold> for <link>help</link>"
|
|
19
|
+
* <Trans
|
|
20
|
+
* i18nKey="helpText"
|
|
21
|
+
* components={{
|
|
22
|
+
* bold: <strong />,
|
|
23
|
+
* link: <a href="/help" />,
|
|
24
|
+
* }}
|
|
25
|
+
* />
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* @example With pluralization
|
|
29
|
+
* ```tsx
|
|
30
|
+
* // Translation: "{{count}} item" / "{{count}} items"
|
|
31
|
+
* <Trans i18nKey="itemCount" count={5} />
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* @example With children for interpolation
|
|
35
|
+
* ```tsx
|
|
36
|
+
* // Translation: "Read <0>our terms</0> and <1>privacy policy</1>"
|
|
37
|
+
* <Trans i18nKey="legal">
|
|
38
|
+
* <a href="/terms">our terms</a>
|
|
39
|
+
* <a href="/privacy">privacy policy</a>
|
|
40
|
+
* </Trans>
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export function Trans({
|
|
44
|
+
i18nKey,
|
|
45
|
+
ns,
|
|
46
|
+
defaults,
|
|
47
|
+
components,
|
|
48
|
+
values,
|
|
49
|
+
count,
|
|
50
|
+
context,
|
|
51
|
+
parent,
|
|
52
|
+
children,
|
|
53
|
+
shouldUnescape,
|
|
54
|
+
}: TransProps) {
|
|
55
|
+
return (
|
|
56
|
+
<I18nextTrans
|
|
57
|
+
i18nKey={i18nKey}
|
|
58
|
+
ns={ns}
|
|
59
|
+
defaults={defaults}
|
|
60
|
+
components={components}
|
|
61
|
+
values={values}
|
|
62
|
+
count={count}
|
|
63
|
+
context={context}
|
|
64
|
+
parent={parent}
|
|
65
|
+
shouldUnescape={shouldUnescape}
|
|
66
|
+
>
|
|
67
|
+
{children}
|
|
68
|
+
</I18nextTrans>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export default Trans;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { ReactElement, ReactNode } from 'react';
|
|
2
|
+
import type { Namespace } from 'i18next';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Props for the Trans component
|
|
6
|
+
*/
|
|
7
|
+
export interface TransProps {
|
|
8
|
+
/**
|
|
9
|
+
* Translation key
|
|
10
|
+
*/
|
|
11
|
+
i18nKey: string;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Namespace for the translation
|
|
15
|
+
*/
|
|
16
|
+
ns?: Namespace;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Default value if key is not found
|
|
20
|
+
*/
|
|
21
|
+
defaults?: string;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Component map for interpolation
|
|
25
|
+
* Keys correspond to tags in the translation string
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* Translation: "Click <bold>here</bold> for <link>help</link>"
|
|
29
|
+
* Components: { bold: <strong />, link: <a href="/help" /> }
|
|
30
|
+
*/
|
|
31
|
+
components?: Record<string, ReactElement>;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Values for interpolation
|
|
35
|
+
*/
|
|
36
|
+
values?: Record<string, unknown>;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Count for pluralization
|
|
40
|
+
*/
|
|
41
|
+
count?: number;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Context for contextual translations
|
|
45
|
+
*/
|
|
46
|
+
context?: string;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Tag name for the wrapper element
|
|
50
|
+
* @default React.Fragment
|
|
51
|
+
*/
|
|
52
|
+
parent?: string | null;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Children elements that can be used for interpolation
|
|
56
|
+
*/
|
|
57
|
+
children?: ReactNode;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Whether to unescape HTML entities
|
|
61
|
+
* @default false
|
|
62
|
+
*/
|
|
63
|
+
shouldUnescape?: boolean;
|
|
64
|
+
}
|