@lingual/i18n-check 0.7.4 → 0.8.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.
@@ -29,9 +29,14 @@ const sortParsedKeys = (a, b) => {
29
29
  const compareTranslationFiles = (a, b) => {
30
30
  let diffs = [];
31
31
  for (const key in a) {
32
- if (b[key] !== undefined &&
33
- (0, exports.hasDiff)((0, icu_messageformat_parser_1.parse)(String(a[key])), (0, icu_messageformat_parser_1.parse)(String(b[key])))) {
34
- diffs.push(key);
32
+ if (b[key] === undefined) {
33
+ continue;
34
+ }
35
+ const parsedTranslationA = (0, icu_messageformat_parser_1.parse)(String(a[key]));
36
+ const parsedTranslationB = (0, icu_messageformat_parser_1.parse)(String(b[key]));
37
+ if ((0, exports.hasDiff)(parsedTranslationA, parsedTranslationB)) {
38
+ const msg = getErrorMessage(parsedTranslationA, parsedTranslationB);
39
+ diffs.push({ key, msg });
35
40
  }
36
41
  }
37
42
  return diffs;
@@ -64,7 +69,7 @@ const hasDiff = (a, b) => {
64
69
  if ((0, icu_messageformat_parser_1.isTagElement)(formatElementA) && (0, icu_messageformat_parser_1.isTagElement)(formatElementB)) {
65
70
  return (0, exports.hasDiff)(formatElementA.children, formatElementB.children);
66
71
  }
67
- if (((0, icu_messageformat_parser_1.isSelectElement)(formatElementA) && (0, icu_messageformat_parser_1.isSelectElement)(formatElementB))) {
72
+ if ((0, icu_messageformat_parser_1.isSelectElement)(formatElementA) && (0, icu_messageformat_parser_1.isSelectElement)(formatElementB)) {
68
73
  const optionsA = Object.keys(formatElementA.options).sort();
69
74
  const optionsB = Object.keys(formatElementB.options).sort();
70
75
  if (optionsA.join("-") !== optionsB.join("-")) {
@@ -92,3 +97,88 @@ const hasDiff = (a, b) => {
92
97
  return hasErrors;
93
98
  };
94
99
  exports.hasDiff = hasDiff;
100
+ const getErrorMessage = (a, b) => {
101
+ const compA = a
102
+ .filter((element) => !(0, icu_messageformat_parser_1.isLiteralElement)(element))
103
+ .sort(sortParsedKeys);
104
+ const compB = b
105
+ .filter((element) => !(0, icu_messageformat_parser_1.isLiteralElement)(element))
106
+ .sort(sortParsedKeys);
107
+ const errors = compA.reduce((acc, formatElementA, index) => {
108
+ const formatElementB = compB[index];
109
+ if (!formatElementB) {
110
+ acc.push(`Missing element ${typeLookup[formatElementA.type]}`);
111
+ return acc;
112
+ }
113
+ if (formatElementA.type !== formatElementB.type) {
114
+ acc.push(`Expected element of type "${typeLookup[formatElementA.type]}" but received "${typeLookup[formatElementB.type]}"`);
115
+ return acc;
116
+ }
117
+ if (formatElementA.location !== formatElementB.location) {
118
+ acc.push(`Expected location to be ${formatElementA.location?.start?.line}:${formatElementA.location?.start?.column}`);
119
+ return acc;
120
+ }
121
+ if ((0, icu_messageformat_parser_1.isPoundElement)(formatElementA) && (0, icu_messageformat_parser_1.isPoundElement)(formatElementB)) {
122
+ return acc;
123
+ }
124
+ if (!(0, icu_messageformat_parser_1.isPoundElement)(formatElementA) &&
125
+ !(0, icu_messageformat_parser_1.isPoundElement)(formatElementB) &&
126
+ formatElementA.value !== formatElementB.value) {
127
+ acc.push(`Expected ${typeLookup[formatElementA.type]} to contain "${formatElementA.value}" but received "${formatElementB.value}"`);
128
+ return acc;
129
+ }
130
+ if ((0, icu_messageformat_parser_1.isTagElement)(formatElementA) && (0, icu_messageformat_parser_1.isTagElement)(formatElementB)) {
131
+ acc.push(`Error in pound element: ${getErrorMessage(formatElementA.children, formatElementB.children)}`);
132
+ return acc;
133
+ }
134
+ if ((0, icu_messageformat_parser_1.isSelectElement)(formatElementA) && (0, icu_messageformat_parser_1.isSelectElement)(formatElementB)) {
135
+ const optionsA = Object.keys(formatElementA.options).sort();
136
+ let elementErrors = [];
137
+ optionsA.forEach((key) => {
138
+ if (formatElementB.options[key]) {
139
+ elementErrors.push(getErrorMessage(formatElementA.options[key].value, formatElementB.options[key].value));
140
+ }
141
+ });
142
+ acc.push(`Error in select: ${elementErrors
143
+ .flatMap((elementError) => elementError)
144
+ .join(", ")}`);
145
+ return acc;
146
+ }
147
+ if ((0, icu_messageformat_parser_1.isPluralElement)(formatElementA) && (0, icu_messageformat_parser_1.isPluralElement)(formatElementB)) {
148
+ const optionsA = Object.keys(formatElementA.options).sort();
149
+ let elementErrors = [];
150
+ optionsA.forEach((key) => {
151
+ if (formatElementB.options[key]) {
152
+ elementErrors.push(getErrorMessage(formatElementA.options[key].value, formatElementB.options[key].value));
153
+ }
154
+ });
155
+ acc.push(`Error in plural: ${elementErrors
156
+ .flatMap((elementError) => elementError)
157
+ .join(", ")}`);
158
+ return acc;
159
+ }
160
+ return acc;
161
+ }, []);
162
+ if (compA.length < compB.length) {
163
+ const unexpectedElements = compB
164
+ .slice(compA.length)
165
+ .reduce((acc, formatElementB) => {
166
+ acc.push(`Unexpected ${typeLookup[formatElementB.type]} element`);
167
+ return acc;
168
+ }, [])
169
+ .join(", ");
170
+ return [...errors, unexpectedElements].join(", ");
171
+ }
172
+ return errors.join(", ");
173
+ };
174
+ const typeLookup = {
175
+ 0: "literal",
176
+ 1: "argument",
177
+ 2: "number",
178
+ 3: "date",
179
+ 4: "time",
180
+ 5: "select",
181
+ 6: "plural",
182
+ 7: "pound",
183
+ 8: "tag",
184
+ };
@@ -12,7 +12,7 @@ describe("findInvalidTranslations:compareTranslationFiles", () => {
12
12
  expect((0, findInvalidTranslations_1.compareTranslationFiles)((0, flattenTranslations_1.flattenTranslations)({
13
13
  ...sourceFile,
14
14
  "ten.eleven.twelve": "ten eleven twelve",
15
- }), (0, flattenTranslations_1.flattenTranslations)(secondaryFile))).toEqual(["multipleVariables"]);
15
+ }), (0, flattenTranslations_1.flattenTranslations)(secondaryFile))).toEqual([{ key: "multipleVariables", msg: "Unexpected date element" }]);
16
16
  });
17
17
  it("should return empty array if placeholders are identical but in different positions", () => {
18
18
  expect((0, findInvalidTranslations_1.compareTranslationFiles)({
@@ -27,7 +27,9 @@ describe("findInvalidTranslations", () => {
27
27
  expect((0, findInvalidTranslations_1.findInvalidTranslations)(sourceFile, { de: sourceFile })).toEqual({});
28
28
  });
29
29
  it("should return an object containing the keys for the missing language", () => {
30
- expect((0, findInvalidTranslations_1.findInvalidTranslations)({ ...sourceFile, "ten.eleven.twelve": "ten eleven twelve" }, { de: secondaryFile })).toEqual({ de: ["multipleVariables"] });
30
+ expect((0, findInvalidTranslations_1.findInvalidTranslations)({ ...sourceFile, "ten.eleven.twelve": "ten eleven twelve" }, { de: secondaryFile })).toEqual({
31
+ de: [{ key: "multipleVariables", msg: "Unexpected date element" }],
32
+ });
31
33
  });
32
34
  it("should return an object containing the keys for every language with missing key", () => {
33
35
  expect((0, findInvalidTranslations_1.findInvalidTranslations)({ ...sourceFile, "ten.eleven.twelve": "ten eleven twelve" }, {
@@ -38,8 +40,13 @@ describe("findInvalidTranslations", () => {
38
40
  "message.text-format": "yo,<p><b>John</b></p>!",
39
41
  },
40
42
  })).toEqual({
41
- de: ["multipleVariables"],
42
- fr: ["message.text-format"],
43
+ de: [{ key: "multipleVariables", msg: "Unexpected date element" }],
44
+ fr: [
45
+ {
46
+ key: "message.text-format",
47
+ msg: 'Expected tag to contain "b" but received "p"',
48
+ },
49
+ ],
43
50
  });
44
51
  });
45
52
  it("should allow for different types of keys per locale", () => {
@@ -47,9 +54,14 @@ describe("findInvalidTranslations", () => {
47
54
  de: {
48
55
  ...secondaryFile,
49
56
  "message.plural": "{count, plural, other {# of {total} items}}",
50
- }
57
+ },
51
58
  })).toEqual({
52
- de: ["multipleVariables"]
59
+ de: [
60
+ {
61
+ key: "multipleVariables",
62
+ msg: "Unexpected date element",
63
+ },
64
+ ],
53
65
  });
54
66
  });
55
67
  it("should fail if a variable is changed in one of the translations", () => {
@@ -57,9 +69,15 @@ describe("findInvalidTranslations", () => {
57
69
  de: {
58
70
  ...secondaryFile,
59
71
  "message.plural": "{count, plural, other {# of {cargado} items}}",
60
- }
72
+ },
61
73
  })).toEqual({
62
- de: ["message.plural", "multipleVariables"]
74
+ de: [
75
+ {
76
+ key: "message.plural",
77
+ msg: 'Error in plural: Expected argument to contain "total" but received "cargado"',
78
+ },
79
+ { key: "multipleVariables", msg: "Unexpected date element" },
80
+ ],
63
81
  });
64
82
  });
65
83
  });
@@ -25,9 +25,14 @@ exports.findInvalid18nTranslations = findInvalid18nTranslations;
25
25
  const compareTranslationFiles = (a, b) => {
26
26
  let diffs = [];
27
27
  for (const key in a) {
28
- if (b[key] !== undefined &&
29
- (0, exports.hasDiff)((0, i18NextParser_1.parse)(String(a[key])), (0, i18NextParser_1.parse)(String(b[key])))) {
30
- diffs.push(key);
28
+ if (b[key] === undefined) {
29
+ continue;
30
+ }
31
+ const parsedTranslationA = (0, i18NextParser_1.parse)(String(a[key]));
32
+ const parsedTranslationB = (0, i18NextParser_1.parse)(String(b[key]));
33
+ if ((0, exports.hasDiff)(parsedTranslationA, parsedTranslationB)) {
34
+ const msg = getErrorMessage(parsedTranslationA, parsedTranslationB);
35
+ diffs.push({ key, msg });
31
36
  }
32
37
  }
33
38
  return diffs;
@@ -103,3 +108,85 @@ const hasDiff = (a, b) => {
103
108
  return hasErrors;
104
109
  };
105
110
  exports.hasDiff = hasDiff;
111
+ const getErrorMessage = (a, b) => {
112
+ const compA = a
113
+ .filter((element) => element.type !== "text")
114
+ .sort(sortParsedKeys);
115
+ const compB = b
116
+ .filter((element) => element.type !== "text")
117
+ .sort(sortParsedKeys);
118
+ const errors = compA.reduce((acc, formatElementA, index) => {
119
+ const formatElementB = compB[index];
120
+ if (!formatElementB) {
121
+ acc.push(`Missing element ${formatElementA.type}`);
122
+ return acc;
123
+ }
124
+ if (formatElementA.type !== formatElementB.type) {
125
+ acc.push(`Expected element of type "${formatElementA.type}" but received "${formatElementB.type}"`);
126
+ return acc;
127
+ }
128
+ if (formatElementA.type === "tag" && formatElementB.type === "tag") {
129
+ if (formatElementA.raw !== formatElementB.raw) {
130
+ acc.push(`Expected tag "${formatElementA.raw}" but received "${formatElementB.raw}"`);
131
+ }
132
+ else if (formatElementA.voidElement !== formatElementB.voidElement &&
133
+ formatElementA.voidElement === true) {
134
+ acc.push(`Expected a self-closing "${formatElementB.raw}" tag`);
135
+ return acc;
136
+ }
137
+ else if (formatElementA.voidElement !== formatElementB.voidElement &&
138
+ formatElementA.voidElement === false) {
139
+ acc.push(`Non expected self-closing "${formatElementB.raw}" tag`);
140
+ return acc;
141
+ }
142
+ }
143
+ if ((formatElementA.type === "interpolation" &&
144
+ formatElementB.type === "interpolation") ||
145
+ (formatElementA.type === "interpolation_unescaped" &&
146
+ formatElementB.type === "interpolation_unescaped") ||
147
+ (formatElementA.type === "nesting" &&
148
+ formatElementB.type === "nesting") ||
149
+ (formatElementA.type === "plural" && formatElementB.type === "plural")) {
150
+ if (formatElementA.prefix !== formatElementA.prefix) {
151
+ acc.push(`Error in ${formatElementA.type}: Expected prefix "${formatElementA.prefix}" but received "${formatElementB.prefix}"`);
152
+ return acc;
153
+ }
154
+ if (formatElementA.suffix !== formatElementA.suffix) {
155
+ acc.push(`Error in ${formatElementA.type}: Expected suffix "${formatElementA.suffix}" but received "${formatElementB.suffix}"`);
156
+ return acc;
157
+ }
158
+ const optionsA = formatElementA.variable
159
+ .split(",")
160
+ .map((value) => value.trim())
161
+ .sort();
162
+ const optionsB = formatElementB.variable
163
+ .split(",")
164
+ .map((value) => value.trim())
165
+ .sort();
166
+ let elementErrors = [];
167
+ optionsA.forEach((key, index) => {
168
+ if (key !== optionsB[index]) {
169
+ elementErrors.push(`Expected ${key} but received ${optionsB[index]}`);
170
+ }
171
+ });
172
+ if (elementErrors.length > 0) {
173
+ acc.push(`Error in ${formatElementA.type}: ${elementErrors
174
+ .flatMap((elementError) => elementError)
175
+ .join(", ")}`);
176
+ }
177
+ return acc;
178
+ }
179
+ return acc;
180
+ }, []);
181
+ if (compA.length < compB.length) {
182
+ const unexpectedElements = compB
183
+ .slice(compA.length)
184
+ .reduce((acc, formatElementB) => {
185
+ acc.push(`Unexpected ${formatElementB.type} element`);
186
+ return acc;
187
+ }, [])
188
+ .join(", ");
189
+ return [...errors, unexpectedElements].join(", ");
190
+ }
191
+ return errors.join(", ");
192
+ };
@@ -12,7 +12,16 @@ describe("findInvalid18nTranslations:compareTranslationFiles", () => {
12
12
  expect((0, findInvalidi18nTranslations_1.compareTranslationFiles)((0, flattenTranslations_1.flattenTranslations)({
13
13
  ...sourceFile,
14
14
  "ten.eleven.twelve": "ten eleven twelve",
15
- }), (0, flattenTranslations_1.flattenTranslations)(targetFile))).toEqual(["key_with_broken_de", "intlNumber_broken_de"]);
15
+ }), (0, flattenTranslations_1.flattenTranslations)(targetFile))).toEqual([
16
+ {
17
+ key: "key_with_broken_de",
18
+ msg: "Error in interpolation: Expected value but received val",
19
+ },
20
+ {
21
+ key: "intlNumber_broken_de",
22
+ msg: "Missing element interpolation",
23
+ },
24
+ ]);
16
25
  });
17
26
  it("should return an empty array if the strings contain paranthesis that have different content", () => {
18
27
  expect((0, findInvalidi18nTranslations_1.compareTranslationFiles)((0, flattenTranslations_1.flattenTranslations)({
@@ -31,7 +40,12 @@ describe("findInvalid18nTranslations:compareTranslationFiles", () => {
31
40
  tag: "This is some <b>bold text</b> and some <i>italic</i> text.",
32
41
  }, {
33
42
  tag: "There is some <b>bold text</b> and some other <span>italic</span> text.",
34
- })).toEqual(["tag"]);
43
+ })).toEqual([
44
+ {
45
+ key: "tag",
46
+ msg: 'Expected tag "</i>" but received "</span>", Expected tag "<i>" but received "<span>"',
47
+ },
48
+ ]);
35
49
  });
36
50
  it("should return empty array if tags are identical", () => {
37
51
  expect((0, findInvalidi18nTranslations_1.compareTranslationFiles)({
@@ -46,7 +60,18 @@ describe("findInvalidTranslations", () => {
46
60
  expect((0, findInvalidi18nTranslations_1.findInvalid18nTranslations)(sourceFile, { de: sourceFile })).toEqual({});
47
61
  });
48
62
  it("should return an object containing the keys for the missing language", () => {
49
- expect((0, findInvalidi18nTranslations_1.findInvalid18nTranslations)({ ...sourceFile, "ten.eleven.twelve": "ten eleven twelve" }, { de: targetFile })).toEqual({ de: ["key_with_broken_de", "intlNumber_broken_de"] });
63
+ expect((0, findInvalidi18nTranslations_1.findInvalid18nTranslations)({ ...sourceFile, "ten.eleven.twelve": "ten eleven twelve" }, { de: targetFile })).toEqual({
64
+ de: [
65
+ {
66
+ key: "key_with_broken_de",
67
+ msg: "Error in interpolation: Expected value but received val",
68
+ },
69
+ {
70
+ key: "intlNumber_broken_de",
71
+ msg: "Missing element interpolation",
72
+ },
73
+ ],
74
+ });
50
75
  });
51
76
  it("should return an object containing the keys for every language with missing key", () => {
52
77
  expect((0, findInvalidi18nTranslations_1.findInvalid18nTranslations)({ ...sourceFile, "ten.eleven.twelve": "ten eleven twelve" }, {
@@ -55,8 +80,22 @@ describe("findInvalidTranslations", () => {
55
80
  key_with_broken_de: "Some format {{value, formatname}} and some other format {{value, formatname}}",
56
81
  },
57
82
  })).toEqual({
58
- de: ["key_with_broken_de", "intlNumber_broken_de"],
59
- fr: ["key_with_broken_de"],
83
+ de: [
84
+ {
85
+ key: "key_with_broken_de",
86
+ msg: "Error in interpolation: Expected value but received val",
87
+ },
88
+ {
89
+ key: "intlNumber_broken_de",
90
+ msg: "Missing element interpolation",
91
+ },
92
+ ],
93
+ fr: [
94
+ {
95
+ key: "key_with_broken_de",
96
+ msg: "Unexpected interpolation element",
97
+ },
98
+ ],
60
99
  });
61
100
  });
62
101
  it("should find invalid interval", () => {
@@ -67,7 +106,12 @@ describe("findInvalidTranslations", () => {
67
106
  key1_interval: "(1-2)[one or two items];(3-7)[a few items];(7-inf)[a lot of items];",
68
107
  },
69
108
  })).toEqual({
70
- de: ["key1_interval"],
109
+ de: [
110
+ {
111
+ key: "key1_interval",
112
+ msg: "Error in plural: Expected 1 but received 1-2, Error in plural: Expected 2-7 but received 3-7",
113
+ },
114
+ ],
71
115
  });
72
116
  });
73
117
  it("should find invalid nested interpolation", () => {
@@ -78,7 +122,12 @@ describe("findInvalidTranslations", () => {
78
122
  "tree.one": "added {{somethings}}",
79
123
  },
80
124
  })).toEqual({
81
- de: ["tree.one"],
125
+ de: [
126
+ {
127
+ key: "tree.one",
128
+ msg: "Error in interpolation: Expected something but received somethings",
129
+ },
130
+ ],
82
131
  });
83
132
  });
84
133
  it("should find invalid relative time formatting", () => {
@@ -89,7 +138,12 @@ describe("findInvalidTranslations", () => {
89
138
  intlRelativeTimeWithOptionsExplicit: "Lorem {{val, relativetime(range: quarter; style: long;)}}",
90
139
  },
91
140
  })).toEqual({
92
- de: ["intlRelativeTimeWithOptionsExplicit"],
141
+ de: [
142
+ {
143
+ key: "intlRelativeTimeWithOptionsExplicit",
144
+ msg: "Error in interpolation: Expected relativetime(range: quarter; style: narrow;) but received relativetime(range: quarter; style: long;)",
145
+ },
146
+ ],
93
147
  });
94
148
  });
95
149
  it("should find invalid key with options", () => {
@@ -100,7 +154,12 @@ describe("findInvalidTranslations", () => {
100
154
  keyWithOptions: "Some format {{value, formatname(option3Name: option3Value; option4Name: option4Value)}}",
101
155
  },
102
156
  })).toEqual({
103
- de: ["keyWithOptions"],
157
+ de: [
158
+ {
159
+ key: "keyWithOptions",
160
+ msg: "Error in interpolation: Expected formatname(option1Name: option1Value; option2Name: option2Value) but received formatname(option3Name: option3Value; option4Name: option4Value)",
161
+ },
162
+ ],
104
163
  });
105
164
  });
106
165
  it("should find invalid nesting", () => {
@@ -111,7 +170,12 @@ describe("findInvalidTranslations", () => {
111
170
  nesting1: "1 $t(nesting3)",
112
171
  },
113
172
  })).toEqual({
114
- de: ["nesting1"],
173
+ de: [
174
+ {
175
+ key: "nesting1",
176
+ msg: "Error in nesting: Expected nesting2 but received nesting3",
177
+ },
178
+ ],
115
179
  });
116
180
  });
117
181
  it("should find invalid tags", () => {
@@ -122,7 +186,12 @@ describe("findInvalidTranslations", () => {
122
186
  tag: "There is some <b>bold text</b> and some other <span>text inside a span</span>!",
123
187
  },
124
188
  })).toEqual({
125
- de: ["tag"],
189
+ de: [
190
+ {
191
+ key: "tag",
192
+ msg: 'Expected tag "</i>" but received "</span>", Expected tag "<i>" but received "<span>"',
193
+ },
194
+ ],
126
195
  });
127
196
  });
128
197
  it("should recognize special characters", () => {
@@ -2,5 +2,7 @@ export declare const extract: (filesPaths: string[]) => {
2
2
  key: string;
3
3
  meta: {
4
4
  file: string;
5
+ namespace?: string;
6
+ dynamic?: boolean;
5
7
  };
6
8
  }[];
@@ -44,7 +44,7 @@ const GET_TRANSLATIONS = "getTranslations";
44
44
  const COMMENT_CONTAINS_STATIC_KEY_REGEX = /t\((["'])(.*?[^\\])(["'])\)/;
45
45
  const extract = (filesPaths) => {
46
46
  return filesPaths.flatMap(getKeys).sort((a, b) => {
47
- return a > b ? 1 : -1;
47
+ return a.key > b.key ? 1 : -1;
48
48
  });
49
49
  };
50
50
  exports.extract = extract;
@@ -53,24 +53,36 @@ const getKeys = (path) => {
53
53
  const sourceFile = ts.createSourceFile(path, content, ts.ScriptTarget.Latest, true);
54
54
  const foundKeys = [];
55
55
  let namespaces = [];
56
- let variable = "t";
57
- const getCurrentNamespace = () => {
56
+ const getCurrentNamespaces = (range = 1) => {
58
57
  if (namespaces.length > 0) {
59
- return namespaces[namespaces.length - 1];
58
+ return namespaces.slice(namespaces.length - range);
60
59
  }
61
60
  return null;
62
61
  };
63
- const pushNamespace = (name) => {
64
- namespaces.push(name);
62
+ const getCurrentNamespaceForIdentifier = (name) => {
63
+ return [...namespaces].reverse().find((namespace) => {
64
+ return namespace.variable === name;
65
+ });
65
66
  };
66
- const removeNamespace = () => {
67
+ const pushNamespace = (namespace) => {
68
+ namespaces.push(namespace);
69
+ };
70
+ const setNamespaceAsDynamic = (name) => {
71
+ namespaces = namespaces.map((namespace) => {
72
+ if (namespace.name === name) {
73
+ return { ...namespace, dynamic: true };
74
+ }
75
+ return namespace;
76
+ });
77
+ };
78
+ const removeNamespaces = (range = 1) => {
67
79
  if (namespaces.length > 0) {
68
- namespaces.pop();
80
+ namespaces = namespaces.slice(0, namespaces.length - range);
69
81
  }
70
82
  };
71
83
  const visit = (node) => {
72
84
  let key = null;
73
- let current = namespaces.length;
85
+ let initialNamespacesLength = namespaces.length;
74
86
  if (node === undefined) {
75
87
  return;
76
88
  }
@@ -82,14 +94,12 @@ const getKeys = (path) => {
82
94
  // from the default `t`, i.e.: const other = useTranslations("namespace1");
83
95
  if (node.initializer.expression.text === USE_TRANSLATIONS) {
84
96
  const [argument] = node.initializer.arguments;
97
+ const variable = ts.isIdentifier(node.name) ? node.name.text : "t";
85
98
  if (argument && ts.isStringLiteral(argument)) {
86
- pushNamespace(argument.text);
99
+ pushNamespace({ name: argument.text, variable });
87
100
  }
88
101
  else if (argument === undefined) {
89
- pushNamespace("");
90
- }
91
- if (ts.isIdentifier(node.name)) {
92
- variable = node.name.text;
102
+ pushNamespace({ name: "", variable });
93
103
  }
94
104
  }
95
105
  }
@@ -109,6 +119,7 @@ const getKeys = (path) => {
109
119
  ts.isIdentifier(node.initializer.expression.expression)) {
110
120
  if (node.initializer.expression.expression.text === GET_TRANSLATIONS) {
111
121
  const [argument] = node.initializer.expression.arguments;
122
+ const variable = ts.isIdentifier(node.name) ? node.name.text : "t";
112
123
  if (argument && ts.isObjectLiteralExpression(argument)) {
113
124
  argument.properties.forEach((property) => {
114
125
  if (property &&
@@ -117,18 +128,15 @@ const getKeys = (path) => {
117
128
  ts.isIdentifier(property.name) &&
118
129
  property.name.text === "namespace" &&
119
130
  ts.isStringLiteral(property.initializer)) {
120
- pushNamespace(property.initializer.text);
131
+ pushNamespace({ name: property.initializer.text, variable });
121
132
  }
122
133
  });
123
134
  }
124
135
  else if (argument && ts.isStringLiteral(argument)) {
125
- pushNamespace(argument.text);
136
+ pushNamespace({ name: argument.text, variable });
126
137
  }
127
138
  else if (argument === undefined) {
128
- pushNamespace("");
129
- }
130
- if (ts.isIdentifier(node.name)) {
131
- variable = node.name.text;
139
+ pushNamespace({ name: "", variable });
132
140
  }
133
141
  }
134
142
  }
@@ -168,7 +176,7 @@ const getKeys = (path) => {
168
176
  if (key) {
169
177
  foundKeys.push({
170
178
  key: inlineNamespace ? `${inlineNamespace}.${key}` : key,
171
- meta: { file: path },
179
+ meta: { file: path, namespace: inlineNamespace ?? undefined },
172
180
  });
173
181
  }
174
182
  }
@@ -177,35 +185,50 @@ const getKeys = (path) => {
177
185
  }
178
186
  }
179
187
  // Search for `t()` calls
180
- if (getCurrentNamespace() !== null &&
188
+ if (getCurrentNamespaces() !== null &&
181
189
  ts.isCallExpression(node) &&
182
190
  ts.isIdentifier(node.expression)) {
183
191
  const expressionName = node.expression.text;
184
- if (expressionName === variable) {
192
+ const namespace = getCurrentNamespaceForIdentifier(expressionName);
193
+ if (namespace) {
185
194
  const [argument] = node.arguments;
186
195
  if (argument && ts.isStringLiteral(argument)) {
187
- key = argument.text;
196
+ key = { name: argument.text, identifier: expressionName };
197
+ }
198
+ else if (argument && ts.isIdentifier(argument)) {
199
+ setNamespaceAsDynamic(namespace.name);
200
+ }
201
+ else if (argument && ts.isTemplateExpression(argument)) {
202
+ setNamespaceAsDynamic(namespace.name);
188
203
  }
189
204
  }
190
205
  }
191
206
  // Search for `t.*()` calls, i.e. t.html() or t.rich()
192
- if (getCurrentNamespace() !== null &&
207
+ if (getCurrentNamespaces() !== null &&
193
208
  ts.isCallExpression(node) &&
194
209
  ts.isPropertyAccessExpression(node.expression) &&
195
210
  ts.isIdentifier(node.expression.expression)) {
196
211
  const expressionName = node.expression.expression.text;
197
- if (expressionName === variable) {
212
+ const namespace = getCurrentNamespaceForIdentifier(expressionName);
213
+ if (namespace) {
198
214
  const [argument] = node.arguments;
199
215
  if (argument && ts.isStringLiteral(argument)) {
200
- key = argument.text;
216
+ key = { name: argument.text, identifier: expressionName };
217
+ }
218
+ else if (argument && ts.isIdentifier(argument)) {
219
+ setNamespaceAsDynamic(namespace.name);
220
+ }
221
+ else if (argument && ts.isTemplateExpression(argument)) {
222
+ setNamespaceAsDynamic(namespace.name);
201
223
  }
202
224
  }
203
225
  }
204
226
  if (key) {
205
- const namespace = getCurrentNamespace();
227
+ const namespace = getCurrentNamespaceForIdentifier(key.identifier);
228
+ const namespaceName = namespace ? namespace.name : "";
206
229
  foundKeys.push({
207
- key: namespace ? `${namespace}.${key}` : key,
208
- meta: { file: path },
230
+ key: namespaceName ? `${namespaceName}.${key.name}` : key.name,
231
+ meta: { file: path, namespace: namespaceName },
209
232
  });
210
233
  }
211
234
  // Search for single-line comments that contain the static values of a dynamic key
@@ -225,18 +248,32 @@ const getKeys = (path) => {
225
248
  // capture the string comment
226
249
  const commentKey = COMMENT_CONTAINS_STATIC_KEY_REGEX.exec(comment)?.[2];
227
250
  if (commentKey) {
228
- const namespace = getCurrentNamespace();
251
+ const namespace = getCurrentNamespaces();
252
+ const namespaceName = namespace ? namespace[0]?.name : "";
229
253
  foundKeys.push({
230
- key: namespace ? `${namespace}.${commentKey}` : commentKey,
231
- meta: { file: path },
254
+ key: namespaceName
255
+ ? `${namespaceName}.${commentKey}`
256
+ : commentKey,
257
+ meta: { file: path, namespace: namespaceName },
232
258
  });
233
259
  }
234
260
  }
235
261
  });
236
262
  }
237
263
  ts.forEachChild(node, visit);
238
- if (ts.isFunctionLike(node) && namespaces.length > current) {
239
- removeNamespace();
264
+ if (ts.isFunctionLike(node) &&
265
+ namespaces.length > initialNamespacesLength) {
266
+ // check if the namespaces are dynamic and add a placeholder key
267
+ const currentNamespaces = getCurrentNamespaces(namespaces?.length - initialNamespacesLength);
268
+ currentNamespaces?.forEach((namespace) => {
269
+ if (namespace.dynamic) {
270
+ foundKeys.push({
271
+ key: namespace.name,
272
+ meta: { file: path, namespace: namespace.name, dynamic: true },
273
+ });
274
+ }
275
+ });
276
+ removeNamespaces(namespaces.length - initialNamespacesLength);
240
277
  }
241
278
  };
242
279
  ts.forEachChild(sourceFile, visit);