@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.
@@ -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
+ }