ai-localize-locale-engine 2.0.1 → 2.0.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/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # ai-localize-locale-engine
2
2
 
3
+ ## 2.0.3
4
+
5
+ ### Minor Changes
6
+
7
+ - **`staticKeys` injection** — `Extractor` and `Writer` now merge `staticKeys` entries from config into every generated locale file; in nested mode they are placed in the default namespace file; in flat mode they are merged at the top level
8
+ - **`keyStyle: "screaming_snake"` support** — `Extractor` honours the new `keyStyle` config field; when set to `"screaming_snake"` all scanned hardcoded text generates UPPER_SNAKE_CASE keys matching the text value
9
+
10
+ ### Patch Changes
11
+
12
+ - Updated dependencies
13
+ - ai-localize-shared@2.0.3
14
+ - ai-localize-config@2.0.3
15
+
16
+ ## 2.0.2
17
+
18
+ ### Minor Changes
19
+
20
+ - **Copy source value to target languages** — locale extract now initialises target language entries with the English source value instead of an empty string, so translators have immediate context
21
+ - **Flat locale file layout** — `localeStructure: "flat"` writes one `<lang>.json` per language with all namespace keys merged; non-default namespace keys are prefixed with `<namespace>.` to avoid collisions
22
+
23
+ ### Patch Changes
24
+
25
+ - Updated dependencies
26
+ - ai-localize-shared@2.0.2
27
+
3
28
  ## 2.0.1
4
29
 
5
30
  ### Patch Changes
package/dist/index.d.mts CHANGED
@@ -4,6 +4,26 @@ interface ExtractOptions {
4
4
  defaultLanguage?: string;
5
5
  targetLanguages?: string[];
6
6
  namespaceSplitting?: boolean;
7
+ /**
8
+ * Static key/value pairs to inject into every generated locale file,
9
+ * in addition to the keys discovered by scanning source files.
10
+ *
11
+ * These keys are placed in the default namespace (`common` / `translation`)
12
+ * so they end up in `locales/<lang>/translation.json` (nested mode) or at
13
+ * the top level of `locales/<lang>.json` (flat mode).
14
+ *
15
+ * Static keys that collide with scanned keys are **skipped** — scanned
16
+ * keys always take precedence so existing translations are never overwritten.
17
+ *
18
+ * Example:
19
+ * staticKeys: {
20
+ * MAX_COUNT: "Max Count",
21
+ * ALLOWED: "Allowed",
22
+ * DISABLED: "Disabled",
23
+ * UNLIMITED: "Unlimited",
24
+ * }
25
+ */
26
+ staticKeys?: Record<string, string>;
7
27
  }
8
28
  interface ExtractionResult {
9
29
  localeFiles: LocaleFile[];
package/dist/index.d.ts CHANGED
@@ -4,6 +4,26 @@ interface ExtractOptions {
4
4
  defaultLanguage?: string;
5
5
  targetLanguages?: string[];
6
6
  namespaceSplitting?: boolean;
7
+ /**
8
+ * Static key/value pairs to inject into every generated locale file,
9
+ * in addition to the keys discovered by scanning source files.
10
+ *
11
+ * These keys are placed in the default namespace (`common` / `translation`)
12
+ * so they end up in `locales/<lang>/translation.json` (nested mode) or at
13
+ * the top level of `locales/<lang>.json` (flat mode).
14
+ *
15
+ * Static keys that collide with scanned keys are **skipped** — scanned
16
+ * keys always take precedence so existing translations are never overwritten.
17
+ *
18
+ * Example:
19
+ * staticKeys: {
20
+ * MAX_COUNT: "Max Count",
21
+ * ALLOWED: "Allowed",
22
+ * DISABLED: "Disabled",
23
+ * UNLIMITED: "Unlimited",
24
+ * }
25
+ */
26
+ staticKeys?: Record<string, string>;
7
27
  }
8
28
  interface ExtractionResult {
9
29
  localeFiles: LocaleFile[];
package/dist/index.js CHANGED
@@ -47,7 +47,8 @@ var LocaleExtractor = class {
47
47
  this.options = {
48
48
  defaultLanguage: options.defaultLanguage || "en",
49
49
  targetLanguages: options.targetLanguages || [],
50
- namespaceSplitting: options.namespaceSplitting ?? true
50
+ namespaceSplitting: options.namespaceSplitting ?? true,
51
+ staticKeys: options.staticKeys ?? {}
51
52
  };
52
53
  }
53
54
  extract(detectedTexts) {
@@ -67,6 +68,18 @@ var LocaleExtractor = class {
67
68
  if (!namespaceMap.has(namespace)) namespaceMap.set(namespace, {});
68
69
  namespaceMap.get(namespace)[localKey] = value;
69
70
  }
71
+ const staticKeys = this.options.staticKeys;
72
+ if (staticKeys && Object.keys(staticKeys).length > 0) {
73
+ if (!namespaceMap.has(import_ai_localize_shared.DEFAULT_NAMESPACE)) {
74
+ namespaceMap.set(import_ai_localize_shared.DEFAULT_NAMESPACE, {});
75
+ }
76
+ const defaultNsBucket = namespaceMap.get(import_ai_localize_shared.DEFAULT_NAMESPACE);
77
+ for (const [key, value] of Object.entries(staticKeys)) {
78
+ if (!(key in defaultNsBucket)) {
79
+ defaultNsBucket[key] = value;
80
+ }
81
+ }
82
+ }
70
83
  for (const [ns, entries] of namespaceMap) {
71
84
  namespaceMap.set(
72
85
  ns,
package/dist/index.mjs CHANGED
@@ -10,7 +10,8 @@ var LocaleExtractor = class {
10
10
  this.options = {
11
11
  defaultLanguage: options.defaultLanguage || "en",
12
12
  targetLanguages: options.targetLanguages || [],
13
- namespaceSplitting: options.namespaceSplitting ?? true
13
+ namespaceSplitting: options.namespaceSplitting ?? true,
14
+ staticKeys: options.staticKeys ?? {}
14
15
  };
15
16
  }
16
17
  extract(detectedTexts) {
@@ -30,6 +31,18 @@ var LocaleExtractor = class {
30
31
  if (!namespaceMap.has(namespace)) namespaceMap.set(namespace, {});
31
32
  namespaceMap.get(namespace)[localKey] = value;
32
33
  }
34
+ const staticKeys = this.options.staticKeys;
35
+ if (staticKeys && Object.keys(staticKeys).length > 0) {
36
+ if (!namespaceMap.has(DEFAULT_NAMESPACE)) {
37
+ namespaceMap.set(DEFAULT_NAMESPACE, {});
38
+ }
39
+ const defaultNsBucket = namespaceMap.get(DEFAULT_NAMESPACE);
40
+ for (const [key, value] of Object.entries(staticKeys)) {
41
+ if (!(key in defaultNsBucket)) {
42
+ defaultNsBucket[key] = value;
43
+ }
44
+ }
45
+ }
33
46
  for (const [ns, entries] of namespaceMap) {
34
47
  namespaceMap.set(
35
48
  ns,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-localize-locale-engine",
3
- "version": "2.0.1",
3
+ "version": "2.0.3",
4
4
  "description": "Locale file generation, merging, deduplication and synchronization",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -13,7 +13,7 @@
13
13
  }
14
14
  },
15
15
  "dependencies": {
16
- "ai-localize-shared": "2.0.1"
16
+ "ai-localize-shared": "2.0.3"
17
17
  },
18
18
  "devDependencies": {
19
19
  "tsup": "^8.0.1",
package/src/extractor.ts CHANGED
@@ -9,6 +9,26 @@ export interface ExtractOptions {
9
9
  defaultLanguage?: string;
10
10
  targetLanguages?: string[];
11
11
  namespaceSplitting?: boolean;
12
+ /**
13
+ * Static key/value pairs to inject into every generated locale file,
14
+ * in addition to the keys discovered by scanning source files.
15
+ *
16
+ * These keys are placed in the default namespace (`common` / `translation`)
17
+ * so they end up in `locales/<lang>/translation.json` (nested mode) or at
18
+ * the top level of `locales/<lang>.json` (flat mode).
19
+ *
20
+ * Static keys that collide with scanned keys are **skipped** — scanned
21
+ * keys always take precedence so existing translations are never overwritten.
22
+ *
23
+ * Example:
24
+ * staticKeys: {
25
+ * MAX_COUNT: "Max Count",
26
+ * ALLOWED: "Allowed",
27
+ * DISABLED: "Disabled",
28
+ * UNLIMITED: "Unlimited",
29
+ * }
30
+ */
31
+ staticKeys?: Record<string, string>;
12
32
  }
13
33
 
14
34
  export interface ExtractionResult {
@@ -20,16 +40,17 @@ export interface ExtractionResult {
20
40
  export class LocaleExtractor {
21
41
  private options: Required<ExtractOptions>;
22
42
 
23
- constructor(options: ExtractOptions = {}) {
43
+ constructor(options: ExtractOptions = {}) {
24
44
  this.options = {
25
45
  defaultLanguage: options.defaultLanguage || "en",
26
46
  targetLanguages: options.targetLanguages || [],
27
47
  namespaceSplitting: options.namespaceSplitting ?? true,
48
+ staticKeys: options.staticKeys ?? {},
28
49
  };
29
50
  }
30
51
 
31
52
  extract(detectedTexts: DetectedText[]): ExtractionResult {
32
- const keyValueMap = new Map<string, string>();
53
+ const keyValueMap = new Map<string, string>();
33
54
  const existingKeys = new Set<string>();
34
55
 
35
56
  for (const dt of detectedTexts) {
@@ -45,30 +66,47 @@ export class LocaleExtractor {
45
66
  for (const [key, value] of keyValueMap) {
46
67
  const { namespace, localKey } = this.options.namespaceSplitting
47
68
  ? splitKeyNamespace(key)
48
- : { namespace: DEFAULT_NAMESPACE, localKey: key };
69
+ : { namespace: DEFAULT_NAMESPACE, localKey: key };
49
70
  if (!namespaceMap.has(namespace)) namespaceMap.set(namespace, {});
50
71
  namespaceMap.get(namespace)![localKey] = value;
51
72
  }
52
73
 
74
+ // Inject static keys into the default namespace.
75
+ // Scanned keys take precedence — static keys are only added when the key
76
+ // does not already exist in the default namespace bucket.
77
+ const staticKeys = this.options.staticKeys;
78
+ if (staticKeys && Object.keys(staticKeys).length > 0) {
79
+ if (!namespaceMap.has(DEFAULT_NAMESPACE)) {
80
+ namespaceMap.set(DEFAULT_NAMESPACE, {});
81
+ }
82
+ const defaultNsBucket = namespaceMap.get(DEFAULT_NAMESPACE)!;
83
+ for (const [key, value] of Object.entries(staticKeys)) {
84
+ if (!(key in defaultNsBucket)) {
85
+ defaultNsBucket[key] = value;
86
+ }
87
+ }
88
+ }
89
+
53
90
  for (const [ns, entries] of namespaceMap) {
54
- namespaceMap.set(
55
- ns,
56
- Object.fromEntries(Object.entries(entries).sort(([a], [b]) => a.localeCompare(b)))
57
- );
91
+ namespaceMap.set(
92
+ ns,
93
+ Object.fromEntries(Object.entries(entries).sort(([a], [b]) => a.localeCompare(b)))
94
+ );
58
95
  }
59
96
 
60
97
  const localeFiles: LocaleFile[] = [];
61
98
  const allLanguages = [this.options.defaultLanguage, ...this.options.targetLanguages];
62
99
 
63
100
  for (const lang of allLanguages) {
64
- for (const [namespace, entries] of namespaceMap) {
65
- // All languages get the source (English) value as the initial translation.
66
- // This makes locale files immediately usable without blank values, and human
67
- // translators can identify untranslated strings by comparing them to the
68
- // default language file.
69
- const langEntries = { ...entries };
101
+ for (const [namespace, entries] of namespaceMap) {
102
+ // Default language gets the source text values.
103
+ // Target languages get empty string values so translators can fill them in.
104
+ const isDefault = lang === this.options.defaultLanguage;
105
+ const langEntries = isDefault
106
+ ? { ...entries }
107
+ : Object.fromEntries(Object.keys(entries).map((k) => [k, '']));
70
108
  localeFiles.push({ language: lang, namespace, entries: langEntries, filePath: "" });
71
- }
109
+ }
72
110
  }
73
111
 
74
112
  return { localeFiles, keyCount: keyValueMap.size, namespaces: Array.from(namespaceMap.keys()) };