@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,211 @@
|
|
|
1
|
+
import { parseKey, hasNestedKey, getNestedValue, flattenKeys } from '../namespace';
|
|
2
|
+
|
|
3
|
+
describe('parseKey', () => {
|
|
4
|
+
it('parses namespace:key format', () => {
|
|
5
|
+
expect(parseKey('common:greeting')).toEqual({
|
|
6
|
+
namespace: 'common',
|
|
7
|
+
localKey: 'greeting',
|
|
8
|
+
});
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('parses namespace:nested.key format', () => {
|
|
12
|
+
expect(parseKey('common:buttons.submit')).toEqual({
|
|
13
|
+
namespace: 'common',
|
|
14
|
+
localKey: 'buttons.submit',
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('parses namespace.key format', () => {
|
|
19
|
+
expect(parseKey('common.greeting')).toEqual({
|
|
20
|
+
namespace: 'common',
|
|
21
|
+
localKey: 'greeting',
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('parses deeply nested namespace.a.b.c format', () => {
|
|
26
|
+
expect(parseKey('common.buttons.primary.label')).toEqual({
|
|
27
|
+
namespace: 'common',
|
|
28
|
+
localKey: 'buttons.primary.label',
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('uses default namespace for single key', () => {
|
|
33
|
+
expect(parseKey('greeting')).toEqual({
|
|
34
|
+
namespace: 'translation',
|
|
35
|
+
localKey: 'greeting',
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('uses custom default namespace', () => {
|
|
40
|
+
expect(parseKey('greeting', 'app')).toEqual({
|
|
41
|
+
namespace: 'app',
|
|
42
|
+
localKey: 'greeting',
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('handles multiple colons', () => {
|
|
47
|
+
expect(parseKey('common:time:12:30')).toEqual({
|
|
48
|
+
namespace: 'common',
|
|
49
|
+
localKey: 'time:12:30',
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('hasNestedKey', () => {
|
|
55
|
+
const obj = {
|
|
56
|
+
level1: {
|
|
57
|
+
level2: {
|
|
58
|
+
level3: 'value',
|
|
59
|
+
},
|
|
60
|
+
simple: 'test',
|
|
61
|
+
},
|
|
62
|
+
top: 'topValue',
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
it('returns true for existing top-level key', () => {
|
|
66
|
+
expect(hasNestedKey(obj, 'top')).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('returns true for existing nested key', () => {
|
|
70
|
+
expect(hasNestedKey(obj, 'level1.level2.level3')).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('returns true for intermediate key', () => {
|
|
74
|
+
expect(hasNestedKey(obj, 'level1.level2')).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('returns false for non-existent key', () => {
|
|
78
|
+
expect(hasNestedKey(obj, 'nonexistent')).toBe(false);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('returns false for non-existent nested key', () => {
|
|
82
|
+
expect(hasNestedKey(obj, 'level1.nonexistent')).toBe(false);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('returns false for path through non-object', () => {
|
|
86
|
+
expect(hasNestedKey(obj, 'top.child')).toBe(false);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('handles empty object', () => {
|
|
90
|
+
expect(hasNestedKey({}, 'any')).toBe(false);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('getNestedValue', () => {
|
|
95
|
+
const obj = {
|
|
96
|
+
greeting: 'Hello',
|
|
97
|
+
nested: {
|
|
98
|
+
deep: {
|
|
99
|
+
value: 'Found it',
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
array: [1, 2, 3],
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
it('gets top-level value', () => {
|
|
106
|
+
expect(getNestedValue(obj, 'greeting')).toBe('Hello');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('gets nested value', () => {
|
|
110
|
+
expect(getNestedValue(obj, 'nested.deep.value')).toBe('Found it');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('gets object value', () => {
|
|
114
|
+
expect(getNestedValue(obj, 'nested.deep')).toEqual({ value: 'Found it' });
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('returns undefined for non-existent key', () => {
|
|
118
|
+
expect(getNestedValue(obj, 'nonexistent')).toBeUndefined();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('returns undefined for non-existent nested key', () => {
|
|
122
|
+
expect(getNestedValue(obj, 'nested.nonexistent')).toBeUndefined();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('handles array values', () => {
|
|
126
|
+
expect(getNestedValue(obj, 'array')).toEqual([1, 2, 3]);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe('flattenKeys', () => {
|
|
131
|
+
it('flattens simple object', () => {
|
|
132
|
+
const obj = {
|
|
133
|
+
a: 'value1',
|
|
134
|
+
b: 'value2',
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
expect(flattenKeys(obj)).toEqual(['a', 'b']);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('flattens nested object', () => {
|
|
141
|
+
const obj = {
|
|
142
|
+
buttons: {
|
|
143
|
+
submit: 'Submit',
|
|
144
|
+
cancel: 'Cancel',
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
expect(flattenKeys(obj)).toEqual(['buttons.submit', 'buttons.cancel']);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('flattens deeply nested object', () => {
|
|
152
|
+
const obj = {
|
|
153
|
+
forms: {
|
|
154
|
+
validation: {
|
|
155
|
+
errors: {
|
|
156
|
+
required: 'Required',
|
|
157
|
+
email: 'Invalid email',
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
expect(flattenKeys(obj)).toEqual([
|
|
164
|
+
'forms.validation.errors.required',
|
|
165
|
+
'forms.validation.errors.email',
|
|
166
|
+
]);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('uses prefix when provided', () => {
|
|
170
|
+
const obj = {
|
|
171
|
+
submit: 'Submit',
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
expect(flattenKeys(obj, 'buttons')).toEqual(['buttons.submit']);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('handles mixed nesting levels', () => {
|
|
178
|
+
const obj = {
|
|
179
|
+
simple: 'value',
|
|
180
|
+
nested: {
|
|
181
|
+
deep: 'deepValue',
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
expect(flattenKeys(obj)).toEqual(['simple', 'nested.deep']);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('handles empty object', () => {
|
|
189
|
+
expect(flattenKeys({})).toEqual([]);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('ignores arrays (treats as leaf values)', () => {
|
|
193
|
+
const obj = {
|
|
194
|
+
items: ['a', 'b', 'c'],
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
expect(flattenKeys(obj)).toEqual(['items']);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('handles null values', () => {
|
|
201
|
+
const obj = {
|
|
202
|
+
nullValue: null,
|
|
203
|
+
valid: 'value',
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
expect(flattenKeys(obj as Record<string, unknown>)).toEqual([
|
|
207
|
+
'nullValue',
|
|
208
|
+
'valid',
|
|
209
|
+
]);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { parseKey, hasNestedKey, getNestedValue, flattenKeys } from './namespace';
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse a full translation key into namespace and local key parts
|
|
3
|
+
*
|
|
4
|
+
* @param fullKey - The full key (e.g., "common:buttons.submit" or "common.buttons.submit")
|
|
5
|
+
* @param defaultNamespace - Default namespace to use if not specified
|
|
6
|
+
* @returns Object with namespace and localKey
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* parseKey('common:buttons.submit') // { namespace: 'common', localKey: 'buttons.submit' }
|
|
10
|
+
* parseKey('buttons.submit', 'common') // { namespace: 'common', localKey: 'buttons.submit' }
|
|
11
|
+
* parseKey('common.buttons.submit') // { namespace: 'common', localKey: 'buttons.submit' }
|
|
12
|
+
*/
|
|
13
|
+
export function parseKey(
|
|
14
|
+
fullKey: string,
|
|
15
|
+
defaultNamespace: string = 'translation'
|
|
16
|
+
): { namespace: string; localKey: string } {
|
|
17
|
+
// Handle namespace:key format (explicit namespace separator)
|
|
18
|
+
if (fullKey.includes(':')) {
|
|
19
|
+
const [namespace, ...rest] = fullKey.split(':');
|
|
20
|
+
return { namespace, localKey: rest.join(':') };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Handle namespace.key format (first segment is namespace if multiple segments)
|
|
24
|
+
const segments = fullKey.split('.');
|
|
25
|
+
if (segments.length > 1) {
|
|
26
|
+
return { namespace: segments[0], localKey: segments.slice(1).join('.') };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Single key without namespace
|
|
30
|
+
return { namespace: defaultNamespace, localKey: fullKey };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Check if an object has a nested key
|
|
35
|
+
*
|
|
36
|
+
* @param obj - The object to check
|
|
37
|
+
* @param keyPath - Dot-separated key path
|
|
38
|
+
* @returns Whether the key exists
|
|
39
|
+
*/
|
|
40
|
+
export function hasNestedKey(obj: Record<string, unknown>, keyPath: string): boolean {
|
|
41
|
+
const parts = keyPath.split('.');
|
|
42
|
+
let current: unknown = obj;
|
|
43
|
+
|
|
44
|
+
for (const part of parts) {
|
|
45
|
+
if (current === undefined || current === null) return false;
|
|
46
|
+
if (typeof current !== 'object') return false;
|
|
47
|
+
current = (current as Record<string, unknown>)[part];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return current !== undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get a nested value from an object
|
|
55
|
+
*
|
|
56
|
+
* @param obj - The object to read from
|
|
57
|
+
* @param keyPath - Dot-separated key path
|
|
58
|
+
* @returns The value or undefined
|
|
59
|
+
*/
|
|
60
|
+
export function getNestedValue(
|
|
61
|
+
obj: Record<string, unknown>,
|
|
62
|
+
keyPath: string
|
|
63
|
+
): unknown {
|
|
64
|
+
const parts = keyPath.split('.');
|
|
65
|
+
let current: unknown = obj;
|
|
66
|
+
|
|
67
|
+
for (const part of parts) {
|
|
68
|
+
if (current === undefined || current === null) return undefined;
|
|
69
|
+
if (typeof current !== 'object') return undefined;
|
|
70
|
+
current = (current as Record<string, unknown>)[part];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return current;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Flatten a nested object into dot-notation keys
|
|
78
|
+
*
|
|
79
|
+
* @param obj - The object to flatten
|
|
80
|
+
* @param prefix - Key prefix
|
|
81
|
+
* @returns Array of flattened keys
|
|
82
|
+
*/
|
|
83
|
+
export function flattenKeys(obj: Record<string, unknown>, prefix: string = ''): string[] {
|
|
84
|
+
const keys: string[] = [];
|
|
85
|
+
|
|
86
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
87
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
88
|
+
|
|
89
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
90
|
+
keys.push(...flattenKeys(value as Record<string, unknown>, fullKey));
|
|
91
|
+
} else {
|
|
92
|
+
keys.push(fullKey);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return keys;
|
|
97
|
+
}
|