@lingual/i18n-check 0.7.5 → 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;
@@ -67,6 +67,14 @@ const getKeys = (path) => {
67
67
  const pushNamespace = (namespace) => {
68
68
  namespaces.push(namespace);
69
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
+ };
70
78
  const removeNamespaces = (range = 1) => {
71
79
  if (namespaces.length > 0) {
72
80
  namespaces = namespaces.slice(0, namespaces.length - range);
@@ -74,7 +82,7 @@ const getKeys = (path) => {
74
82
  };
75
83
  const visit = (node) => {
76
84
  let key = null;
77
- let current = namespaces.length;
85
+ let initialNamespacesLength = namespaces.length;
78
86
  if (node === undefined) {
79
87
  return;
80
88
  }
@@ -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
  }
@@ -187,6 +195,12 @@ const getKeys = (path) => {
187
195
  if (argument && ts.isStringLiteral(argument)) {
188
196
  key = { name: argument.text, identifier: expressionName };
189
197
  }
198
+ else if (argument && ts.isIdentifier(argument)) {
199
+ setNamespaceAsDynamic(namespace.name);
200
+ }
201
+ else if (argument && ts.isTemplateExpression(argument)) {
202
+ setNamespaceAsDynamic(namespace.name);
203
+ }
190
204
  }
191
205
  }
192
206
  // Search for `t.*()` calls, i.e. t.html() or t.rich()
@@ -201,6 +215,12 @@ const getKeys = (path) => {
201
215
  if (argument && ts.isStringLiteral(argument)) {
202
216
  key = { name: argument.text, identifier: expressionName };
203
217
  }
218
+ else if (argument && ts.isIdentifier(argument)) {
219
+ setNamespaceAsDynamic(namespace.name);
220
+ }
221
+ else if (argument && ts.isTemplateExpression(argument)) {
222
+ setNamespaceAsDynamic(namespace.name);
223
+ }
204
224
  }
205
225
  }
206
226
  if (key) {
@@ -208,7 +228,7 @@ const getKeys = (path) => {
208
228
  const namespaceName = namespace ? namespace.name : "";
209
229
  foundKeys.push({
210
230
  key: namespaceName ? `${namespaceName}.${key.name}` : key.name,
211
- meta: { file: path },
231
+ meta: { file: path, namespace: namespaceName },
212
232
  });
213
233
  }
214
234
  // Search for single-line comments that contain the static values of a dynamic key
@@ -234,15 +254,26 @@ const getKeys = (path) => {
234
254
  key: namespaceName
235
255
  ? `${namespaceName}.${commentKey}`
236
256
  : commentKey,
237
- meta: { file: path },
257
+ meta: { file: path, namespace: namespaceName },
238
258
  });
239
259
  }
240
260
  }
241
261
  });
242
262
  }
243
263
  ts.forEachChild(node, visit);
244
- if (ts.isFunctionLike(node) && namespaces.length > current) {
245
- removeNamespaces(namespaces.length - current);
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);
246
277
  }
247
278
  };
248
279
  ts.forEachChild(sourceFile, visit);