@scoutello/i18n-magic 0.18.0 → 0.20.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/src/lib/utils.ts CHANGED
@@ -5,7 +5,7 @@ import path from "node:path"
5
5
  import type OpenAI from "openai"
6
6
  import prompts from "prompts"
7
7
  import { languages } from "./languges"
8
- import type { Configuration } from "./types"
8
+ import type { Configuration, GlobPatternConfig } from "./types"
9
9
 
10
10
  export const loadConfig = ({
11
11
  configPath = "i18n-magic.js",
@@ -180,33 +180,145 @@ export const getPureKey = (
180
180
  return null
181
181
  }
182
182
 
183
- export const getMissingKeys = async ({
183
+ /**
184
+ * Extracts all glob patterns from the configuration, handling both string and object formats
185
+ */
186
+ export const extractGlobPatterns = (
187
+ globPatterns: (string | GlobPatternConfig)[],
188
+ ): string[] => {
189
+ return globPatterns.map((pattern) =>
190
+ typeof pattern === "string" ? pattern : pattern.pattern,
191
+ )
192
+ }
193
+
194
+ /**
195
+ * Gets the namespaces associated with a specific file path based on glob pattern configuration
196
+ */
197
+ export const getNamespacesForFile = (
198
+ filePath: string,
199
+ globPatterns: (string | { pattern: string; namespaces: string[] })[],
200
+ defaultNamespace: string,
201
+ ): string[] => {
202
+ const matchingNamespaces: string[] = []
203
+
204
+ for (const pattern of globPatterns) {
205
+ if (typeof pattern === "object") {
206
+ // Use minimatch or similar logic to check if file matches pattern
207
+ const globPattern = pattern.pattern
208
+ // For now, using a simple includes check - in production you'd want proper glob matching
209
+ if (filePath.includes(globPattern.replace("**/*", "").replace("*", ""))) {
210
+ matchingNamespaces.push(...pattern.namespaces)
211
+ }
212
+ }
213
+ }
214
+
215
+ // If no specific namespaces found, use default namespace
216
+ return matchingNamespaces.length > 0
217
+ ? [...new Set(matchingNamespaces)]
218
+ : [defaultNamespace]
219
+ }
220
+
221
+ /**
222
+ * Gets all glob patterns that should be used for a specific namespace
223
+ */
224
+ export const getGlobPatternsForNamespace = (
225
+ namespace: string,
226
+ globPatterns: (string | { pattern: string; namespaces: string[] })[],
227
+ ): string[] => {
228
+ const patterns: string[] = []
229
+
230
+ for (const pattern of globPatterns) {
231
+ if (typeof pattern === "string") {
232
+ // String patterns apply to all namespaces
233
+ patterns.push(pattern)
234
+ } else if (pattern.namespaces.includes(namespace)) {
235
+ // Object patterns only apply to specified namespaces
236
+ patterns.push(pattern.pattern)
237
+ }
238
+ }
239
+
240
+ return patterns
241
+ }
242
+
243
+ /**
244
+ * Extracts keys with their associated namespaces based on the files they're found in
245
+ */
246
+ export const getKeysWithNamespaces = async ({
184
247
  globPatterns,
185
- namespaces,
186
248
  defaultNamespace,
187
- defaultLocale,
188
- loadPath,
189
- }: Configuration) => {
249
+ }: Pick<Configuration, "globPatterns" | "defaultNamespace">) => {
190
250
  const parser = new Parser({
191
251
  nsSeparator: false,
192
252
  keySeparator: false,
193
253
  })
194
254
 
195
- const files = await glob([...globPatterns, "!**/node_modules/**"])
255
+ const allPatterns = extractGlobPatterns(globPatterns)
256
+ const files = await glob([...allPatterns, "!**/node_modules/**"])
196
257
 
197
- const keys = []
258
+ const keysWithNamespaces: Array<{
259
+ key: string
260
+ namespaces: string[]
261
+ file: string
262
+ }> = []
198
263
 
199
264
  for (const file of files) {
200
265
  const content = fs.readFileSync(file, "utf-8")
266
+ const fileKeys: string[] = []
267
+
201
268
  parser.parseFuncFromString(content, { list: ["t"] }, (key: string) => {
202
- keys.push(key)
269
+ fileKeys.push(key)
203
270
  })
271
+
272
+ // Get namespaces for this file
273
+ const fileNamespaces = getNamespacesForFile(
274
+ file,
275
+ globPatterns,
276
+ defaultNamespace,
277
+ )
278
+
279
+ // Add each key with its associated namespaces
280
+ for (const key of fileKeys) {
281
+ keysWithNamespaces.push({
282
+ key,
283
+ namespaces: fileNamespaces,
284
+ file,
285
+ })
286
+ }
204
287
  }
205
288
 
206
- const uniqueKeys = removeDuplicatesFromArray(keys)
289
+ return keysWithNamespaces
290
+ }
207
291
 
292
+ export const getMissingKeys = async ({
293
+ globPatterns,
294
+ namespaces,
295
+ defaultNamespace,
296
+ defaultLocale,
297
+ loadPath,
298
+ }: Configuration) => {
299
+ const keysWithNamespaces = await getKeysWithNamespaces({
300
+ globPatterns,
301
+ defaultNamespace,
302
+ })
208
303
  const newKeys = []
209
304
 
305
+ // Group keys by namespace
306
+ const keysByNamespace: Record<string, Set<string>> = {}
307
+
308
+ for (const { key, namespaces: keyNamespaces } of keysWithNamespaces) {
309
+ for (const namespace of keyNamespaces) {
310
+ if (!keysByNamespace[namespace]) {
311
+ keysByNamespace[namespace] = new Set()
312
+ }
313
+
314
+ const pureKey = getPureKey(key, namespace, namespace === defaultNamespace)
315
+ if (pureKey) {
316
+ keysByNamespace[namespace].add(pureKey)
317
+ }
318
+ }
319
+ }
320
+
321
+ // Check for missing keys in each namespace
210
322
  for (const namespace of namespaces) {
211
323
  const existingKeys = await loadLocalesFile(
212
324
  loadPath,
@@ -214,17 +326,13 @@ export const getMissingKeys = async ({
214
326
  namespace,
215
327
  )
216
328
 
217
- console.log(Object.keys(existingKeys).length, "existing keys")
329
+ console.log(Object.keys(existingKeys).length, "existing keys in", namespace)
218
330
 
219
- for (const key of uniqueKeys) {
220
- const pureKey = getPureKey(key, namespace, namespace === defaultNamespace)
221
-
222
- if (!pureKey) {
223
- continue
224
- }
331
+ const keysForNamespace = keysByNamespace[namespace] || new Set()
225
332
 
226
- if (!existingKeys[pureKey]) {
227
- newKeys.push({ key: pureKey, namespace })
333
+ for (const key of keysForNamespace) {
334
+ if (!existingKeys[key]) {
335
+ newKeys.push({ key, namespace })
228
336
  }
229
337
  }
230
338
  }
@@ -1,165 +0,0 @@
1
- import glob from "fast-glob"
2
- import { Parser } from "i18next-scanner"
3
- import fs from "node:fs"
4
- import type { Configuration } from "../lib/types"
5
- import {
6
- getPureKey,
7
- loadLocalesFile,
8
- removeDuplicatesFromArray,
9
- writeLocalesFile,
10
- } from "../lib/utils"
11
-
12
- export interface PruneOptions {
13
- sourceNamespace: string
14
- newNamespace: string
15
- globPatterns: string[]
16
- }
17
-
18
- export interface PruneResult {
19
- locale: string
20
- keyCount: number
21
- success: boolean
22
- error?: string
23
- }
24
-
25
- export interface PruneResponse {
26
- success: boolean
27
- message: string
28
- keysCount: number
29
- results?: PruneResult[]
30
- }
31
-
32
- export const createPrunedNamespaceAutomated = async (
33
- config: Configuration,
34
- options: PruneOptions,
35
- ): Promise<PruneResponse> => {
36
- const { namespaces, loadPath, savePath, locales, defaultNamespace } = config
37
- const { sourceNamespace, newNamespace, globPatterns } = options
38
-
39
- // Validate inputs
40
- if (!namespaces.includes(sourceNamespace)) {
41
- throw new Error(
42
- `Source namespace '${sourceNamespace}' not found in configuration`,
43
- )
44
- }
45
-
46
- if (namespaces.includes(newNamespace)) {
47
- throw new Error(`Namespace '${newNamespace}' already exists`)
48
- }
49
-
50
- console.log(
51
- `Creating pruned namespace '${newNamespace}' from '${sourceNamespace}'`,
52
- )
53
- console.log(`Using glob patterns: ${globPatterns.join(", ")}`)
54
-
55
- // Extract keys from files matching the glob patterns
56
- const parser = new Parser({
57
- nsSeparator: false,
58
- keySeparator: false,
59
- })
60
-
61
- const files = await glob([...globPatterns, "!**/node_modules/**"])
62
- console.log(`Found ${files.length} files to scan`)
63
-
64
- const extractedKeys = []
65
-
66
- for (const file of files) {
67
- const content = fs.readFileSync(file, "utf-8")
68
- parser.parseFuncFromString(content, { list: ["t"] }, (key: string) => {
69
- extractedKeys.push(key)
70
- })
71
- }
72
-
73
- const uniqueExtractedKeys = removeDuplicatesFromArray(extractedKeys)
74
- console.log(`Found ${uniqueExtractedKeys.length} unique translation keys`)
75
-
76
- // Filter keys that belong to the source namespace
77
- const relevantKeys = []
78
-
79
- for (const key of uniqueExtractedKeys) {
80
- const pureKey = getPureKey(
81
- key,
82
- sourceNamespace,
83
- sourceNamespace === defaultNamespace,
84
- )
85
-
86
- if (pureKey) {
87
- relevantKeys.push(pureKey)
88
- }
89
- }
90
-
91
- console.log(
92
- `Found ${relevantKeys.length} keys from namespace '${sourceNamespace}'`,
93
- )
94
-
95
- if (relevantKeys.length === 0) {
96
- console.log("No relevant keys found. Exiting...")
97
- return {
98
- success: false,
99
- message: "No relevant keys found",
100
- keysCount: 0,
101
- }
102
- }
103
-
104
- // Get translations from source namespace and create new namespace files
105
- const results: PruneResult[] = []
106
-
107
- for (const locale of locales) {
108
- try {
109
- // Load source namespace translations
110
- const sourceTranslations = await loadLocalesFile(
111
- loadPath,
112
- locale,
113
- sourceNamespace,
114
- )
115
-
116
- // Create new namespace with only the keys used in the glob pattern files
117
- const newNamespaceTranslations: Record<string, string> = {}
118
-
119
- for (const key of relevantKeys) {
120
- if (sourceTranslations[key]) {
121
- newNamespaceTranslations[key] = sourceTranslations[key]
122
- }
123
- }
124
-
125
- // Write the new namespace file
126
- await writeLocalesFile(
127
- savePath,
128
- locale,
129
- newNamespace,
130
- newNamespaceTranslations,
131
- )
132
-
133
- const keyCount = Object.keys(newNamespaceTranslations).length
134
- console.log(
135
- `Created pruned namespace '${newNamespace}' for locale '${locale}' with ${keyCount} keys`,
136
- )
137
-
138
- results.push({
139
- locale,
140
- keyCount,
141
- success: true,
142
- })
143
- } catch (error) {
144
- console.error(
145
- `Error creating pruned namespace for locale '${locale}':`,
146
- error,
147
- )
148
- results.push({
149
- locale,
150
- keyCount: 0,
151
- success: false,
152
- error: error instanceof Error ? error.message : String(error),
153
- })
154
- }
155
- }
156
-
157
- console.log(`✅ Successfully created pruned namespace '${newNamespace}'`)
158
-
159
- return {
160
- success: true,
161
- message: `Created pruned namespace '${newNamespace}' with ${relevantKeys.length} keys`,
162
- keysCount: relevantKeys.length,
163
- results,
164
- }
165
- }
@@ -1,165 +0,0 @@
1
- import glob from "fast-glob"
2
- import { Parser } from "i18next-scanner"
3
- import fs from "node:fs"
4
- import prompts from "prompts"
5
- import type { Configuration } from "../lib/types"
6
- import {
7
- getPureKey,
8
- loadLocalesFile,
9
- removeDuplicatesFromArray,
10
- writeLocalesFile,
11
- } from "../lib/utils"
12
-
13
- export const createPrunedNamespace = async (config: Configuration) => {
14
- const { namespaces, loadPath, savePath, locales, defaultNamespace } = config
15
-
16
- // Step 1: Ask for source namespace
17
- const sourceNamespaceResponse = await prompts({
18
- type: "select",
19
- name: "value",
20
- message: "Select source namespace to create pruned version from:",
21
- choices: namespaces.map((namespace) => ({
22
- title: namespace,
23
- value: namespace,
24
- })),
25
- onState: (state) => {
26
- if (state.aborted) {
27
- process.nextTick(() => {
28
- process.exit(0)
29
- })
30
- }
31
- },
32
- })
33
-
34
- const sourceNamespace = sourceNamespaceResponse.value
35
-
36
- // Step 2: Ask for new namespace name
37
- const newNamespaceResponse = await prompts({
38
- type: "text",
39
- name: "value",
40
- message: "Enter the name for the new namespace:",
41
- validate: (value) => {
42
- if (!value) return "Namespace name cannot be empty"
43
- if (namespaces.includes(value)) return "Namespace already exists"
44
- return true
45
- },
46
- onState: (state) => {
47
- if (state.aborted) {
48
- process.nextTick(() => {
49
- process.exit(0)
50
- })
51
- }
52
- },
53
- })
54
-
55
- const newNamespace = newNamespaceResponse.value
56
-
57
- // Step 3: Ask for glob patterns to find relevant keys
58
- const globPatternsResponse = await prompts({
59
- type: "list",
60
- name: "value",
61
- message: "Enter glob patterns to find relevant keys (comma separated):",
62
- initial: config.globPatterns.join(","),
63
- separator: ",",
64
- onState: (state) => {
65
- if (state.aborted) {
66
- process.nextTick(() => {
67
- process.exit(0)
68
- })
69
- }
70
- },
71
- })
72
-
73
- const selectedGlobPatterns = globPatternsResponse.value
74
-
75
- console.log(
76
- `Finding keys used in files matching: ${selectedGlobPatterns.join(", ")}`,
77
- )
78
-
79
- // Extract keys from files matching the glob patterns
80
- const parser = new Parser({
81
- nsSeparator: false,
82
- keySeparator: false,
83
- })
84
-
85
- const files = await glob([...selectedGlobPatterns, "!**/node_modules/**"])
86
- console.log(`Found ${files.length} files to scan`)
87
-
88
- const extractedKeys = []
89
-
90
- for (const file of files) {
91
- const content = fs.readFileSync(file, "utf-8")
92
- parser.parseFuncFromString(content, { list: ["t"] }, (key: string) => {
93
- extractedKeys.push(key)
94
- })
95
- }
96
-
97
- const uniqueExtractedKeys = removeDuplicatesFromArray(extractedKeys)
98
- console.log(`Found ${uniqueExtractedKeys.length} unique translation keys`)
99
-
100
- // Filter keys that belong to the source namespace
101
- const relevantKeys = []
102
-
103
- for (const key of uniqueExtractedKeys) {
104
- const pureKey = getPureKey(
105
- key,
106
- sourceNamespace,
107
- sourceNamespace === defaultNamespace,
108
- )
109
-
110
- if (pureKey) {
111
- relevantKeys.push(pureKey)
112
- }
113
- }
114
-
115
- console.log(
116
- `Found ${relevantKeys.length} keys from namespace '${sourceNamespace}'`,
117
- )
118
-
119
- if (relevantKeys.length === 0) {
120
- console.log("No relevant keys found. Exiting...")
121
- return
122
- }
123
-
124
- // Get translations from source namespace and create new namespace files
125
- for (const locale of locales) {
126
- try {
127
- // Load source namespace translations
128
- const sourceTranslations = await loadLocalesFile(
129
- loadPath,
130
- locale,
131
- sourceNamespace,
132
- )
133
-
134
- // Create new namespace with only the keys used in the glob pattern files
135
- const newNamespaceTranslations: Record<string, string> = {}
136
-
137
- for (const key of relevantKeys) {
138
- if (sourceTranslations[key]) {
139
- newNamespaceTranslations[key] = sourceTranslations[key]
140
- }
141
- }
142
-
143
- // Write the new namespace file
144
- await writeLocalesFile(
145
- savePath,
146
- locale,
147
- newNamespace,
148
- newNamespaceTranslations,
149
- )
150
-
151
- console.log(
152
- `Created pruned namespace '${newNamespace}' for locale '${locale}' with ${
153
- Object.keys(newNamespaceTranslations).length
154
- } keys`,
155
- )
156
- } catch (error) {
157
- console.error(
158
- `Error creating pruned namespace for locale '${locale}':`,
159
- error,
160
- )
161
- }
162
- }
163
-
164
- console.log(`✅ Successfully created pruned namespace '${newNamespace}'`)
165
- }