i18next-cli 1.56.0 → 1.56.2

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/dist/cjs/cli.js CHANGED
@@ -32,7 +32,7 @@ const program = new commander.Command();
32
32
  program
33
33
  .name('i18next-cli')
34
34
  .description('A unified, high-performance i18next CLI.')
35
- .version('1.56.0'); // This string is replaced with the actual version at build time by rollup
35
+ .version('1.56.2'); // This string is replaced with the actual version at build time by rollup
36
36
  // new: global config override option
37
37
  program.option('-c, --config <path>', 'Path to i18next-cli config file (overrides detection)');
38
38
  program
@@ -755,14 +755,22 @@ function findHardcodedStrings(ast, code, config) {
755
755
  }
756
756
  if (node.type === 'StringLiteral') {
757
757
  const parent = currentAncestors[currentAncestors.length - 2];
758
+ const grandparent = currentAncestors[currentAncestors.length - 3];
758
759
  // Determine whether this attribute is inside any ignored element (handles nested Trans etc.)
759
760
  const insideIgnored = isWithinIgnoredElement(currentAncestors);
760
- if (parent?.type === 'JSXAttribute' && !insideIgnored) {
761
- const rawAttrName = extractAttrName(parent.name);
761
+ // A StringLiteral can be an attribute value in two forms:
762
+ // <tag attr="value" /> → parent is JSXAttribute
763
+ // <tag attr={"value"} /> → parent is JSXExpressionContainer, grandparent is JSXAttribute
764
+ const attrNode = parent?.type === 'JSXAttribute'
765
+ ? parent
766
+ : (parent?.type === 'JSXExpressionContainer' && grandparent?.type === 'JSXAttribute' ? grandparent : null);
767
+ if (attrNode && !insideIgnored) {
768
+ const rawAttrName = extractAttrName(attrNode.name);
762
769
  const attrNameLower = rawAttrName ? String(rawAttrName).toLowerCase() : null;
763
770
  // Check tag-level acceptance if acceptedTagsSet provided: attributes should only be considered
764
- // when the nearest enclosing element is accepted.
765
- const parentElement = currentAncestors.slice(0, -2).reverse().find(a => a && typeof a === 'object' && (a.type === 'JSXElement' || a.type === 'JSXOpeningElement' || a.type === 'JSXSelfClosingElement'));
771
+ // when the nearest enclosing element is accepted. Use the ancestors above the attrNode.
772
+ const attrNodeIdx = currentAncestors.indexOf(attrNode);
773
+ const parentElement = currentAncestors.slice(0, attrNodeIdx).reverse().find(a => a && typeof a === 'object' && (a.type === 'JSXElement' || a.type === 'JSXOpeningElement' || a.type === 'JSXSelfClosingElement'));
766
774
  if (acceptedTagsSet && parentElement) {
767
775
  const parentName = extractJSXName(parentElement);
768
776
  if (!parentName || !acceptedTagsSet.has(String(parentName).toLowerCase())) {
@@ -786,6 +794,21 @@ function findHardcodedStrings(ast, code, config) {
786
794
  }
787
795
  }
788
796
  }
797
+ // Hardcoded string inside a JSX expression container used as element child:
798
+ // <tag>{"hello"}</tag> → parent is JSXExpressionContainer, grandparent is JSXElement/JSXFragment
799
+ // Apply the same filters used for JSXText so this behaves like raw text.
800
+ const isJsxChildExpression = parent?.type === 'JSXExpressionContainer' &&
801
+ (grandparent?.type === 'JSXElement' || grandparent?.type === 'JSXFragment');
802
+ if (isJsxChildExpression && !insideIgnored) {
803
+ // Respect attribute-only mode: when acceptedAttributes is set without acceptedTags,
804
+ // only attribute strings are linted — skip JSX child text.
805
+ if (!(acceptedAttributesSet && !acceptedTagsSet)) {
806
+ const text = node.value.trim();
807
+ if (text && text.length > 1 && text !== '...' && !isUrlOrPath(text) && isNaN(Number(text)) && !text.startsWith('{{')) {
808
+ nodesToLint.push(node);
809
+ }
810
+ }
811
+ }
789
812
  }
790
813
  // Recurse into children
791
814
  for (const key of Object.keys(node)) {
@@ -10,6 +10,7 @@ require('glob');
10
10
  var nestedObject = require('./utils/nested-object.js');
11
11
  var fileUtils = require('./utils/file-utils.js');
12
12
  var pluralRules = require('./utils/plural-rules.js');
13
+ var nesting = require('./utils/nesting.js');
13
14
  var contextVariants = require('./utils/context-variants.js');
14
15
  var funnelMsgTracker = require('./utils/funnel-msg-tracker.js');
15
16
  require('./extractor/parsers/jsx-parser.js');
@@ -107,13 +108,23 @@ async function generateStatusReport(config) {
107
108
  keysByNs,
108
109
  locales: new Map(),
109
110
  };
110
- // Discover context variants that live in the primary translation file but
111
- // are not directly extracted as keys (see issue #243). When source code uses
112
- // a dynamic context value like `t('exportType', { context: type })`, the
113
- // extractor can only tag the base key as "accepting context"; the actual
114
- // context values (`_gas`, `_water`, ...) are only visible in the primary
115
- // translation file. Without this scan, status never checks those variants
116
- // for translation gaps in secondary locales.
111
+ // Build per-namespace "virtual" key lists for translation entries that the
112
+ // AST-based extractor cannot see on its own. Both inputs come from the
113
+ // primary translation file:
114
+ //
115
+ // 1. Context variants of an accepting-context key (see issue #243).
116
+ // `t('exportType', { context: dynamic })` only registers the base key;
117
+ // the concrete `exportType_gas` / `exportType_water` variants live in
118
+ // the primary file.
119
+ //
120
+ // 2. Keys reachable only via `$t(...)` nested references from inside an
121
+ // existing translation value (see follow-up to issue #241).
122
+ // `"girlsAndBoys": "... $t(boys, {\"count\": x}) ..."` doesn't appear
123
+ // in source code, yet the referenced keys (`boys`, plus per-locale
124
+ // plural forms) must be checked in every secondary locale.
125
+ //
126
+ // Both scans need the primary translation file per namespace, so the load
127
+ // is shared.
117
128
  const keysAcceptingContext = new Set();
118
129
  for (const keys of keysByNs.values()) {
119
130
  for (const k of keys) {
@@ -122,14 +133,61 @@ async function generateStatusReport(config) {
122
133
  }
123
134
  }
124
135
  const contextVariantsByNs = new Map();
125
- if (keysAcceptingContext.size > 0) {
126
- const primaryMerged = mergeNamespaces
127
- ? ((await fileUtils.loadTranslationFile(node_path.resolve(process.cwd(), fileUtils.getOutputPath(config.extract.output, primaryLanguage, (defaultNS === false ? 'translation' : (defaultNS || 'translation')))))) || {})
128
- : null;
129
- for (const ns of keysByNs.keys()) {
130
- const primaryNsTranslations = mergeNamespaces
131
- ? (primaryMerged?.[ns] ?? primaryMerged ?? {})
132
- : ((await fileUtils.loadTranslationFile(node_path.resolve(process.cwd(), fileUtils.getOutputPath(config.extract.output, primaryLanguage, ns)))) || {});
136
+ const nestedReferenceKeysByNs = new Map();
137
+ const primaryMergedForScan = mergeNamespaces
138
+ ? ((await fileUtils.loadTranslationFile(node_path.resolve(process.cwd(), fileUtils.getOutputPath(config.extract.output, primaryLanguage, (defaultNS === false ? 'translation' : (defaultNS || 'translation')))))) || {})
139
+ : null;
140
+ const collectNestedRefsFromValue = (value, refNs, bucket, seen) => {
141
+ if (typeof value === 'string') {
142
+ if (seen.has(value))
143
+ return;
144
+ seen.add(value);
145
+ const refs = nesting.parseNestedReferences(value, {
146
+ nestingPrefix: config.extract.nestingPrefix,
147
+ nestingSuffix: config.extract.nestingSuffix,
148
+ nestingOptionsSeparator: config.extract.nestingOptionsSeparator,
149
+ nsSeparator: config.extract.nsSeparator,
150
+ defaultNS: config.extract.defaultNS
151
+ });
152
+ for (const ref of refs) {
153
+ // References with an explicit namespace that differs from the current
154
+ // bucket are ignored — they belong to another namespace's scan.
155
+ const normalizedRefNs = ref.ns === undefined || ref.ns === null
156
+ ? (config.extract.defaultNS ?? 'translation')
157
+ : ref.ns;
158
+ if (normalizedRefNs !== refNs)
159
+ continue;
160
+ if (ref.context !== undefined) {
161
+ const ctxKey = `${ref.key}${contextSeparator}${ref.context}`;
162
+ if (ref.hasCount) {
163
+ // Treat `key_ctx` as a base plural key; the per-locale loop
164
+ // expands it into the correct CLDR forms for each target locale.
165
+ bucket.push({ key: ctxKey, hasCount: true });
166
+ }
167
+ else {
168
+ bucket.push({ key: ref.key });
169
+ bucket.push({ key: ctxKey });
170
+ }
171
+ }
172
+ else if (ref.hasCount) {
173
+ bucket.push({ key: ref.key, hasCount: true });
174
+ }
175
+ else {
176
+ bucket.push({ key: ref.key });
177
+ }
178
+ }
179
+ }
180
+ else if (value && typeof value === 'object' && !Array.isArray(value)) {
181
+ for (const v of Object.values(value)) {
182
+ collectNestedRefsFromValue(v, refNs, bucket, seen);
183
+ }
184
+ }
185
+ };
186
+ for (const ns of keysByNs.keys()) {
187
+ const primaryNsTranslations = mergeNamespaces
188
+ ? (primaryMergedForScan?.[ns] ?? primaryMergedForScan ?? {})
189
+ : ((await fileUtils.loadTranslationFile(node_path.resolve(process.cwd(), fileUtils.getOutputPath(config.extract.output, primaryLanguage, ns)))) || {});
190
+ if (keysAcceptingContext.size > 0) {
133
191
  const primaryKeys = nestedObject.getNestedKeys(primaryNsTranslations, keySeparator ?? '.');
134
192
  const variants = [];
135
193
  for (const primaryKey of primaryKeys) {
@@ -140,6 +198,10 @@ async function generateStatusReport(config) {
140
198
  if (variants.length > 0)
141
199
  contextVariantsByNs.set(ns, variants);
142
200
  }
201
+ const nestedRefKeys = [];
202
+ collectNestedRefsFromValue(primaryNsTranslations, ns, nestedRefKeys, new Set());
203
+ if (nestedRefKeys.length > 0)
204
+ nestedReferenceKeysByNs.set(ns, nestedRefKeys);
143
205
  }
144
206
  for (const locale of secondaryLanguages) {
145
207
  let totalTranslatedForLocale = 0;
@@ -209,7 +271,14 @@ async function generateStatusReport(config) {
209
271
  return primaryState;
210
272
  };
211
273
  const processedKeys = new Set();
212
- for (const { key: baseKey, hasCount, isOrdinal, isExpandedPlural } of keysInNs) {
274
+ // Combine AST-extracted keys with nested-reference keys discovered in
275
+ // the primary translation file (see follow-up on issue #241). Both go
276
+ // through the same plural-expansion logic; processedKeys dedupes.
277
+ const nestedRefKeys = nestedReferenceKeysByNs.get(ns) || [];
278
+ const combinedKeysInNs = nestedRefKeys.length > 0
279
+ ? [...keysInNs, ...nestedRefKeys]
280
+ : keysInNs;
281
+ for (const { key: baseKey, hasCount, isOrdinal, isExpandedPlural } of combinedKeysInNs) {
213
282
  if (hasCount) {
214
283
  if (isExpandedPlural) {
215
284
  // This is an already-expanded plural variant key (e.g., key_one, key_other)
package/dist/esm/cli.js CHANGED
@@ -30,7 +30,7 @@ const program = new Command();
30
30
  program
31
31
  .name('i18next-cli')
32
32
  .description('A unified, high-performance i18next CLI.')
33
- .version('1.56.0'); // This string is replaced with the actual version at build time by rollup
33
+ .version('1.56.2'); // This string is replaced with the actual version at build time by rollup
34
34
  // new: global config override option
35
35
  program.option('-c, --config <path>', 'Path to i18next-cli config file (overrides detection)');
36
36
  program
@@ -753,14 +753,22 @@ function findHardcodedStrings(ast, code, config) {
753
753
  }
754
754
  if (node.type === 'StringLiteral') {
755
755
  const parent = currentAncestors[currentAncestors.length - 2];
756
+ const grandparent = currentAncestors[currentAncestors.length - 3];
756
757
  // Determine whether this attribute is inside any ignored element (handles nested Trans etc.)
757
758
  const insideIgnored = isWithinIgnoredElement(currentAncestors);
758
- if (parent?.type === 'JSXAttribute' && !insideIgnored) {
759
- const rawAttrName = extractAttrName(parent.name);
759
+ // A StringLiteral can be an attribute value in two forms:
760
+ // <tag attr="value" /> → parent is JSXAttribute
761
+ // <tag attr={"value"} /> → parent is JSXExpressionContainer, grandparent is JSXAttribute
762
+ const attrNode = parent?.type === 'JSXAttribute'
763
+ ? parent
764
+ : (parent?.type === 'JSXExpressionContainer' && grandparent?.type === 'JSXAttribute' ? grandparent : null);
765
+ if (attrNode && !insideIgnored) {
766
+ const rawAttrName = extractAttrName(attrNode.name);
760
767
  const attrNameLower = rawAttrName ? String(rawAttrName).toLowerCase() : null;
761
768
  // Check tag-level acceptance if acceptedTagsSet provided: attributes should only be considered
762
- // when the nearest enclosing element is accepted.
763
- const parentElement = currentAncestors.slice(0, -2).reverse().find(a => a && typeof a === 'object' && (a.type === 'JSXElement' || a.type === 'JSXOpeningElement' || a.type === 'JSXSelfClosingElement'));
769
+ // when the nearest enclosing element is accepted. Use the ancestors above the attrNode.
770
+ const attrNodeIdx = currentAncestors.indexOf(attrNode);
771
+ const parentElement = currentAncestors.slice(0, attrNodeIdx).reverse().find(a => a && typeof a === 'object' && (a.type === 'JSXElement' || a.type === 'JSXOpeningElement' || a.type === 'JSXSelfClosingElement'));
764
772
  if (acceptedTagsSet && parentElement) {
765
773
  const parentName = extractJSXName(parentElement);
766
774
  if (!parentName || !acceptedTagsSet.has(String(parentName).toLowerCase())) {
@@ -784,6 +792,21 @@ function findHardcodedStrings(ast, code, config) {
784
792
  }
785
793
  }
786
794
  }
795
+ // Hardcoded string inside a JSX expression container used as element child:
796
+ // <tag>{"hello"}</tag> → parent is JSXExpressionContainer, grandparent is JSXElement/JSXFragment
797
+ // Apply the same filters used for JSXText so this behaves like raw text.
798
+ const isJsxChildExpression = parent?.type === 'JSXExpressionContainer' &&
799
+ (grandparent?.type === 'JSXElement' || grandparent?.type === 'JSXFragment');
800
+ if (isJsxChildExpression && !insideIgnored) {
801
+ // Respect attribute-only mode: when acceptedAttributes is set without acceptedTags,
802
+ // only attribute strings are linted — skip JSX child text.
803
+ if (!(acceptedAttributesSet && !acceptedTagsSet)) {
804
+ const text = node.value.trim();
805
+ if (text && text.length > 1 && text !== '...' && !isUrlOrPath(text) && isNaN(Number(text)) && !text.startsWith('{{')) {
806
+ nodesToLint.push(node);
807
+ }
808
+ }
809
+ }
787
810
  }
788
811
  // Recurse into children
789
812
  for (const key of Object.keys(node)) {
@@ -8,6 +8,7 @@ import 'glob';
8
8
  import { getNestedKeys, getNestedValue } from './utils/nested-object.js';
9
9
  import { loadTranslationFile, getOutputPath } from './utils/file-utils.js';
10
10
  import { safePluralRules } from './utils/plural-rules.js';
11
+ import { parseNestedReferences } from './utils/nesting.js';
11
12
  import { isContextVariantOfAcceptingKey } from './utils/context-variants.js';
12
13
  import { shouldShowFunnel, recordFunnelShown } from './utils/funnel-msg-tracker.js';
13
14
  import './extractor/parsers/jsx-parser.js';
@@ -105,13 +106,23 @@ async function generateStatusReport(config) {
105
106
  keysByNs,
106
107
  locales: new Map(),
107
108
  };
108
- // Discover context variants that live in the primary translation file but
109
- // are not directly extracted as keys (see issue #243). When source code uses
110
- // a dynamic context value like `t('exportType', { context: type })`, the
111
- // extractor can only tag the base key as "accepting context"; the actual
112
- // context values (`_gas`, `_water`, ...) are only visible in the primary
113
- // translation file. Without this scan, status never checks those variants
114
- // for translation gaps in secondary locales.
109
+ // Build per-namespace "virtual" key lists for translation entries that the
110
+ // AST-based extractor cannot see on its own. Both inputs come from the
111
+ // primary translation file:
112
+ //
113
+ // 1. Context variants of an accepting-context key (see issue #243).
114
+ // `t('exportType', { context: dynamic })` only registers the base key;
115
+ // the concrete `exportType_gas` / `exportType_water` variants live in
116
+ // the primary file.
117
+ //
118
+ // 2. Keys reachable only via `$t(...)` nested references from inside an
119
+ // existing translation value (see follow-up to issue #241).
120
+ // `"girlsAndBoys": "... $t(boys, {\"count\": x}) ..."` doesn't appear
121
+ // in source code, yet the referenced keys (`boys`, plus per-locale
122
+ // plural forms) must be checked in every secondary locale.
123
+ //
124
+ // Both scans need the primary translation file per namespace, so the load
125
+ // is shared.
115
126
  const keysAcceptingContext = new Set();
116
127
  for (const keys of keysByNs.values()) {
117
128
  for (const k of keys) {
@@ -120,14 +131,61 @@ async function generateStatusReport(config) {
120
131
  }
121
132
  }
122
133
  const contextVariantsByNs = new Map();
123
- if (keysAcceptingContext.size > 0) {
124
- const primaryMerged = mergeNamespaces
125
- ? ((await loadTranslationFile(resolve(process.cwd(), getOutputPath(config.extract.output, primaryLanguage, (defaultNS === false ? 'translation' : (defaultNS || 'translation')))))) || {})
126
- : null;
127
- for (const ns of keysByNs.keys()) {
128
- const primaryNsTranslations = mergeNamespaces
129
- ? (primaryMerged?.[ns] ?? primaryMerged ?? {})
130
- : ((await loadTranslationFile(resolve(process.cwd(), getOutputPath(config.extract.output, primaryLanguage, ns)))) || {});
134
+ const nestedReferenceKeysByNs = new Map();
135
+ const primaryMergedForScan = mergeNamespaces
136
+ ? ((await loadTranslationFile(resolve(process.cwd(), getOutputPath(config.extract.output, primaryLanguage, (defaultNS === false ? 'translation' : (defaultNS || 'translation')))))) || {})
137
+ : null;
138
+ const collectNestedRefsFromValue = (value, refNs, bucket, seen) => {
139
+ if (typeof value === 'string') {
140
+ if (seen.has(value))
141
+ return;
142
+ seen.add(value);
143
+ const refs = parseNestedReferences(value, {
144
+ nestingPrefix: config.extract.nestingPrefix,
145
+ nestingSuffix: config.extract.nestingSuffix,
146
+ nestingOptionsSeparator: config.extract.nestingOptionsSeparator,
147
+ nsSeparator: config.extract.nsSeparator,
148
+ defaultNS: config.extract.defaultNS
149
+ });
150
+ for (const ref of refs) {
151
+ // References with an explicit namespace that differs from the current
152
+ // bucket are ignored — they belong to another namespace's scan.
153
+ const normalizedRefNs = ref.ns === undefined || ref.ns === null
154
+ ? (config.extract.defaultNS ?? 'translation')
155
+ : ref.ns;
156
+ if (normalizedRefNs !== refNs)
157
+ continue;
158
+ if (ref.context !== undefined) {
159
+ const ctxKey = `${ref.key}${contextSeparator}${ref.context}`;
160
+ if (ref.hasCount) {
161
+ // Treat `key_ctx` as a base plural key; the per-locale loop
162
+ // expands it into the correct CLDR forms for each target locale.
163
+ bucket.push({ key: ctxKey, hasCount: true });
164
+ }
165
+ else {
166
+ bucket.push({ key: ref.key });
167
+ bucket.push({ key: ctxKey });
168
+ }
169
+ }
170
+ else if (ref.hasCount) {
171
+ bucket.push({ key: ref.key, hasCount: true });
172
+ }
173
+ else {
174
+ bucket.push({ key: ref.key });
175
+ }
176
+ }
177
+ }
178
+ else if (value && typeof value === 'object' && !Array.isArray(value)) {
179
+ for (const v of Object.values(value)) {
180
+ collectNestedRefsFromValue(v, refNs, bucket, seen);
181
+ }
182
+ }
183
+ };
184
+ for (const ns of keysByNs.keys()) {
185
+ const primaryNsTranslations = mergeNamespaces
186
+ ? (primaryMergedForScan?.[ns] ?? primaryMergedForScan ?? {})
187
+ : ((await loadTranslationFile(resolve(process.cwd(), getOutputPath(config.extract.output, primaryLanguage, ns)))) || {});
188
+ if (keysAcceptingContext.size > 0) {
131
189
  const primaryKeys = getNestedKeys(primaryNsTranslations, keySeparator ?? '.');
132
190
  const variants = [];
133
191
  for (const primaryKey of primaryKeys) {
@@ -138,6 +196,10 @@ async function generateStatusReport(config) {
138
196
  if (variants.length > 0)
139
197
  contextVariantsByNs.set(ns, variants);
140
198
  }
199
+ const nestedRefKeys = [];
200
+ collectNestedRefsFromValue(primaryNsTranslations, ns, nestedRefKeys, new Set());
201
+ if (nestedRefKeys.length > 0)
202
+ nestedReferenceKeysByNs.set(ns, nestedRefKeys);
141
203
  }
142
204
  for (const locale of secondaryLanguages) {
143
205
  let totalTranslatedForLocale = 0;
@@ -207,7 +269,14 @@ async function generateStatusReport(config) {
207
269
  return primaryState;
208
270
  };
209
271
  const processedKeys = new Set();
210
- for (const { key: baseKey, hasCount, isOrdinal, isExpandedPlural } of keysInNs) {
272
+ // Combine AST-extracted keys with nested-reference keys discovered in
273
+ // the primary translation file (see follow-up on issue #241). Both go
274
+ // through the same plural-expansion logic; processedKeys dedupes.
275
+ const nestedRefKeys = nestedReferenceKeysByNs.get(ns) || [];
276
+ const combinedKeysInNs = nestedRefKeys.length > 0
277
+ ? [...keysInNs, ...nestedRefKeys]
278
+ : keysInNs;
279
+ for (const { key: baseKey, hasCount, isOrdinal, isExpandedPlural } of combinedKeysInNs) {
211
280
  if (hasCount) {
212
281
  if (isExpandedPlural) {
213
282
  // This is an already-expanded plural variant key (e.g., key_one, key_other)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i18next-cli",
3
- "version": "1.56.0",
3
+ "version": "1.56.2",
4
4
  "description": "A unified, high-performance i18next CLI.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1 +1 @@
1
- {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../src/status.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,oBAAoB,EAAgB,MAAM,YAAY,CAAA;AAMpE;;GAEG;AACH,UAAU,aAAa;IACrB,0EAA0E;IAC1E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,wCAAwC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uEAAuE;IACvE,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAqDD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,SAAS,CAAE,MAAM,EAAE,oBAAoB,EAAE,OAAO,GAAE,aAAkB,iBAuBzF"}
1
+ {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../src/status.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,oBAAoB,EAAgB,MAAM,YAAY,CAAA;AAOpE;;GAEG;AACH,UAAU,aAAa;IACrB,0EAA0E;IAC1E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,wCAAwC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uEAAuE;IACvE,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAqDD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,SAAS,CAAE,MAAM,EAAE,oBAAoB,EAAE,OAAO,GAAE,aAAkB,iBAuBzF"}