@saasmakers/eslint 0.1.3 → 0.1.4

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.
@@ -183,6 +183,19 @@ const eslint_config = antfu__default(
183
183
  "saasmakers/ts-multiline-ternary": "error",
184
184
  "saasmakers/ts-multiline-union": "error",
185
185
  "saasmakers/ts-sort-tests": "error",
186
+ "saasmakers/vue-i18n-consistent-locales": [
187
+ "error",
188
+ {
189
+ locales: [
190
+ "en",
191
+ "fr",
192
+ "ja"
193
+ ]
194
+ }
195
+ ],
196
+ "saasmakers/vue-i18n-consistent-t": "error",
197
+ "saasmakers/vue-i18n-sort-keys": "error",
198
+ "saasmakers/vue-i18n-unused-strings": "error",
186
199
  "sort-imports": "off",
187
200
  "ts/ban-ts-comment": "off",
188
201
  "ts/ban-ts-ignore": "off",
@@ -175,6 +175,19 @@ var eslint_config = antfu(
175
175
  "saasmakers/ts-multiline-ternary": "error",
176
176
  "saasmakers/ts-multiline-union": "error",
177
177
  "saasmakers/ts-sort-tests": "error",
178
+ "saasmakers/vue-i18n-consistent-locales": [
179
+ "error",
180
+ {
181
+ locales: [
182
+ "en",
183
+ "fr",
184
+ "ja"
185
+ ]
186
+ }
187
+ ],
188
+ "saasmakers/vue-i18n-consistent-t": "error",
189
+ "saasmakers/vue-i18n-sort-keys": "error",
190
+ "saasmakers/vue-i18n-unused-strings": "error",
178
191
  "sort-imports": "off",
179
192
  "ts/ban-ts-comment": "off",
180
193
  "ts/ban-ts-ignore": "off",
@@ -175,6 +175,19 @@ var eslint_config = antfu(
175
175
  "saasmakers/ts-multiline-ternary": "error",
176
176
  "saasmakers/ts-multiline-union": "error",
177
177
  "saasmakers/ts-sort-tests": "error",
178
+ "saasmakers/vue-i18n-consistent-locales": [
179
+ "error",
180
+ {
181
+ locales: [
182
+ "en",
183
+ "fr",
184
+ "ja"
185
+ ]
186
+ }
187
+ ],
188
+ "saasmakers/vue-i18n-consistent-t": "error",
189
+ "saasmakers/vue-i18n-sort-keys": "error",
190
+ "saasmakers/vue-i18n-unused-strings": "error",
178
191
  "sort-imports": "off",
179
192
  "ts/ban-ts-comment": "off",
180
193
  "ts/ban-ts-ignore": "off",
@@ -175,6 +175,19 @@ var eslint_config = antfu(
175
175
  "saasmakers/ts-multiline-ternary": "error",
176
176
  "saasmakers/ts-multiline-union": "error",
177
177
  "saasmakers/ts-sort-tests": "error",
178
+ "saasmakers/vue-i18n-consistent-locales": [
179
+ "error",
180
+ {
181
+ locales: [
182
+ "en",
183
+ "fr",
184
+ "ja"
185
+ ]
186
+ }
187
+ ],
188
+ "saasmakers/vue-i18n-consistent-t": "error",
189
+ "saasmakers/vue-i18n-sort-keys": "error",
190
+ "saasmakers/vue-i18n-unused-strings": "error",
178
191
  "sort-imports": "off",
179
192
  "ts/ban-ts-comment": "off",
180
193
  "ts/ban-ts-ignore": "off",
@@ -175,6 +175,19 @@ const eslint_config = antfu(
175
175
  "saasmakers/ts-multiline-ternary": "error",
176
176
  "saasmakers/ts-multiline-union": "error",
177
177
  "saasmakers/ts-sort-tests": "error",
178
+ "saasmakers/vue-i18n-consistent-locales": [
179
+ "error",
180
+ {
181
+ locales: [
182
+ "en",
183
+ "fr",
184
+ "ja"
185
+ ]
186
+ }
187
+ ],
188
+ "saasmakers/vue-i18n-consistent-t": "error",
189
+ "saasmakers/vue-i18n-sort-keys": "error",
190
+ "saasmakers/vue-i18n-unused-strings": "error",
178
191
  "sort-imports": "off",
179
192
  "ts/ban-ts-comment": "off",
180
193
  "ts/ban-ts-ignore": "off",
package/dist/index.cjs CHANGED
@@ -14084,7 +14084,7 @@ function requireDist () {
14084
14084
 
14085
14085
  var distExports = requireDist();
14086
14086
 
14087
- const rule$2 = {
14087
+ const rule$6 = {
14088
14088
  meta: {
14089
14089
  docs: {
14090
14090
  category: "Stylistic Issues",
@@ -14147,7 +14147,7 @@ const rule$2 = {
14147
14147
  }
14148
14148
  };
14149
14149
 
14150
- const rule$1 = {
14150
+ const rule$5 = {
14151
14151
  meta: {
14152
14152
  docs: {
14153
14153
  category: "Stylistic Issues",
@@ -14213,7 +14213,7 @@ const rule$1 = {
14213
14213
  }
14214
14214
  };
14215
14215
 
14216
- const rule = {
14216
+ const rule$4 = {
14217
14217
  meta: {
14218
14218
  docs: {
14219
14219
  category: "Best Practices",
@@ -14328,15 +14328,382 @@ const rule = {
14328
14328
  }
14329
14329
  };
14330
14330
 
14331
+ const rule$3 = {
14332
+ meta: {
14333
+ docs: {
14334
+ category: "Possible Errors",
14335
+ description: "Enforce consistent i18n locale keys across translations",
14336
+ recommended: true
14337
+ },
14338
+ messages: {
14339
+ invalidJson: "Invalid JSON in i18n block: {{error}}",
14340
+ invalidLocale: "Invalid locale: {{locale}}. Allowed locales are: {{allowed}}",
14341
+ missingLocale: "Missing required locale: {{locale}}",
14342
+ missingTranslations: 'Missing translations in "{{locale}}" locale: {{missing}}'
14343
+ },
14344
+ schema: [
14345
+ {
14346
+ additionalProperties: false,
14347
+ properties: {
14348
+ locales: {
14349
+ items: { type: "string" },
14350
+ type: "array"
14351
+ }
14352
+ },
14353
+ type: "object"
14354
+ }
14355
+ ],
14356
+ type: "problem"
14357
+ },
14358
+ create(context) {
14359
+ if (!context.filename.endsWith(".vue")) {
14360
+ return {};
14361
+ }
14362
+ const locales = context.options[0]?.locales || ["en", "fr"];
14363
+ return {
14364
+ Program() {
14365
+ const source = context.sourceCode.getText();
14366
+ const i18nMatch = source.match(/(<i18n\s+lang=["']json["']>)([\s\S]*?)(<\/i18n>)/i);
14367
+ if (i18nMatch) {
14368
+ const startOffset = i18nMatch.index + i18nMatch[1].length;
14369
+ const i18nContent = i18nMatch[2].trim();
14370
+ try {
14371
+ const parsed = JSON.parse(i18nContent);
14372
+ for (const locale of locales) {
14373
+ if (!parsed[locale]) {
14374
+ context.report({
14375
+ data: { locale },
14376
+ loc: {
14377
+ end: context.sourceCode.getLocFromIndex(startOffset + i18nContent.length),
14378
+ start: context.sourceCode.getLocFromIndex(startOffset)
14379
+ },
14380
+ messageId: "missingLocale"
14381
+ });
14382
+ }
14383
+ }
14384
+ for (const locale of Object.keys(parsed)) {
14385
+ if (!locales.includes(locale)) {
14386
+ const localeMatch = new RegExp(`"${locale}"\\s*:`, "g").exec(i18nContent);
14387
+ if (localeMatch) {
14388
+ const localeOffset = startOffset + localeMatch.index;
14389
+ context.report({
14390
+ data: {
14391
+ allowed: locales.join(", "),
14392
+ locale
14393
+ },
14394
+ loc: {
14395
+ end: context.sourceCode.getLocFromIndex(localeOffset + locale.length + 2),
14396
+ start: context.sourceCode.getLocFromIndex(localeOffset)
14397
+ },
14398
+ messageId: "invalidLocale"
14399
+ });
14400
+ }
14401
+ }
14402
+ }
14403
+ const allKeys = /* @__PURE__ */ new Set();
14404
+ for (const locale of locales) {
14405
+ if (parsed[locale]) {
14406
+ const keys = getAllKeys(parsed[locale]);
14407
+ keys.forEach((key) => allKeys.add(key));
14408
+ }
14409
+ }
14410
+ for (const locale of locales) {
14411
+ if (parsed[locale]) {
14412
+ const localeKeys = getAllKeys(parsed[locale]);
14413
+ const missingKeys = [...allKeys].filter((key) => !localeKeys.includes(key));
14414
+ if (missingKeys.length > 0) {
14415
+ const localeMatch = new RegExp(`"${locale}"\\s*:\\s*{`, "g").exec(i18nContent);
14416
+ if (localeMatch) {
14417
+ const localeOffset = startOffset + localeMatch.index;
14418
+ context.report({
14419
+ data: {
14420
+ locale,
14421
+ missing: missingKeys.join(", ")
14422
+ },
14423
+ loc: {
14424
+ end: context.sourceCode.getLocFromIndex(localeOffset + locale.length + 2),
14425
+ start: context.sourceCode.getLocFromIndex(localeOffset)
14426
+ },
14427
+ messageId: "missingTranslations"
14428
+ });
14429
+ }
14430
+ }
14431
+ }
14432
+ }
14433
+ } catch (error) {
14434
+ const errorMessage = error instanceof Error ? error.message : String(error);
14435
+ context.report({
14436
+ data: { error: errorMessage },
14437
+ loc: {
14438
+ end: context.sourceCode.getLocFromIndex(startOffset + i18nContent.length),
14439
+ start: context.sourceCode.getLocFromIndex(startOffset)
14440
+ },
14441
+ messageId: "invalidJson"
14442
+ });
14443
+ }
14444
+ }
14445
+ function getAllKeys(object, prefix = "") {
14446
+ let keys = [];
14447
+ for (const key in object) {
14448
+ const newPrefix = prefix ? `${prefix}.${key}` : key;
14449
+ if (typeof object[key] === "object" && object[key] !== null) {
14450
+ keys = [...keys, ...getAllKeys(object[key], newPrefix)];
14451
+ } else {
14452
+ keys.push(newPrefix);
14453
+ }
14454
+ }
14455
+ return keys;
14456
+ }
14457
+ }
14458
+ };
14459
+ }
14460
+ };
14461
+
14462
+ const rule$2 = {
14463
+ meta: {
14464
+ docs: {
14465
+ category: "Best Practices",
14466
+ description: "Enforce using t() instead of $t() in Vue templates",
14467
+ recommended: true
14468
+ },
14469
+ fixable: "code",
14470
+ messages: { useT: "Use t() instead of $t() in Vue templates as it does not work with <i18n> tags." },
14471
+ schema: [],
14472
+ type: "problem"
14473
+ },
14474
+ create(context) {
14475
+ if (!context.filename.endsWith(".vue")) {
14476
+ return {};
14477
+ }
14478
+ return {
14479
+ Program() {
14480
+ const sourceCode = context.sourceCode;
14481
+ const source = sourceCode.getText();
14482
+ const templateMatch = source.match(/<template>([\s\S]*)<\/template>/i);
14483
+ if (!templateMatch || templateMatch.index === void 0) {
14484
+ return;
14485
+ }
14486
+ const templateContent = templateMatch[1];
14487
+ const tPatterns = [
14488
+ {
14489
+ pattern: / \$t\(/g,
14490
+ quote: " "
14491
+ },
14492
+ {
14493
+ pattern: /"\$t\(/g,
14494
+ quote: '"'
14495
+ },
14496
+ {
14497
+ pattern: /`\$t\(/g,
14498
+ quote: "`"
14499
+ },
14500
+ {
14501
+ pattern: /\{\$t\(/g,
14502
+ quote: "{"
14503
+ },
14504
+ {
14505
+ pattern: /\[\$t\(/g,
14506
+ quote: "["
14507
+ }
14508
+ ];
14509
+ for (const { pattern, quote } of tPatterns) {
14510
+ let match;
14511
+ while ((match = pattern.exec(templateContent)) !== null) {
14512
+ const templateStart = templateMatch.index + templateMatch[0].indexOf(templateContent);
14513
+ const start = templateStart + match.index;
14514
+ const end = start + 4;
14515
+ context.report({
14516
+ fix(fixer) {
14517
+ return fixer.replaceTextRange([start, end], `${quote}t(`);
14518
+ },
14519
+ loc: {
14520
+ end: sourceCode.getLocFromIndex(end),
14521
+ start: sourceCode.getLocFromIndex(start)
14522
+ },
14523
+ messageId: "useT"
14524
+ });
14525
+ }
14526
+ }
14527
+ }
14528
+ };
14529
+ }
14530
+ };
14531
+
14532
+ const rule$1 = {
14533
+ meta: {
14534
+ docs: {
14535
+ category: "Best Practices",
14536
+ description: "Enforce consistent indentation and sorted keys in i18n blocks",
14537
+ recommended: true
14538
+ },
14539
+ fixable: "whitespace",
14540
+ messages: {
14541
+ indentError: "Invalid indentation for i18n content. Expected {{expected}} spaces.",
14542
+ invalidJson: "Invalid JSON in i18n block: {{error}}",
14543
+ sortError: "Keys in i18n block should be sorted alphabetically."
14544
+ },
14545
+ schema: [],
14546
+ type: "problem"
14547
+ },
14548
+ create(context) {
14549
+ if (!context.filename.endsWith(".vue")) {
14550
+ return {};
14551
+ }
14552
+ function sortObjectKeys(object) {
14553
+ if (Array.isArray(object)) {
14554
+ return object.map(sortObjectKeys);
14555
+ }
14556
+ if (object && typeof object === "object") {
14557
+ const sortedKeys = Object.keys(object).sort();
14558
+ const result = {};
14559
+ const obj = object;
14560
+ for (const key of sortedKeys) {
14561
+ result[key] = sortObjectKeys(obj[key]);
14562
+ }
14563
+ return result;
14564
+ }
14565
+ return object;
14566
+ }
14567
+ return {
14568
+ Program() {
14569
+ const source = context.sourceCode.getText();
14570
+ const i18nMatch = source.match(/(<i18n\s+lang=["']json["']>)([\s\S]*?)(<\/i18n>)/i);
14571
+ if (i18nMatch) {
14572
+ const startOffset = i18nMatch.index + i18nMatch[1].length;
14573
+ const i18nContent = i18nMatch[2];
14574
+ try {
14575
+ const parsed = JSON.parse(i18nContent.trim());
14576
+ const totalSpaces = 2;
14577
+ const sortedParsed = sortObjectKeys(parsed);
14578
+ const formattedContent = `
14579
+ ${JSON.stringify(sortedParsed, null, totalSpaces).replace(/\n\n+/g, "\n")}
14580
+ `;
14581
+ if (formattedContent.trim() !== i18nContent.trim()) {
14582
+ const currentKeys = JSON.stringify(parsed, null, totalSpaces).trim();
14583
+ const sortedKeys = JSON.stringify(sortedParsed, null, totalSpaces).trim();
14584
+ context.report({
14585
+ data: { expected: String(totalSpaces) },
14586
+ fix(fixer) {
14587
+ return fixer.replaceTextRange(
14588
+ [startOffset, startOffset + i18nContent.length],
14589
+ formattedContent
14590
+ );
14591
+ },
14592
+ loc: {
14593
+ end: context.sourceCode.getLocFromIndex(startOffset + i18nContent.length),
14594
+ start: context.sourceCode.getLocFromIndex(startOffset)
14595
+ },
14596
+ messageId: currentKeys !== sortedKeys ? "sortError" : "indentError"
14597
+ });
14598
+ }
14599
+ } catch (error) {
14600
+ const errorMessage = error instanceof Error ? error.message : String(error);
14601
+ context.report({
14602
+ data: { error: errorMessage },
14603
+ loc: {
14604
+ end: context.sourceCode.getLocFromIndex(startOffset + i18nContent.length),
14605
+ start: context.sourceCode.getLocFromIndex(startOffset)
14606
+ },
14607
+ messageId: "invalidJson"
14608
+ });
14609
+ }
14610
+ }
14611
+ }
14612
+ };
14613
+ }
14614
+ };
14615
+
14616
+ const rule = {
14617
+ meta: {
14618
+ docs: {
14619
+ category: "Best Practices",
14620
+ description: "Detect unused strings in the English locale",
14621
+ recommended: true
14622
+ },
14623
+ messages: {
14624
+ invalidJson: "Invalid JSON in i18n block: {{error}}",
14625
+ unusedString: 'Unused string in English locale: "{{key}}"'
14626
+ },
14627
+ schema: [],
14628
+ type: "problem"
14629
+ },
14630
+ create(context) {
14631
+ if (!context.filename.endsWith(".vue")) {
14632
+ return {};
14633
+ }
14634
+ return {
14635
+ Program() {
14636
+ const source = context.sourceCode.getText();
14637
+ const i18nMatch = source.match(/(<i18n\s+lang=["']json["']>)([\s\S]*?)(<\/i18n>)/i);
14638
+ if (!i18nMatch) {
14639
+ return;
14640
+ }
14641
+ const startOffset = i18nMatch.index + i18nMatch[1].length;
14642
+ const i18nContent = i18nMatch[2].trim();
14643
+ try {
14644
+ const parsed = JSON.parse(i18nContent);
14645
+ if (!parsed.en) {
14646
+ return;
14647
+ }
14648
+ const templateMatch = source.match(/<template>([\s\S]*)<\/template>/i);
14649
+ const templateContent = templateMatch ? templateMatch[1] : "";
14650
+ const scriptMatch = source.match(/<script[^>]*>([\s\S]*?)<\/script>/i);
14651
+ const scriptContent = scriptMatch ? scriptMatch[1] : "";
14652
+ const enKeys = getAllKeys(parsed.en);
14653
+ for (const key of enKeys) {
14654
+ const isUsed = templateContent.includes(key) || scriptContent.includes(key);
14655
+ if (!isUsed) {
14656
+ const keyMatch = new RegExp(`"${key}"\\s*:`, "g").exec(i18nContent);
14657
+ if (keyMatch) {
14658
+ const keyOffset = startOffset + keyMatch.index;
14659
+ context.report({
14660
+ data: { key },
14661
+ loc: {
14662
+ end: context.sourceCode.getLocFromIndex(keyOffset + key.length + 2),
14663
+ start: context.sourceCode.getLocFromIndex(keyOffset)
14664
+ },
14665
+ messageId: "unusedString"
14666
+ });
14667
+ }
14668
+ }
14669
+ }
14670
+ } catch (error) {
14671
+ const errorMessage = error instanceof Error ? error.message : String(error);
14672
+ context.report({
14673
+ data: { error: errorMessage },
14674
+ loc: {
14675
+ end: context.sourceCode.getLocFromIndex(startOffset + i18nContent.length),
14676
+ start: context.sourceCode.getLocFromIndex(startOffset)
14677
+ },
14678
+ messageId: "invalidJson"
14679
+ });
14680
+ }
14681
+ }
14682
+ };
14683
+ function getAllKeys(object, prefix = "") {
14684
+ let keys = [];
14685
+ for (const key in object) {
14686
+ const newPrefix = prefix ? `${prefix}.${key}` : key;
14687
+ if (typeof object[key] === "object" && object[key] !== null) {
14688
+ keys = [...keys, ...getAllKeys(object[key], newPrefix)];
14689
+ } else {
14690
+ keys.push(newPrefix);
14691
+ }
14692
+ }
14693
+ return keys;
14694
+ }
14695
+ }
14696
+ };
14697
+
14331
14698
  const index = {
14332
14699
  rules: {
14333
- "ts-multiline-ternary": rule$2,
14334
- "ts-multiline-union": rule$1,
14335
- "ts-sort-tests": rule
14336
- // 'vue-i18n-consistent-locales': vueI18nConsistentLocales,
14337
- // 'vue-i18n-consistent-t': vueI18nConsistentT,
14338
- // 'vue-i18n-sort-keys': vueI18nSortKeys,
14339
- // 'vue-i18n-unused-strings': vueI18nUnusedStrings,
14700
+ "ts-multiline-ternary": rule$6,
14701
+ "ts-multiline-union": rule$5,
14702
+ "ts-sort-tests": rule$4,
14703
+ "vue-i18n-consistent-locales": rule$3,
14704
+ "vue-i18n-consistent-t": rule$2,
14705
+ "vue-i18n-sort-keys": rule$1,
14706
+ "vue-i18n-unused-strings": rule
14340
14707
  // 'vue-script-format-computed': vueScriptFormatComputed,
14341
14708
  // 'vue-script-format-emits': vueScriptFormatEmits,
14342
14709
  // 'vue-script-format-props': vueScriptFormatProps,
package/dist/index.d.cts CHANGED
@@ -5,6 +5,10 @@ declare const _default: {
5
5
  'ts-multiline-ternary': eslint.Rule.RuleModule;
6
6
  'ts-multiline-union': eslint.Rule.RuleModule;
7
7
  'ts-sort-tests': eslint.Rule.RuleModule;
8
+ 'vue-i18n-consistent-locales': eslint.Rule.RuleModule;
9
+ 'vue-i18n-consistent-t': eslint.Rule.RuleModule;
10
+ 'vue-i18n-sort-keys': eslint.Rule.RuleModule;
11
+ 'vue-i18n-unused-strings': eslint.Rule.RuleModule;
8
12
  };
9
13
  };
10
14
 
package/dist/index.d.mts CHANGED
@@ -5,6 +5,10 @@ declare const _default: {
5
5
  'ts-multiline-ternary': eslint.Rule.RuleModule;
6
6
  'ts-multiline-union': eslint.Rule.RuleModule;
7
7
  'ts-sort-tests': eslint.Rule.RuleModule;
8
+ 'vue-i18n-consistent-locales': eslint.Rule.RuleModule;
9
+ 'vue-i18n-consistent-t': eslint.Rule.RuleModule;
10
+ 'vue-i18n-sort-keys': eslint.Rule.RuleModule;
11
+ 'vue-i18n-unused-strings': eslint.Rule.RuleModule;
8
12
  };
9
13
  };
10
14
 
package/dist/index.d.ts CHANGED
@@ -5,6 +5,10 @@ declare const _default: {
5
5
  'ts-multiline-ternary': eslint.Rule.RuleModule;
6
6
  'ts-multiline-union': eslint.Rule.RuleModule;
7
7
  'ts-sort-tests': eslint.Rule.RuleModule;
8
+ 'vue-i18n-consistent-locales': eslint.Rule.RuleModule;
9
+ 'vue-i18n-consistent-t': eslint.Rule.RuleModule;
10
+ 'vue-i18n-sort-keys': eslint.Rule.RuleModule;
11
+ 'vue-i18n-unused-strings': eslint.Rule.RuleModule;
8
12
  };
9
13
  };
10
14
 
package/dist/index.mjs CHANGED
@@ -14077,7 +14077,7 @@ function requireDist () {
14077
14077
 
14078
14078
  var distExports = requireDist();
14079
14079
 
14080
- const rule$2 = {
14080
+ const rule$6 = {
14081
14081
  meta: {
14082
14082
  docs: {
14083
14083
  category: "Stylistic Issues",
@@ -14140,7 +14140,7 @@ const rule$2 = {
14140
14140
  }
14141
14141
  };
14142
14142
 
14143
- const rule$1 = {
14143
+ const rule$5 = {
14144
14144
  meta: {
14145
14145
  docs: {
14146
14146
  category: "Stylistic Issues",
@@ -14206,7 +14206,7 @@ const rule$1 = {
14206
14206
  }
14207
14207
  };
14208
14208
 
14209
- const rule = {
14209
+ const rule$4 = {
14210
14210
  meta: {
14211
14211
  docs: {
14212
14212
  category: "Best Practices",
@@ -14321,15 +14321,382 @@ const rule = {
14321
14321
  }
14322
14322
  };
14323
14323
 
14324
+ const rule$3 = {
14325
+ meta: {
14326
+ docs: {
14327
+ category: "Possible Errors",
14328
+ description: "Enforce consistent i18n locale keys across translations",
14329
+ recommended: true
14330
+ },
14331
+ messages: {
14332
+ invalidJson: "Invalid JSON in i18n block: {{error}}",
14333
+ invalidLocale: "Invalid locale: {{locale}}. Allowed locales are: {{allowed}}",
14334
+ missingLocale: "Missing required locale: {{locale}}",
14335
+ missingTranslations: 'Missing translations in "{{locale}}" locale: {{missing}}'
14336
+ },
14337
+ schema: [
14338
+ {
14339
+ additionalProperties: false,
14340
+ properties: {
14341
+ locales: {
14342
+ items: { type: "string" },
14343
+ type: "array"
14344
+ }
14345
+ },
14346
+ type: "object"
14347
+ }
14348
+ ],
14349
+ type: "problem"
14350
+ },
14351
+ create(context) {
14352
+ if (!context.filename.endsWith(".vue")) {
14353
+ return {};
14354
+ }
14355
+ const locales = context.options[0]?.locales || ["en", "fr"];
14356
+ return {
14357
+ Program() {
14358
+ const source = context.sourceCode.getText();
14359
+ const i18nMatch = source.match(/(<i18n\s+lang=["']json["']>)([\s\S]*?)(<\/i18n>)/i);
14360
+ if (i18nMatch) {
14361
+ const startOffset = i18nMatch.index + i18nMatch[1].length;
14362
+ const i18nContent = i18nMatch[2].trim();
14363
+ try {
14364
+ const parsed = JSON.parse(i18nContent);
14365
+ for (const locale of locales) {
14366
+ if (!parsed[locale]) {
14367
+ context.report({
14368
+ data: { locale },
14369
+ loc: {
14370
+ end: context.sourceCode.getLocFromIndex(startOffset + i18nContent.length),
14371
+ start: context.sourceCode.getLocFromIndex(startOffset)
14372
+ },
14373
+ messageId: "missingLocale"
14374
+ });
14375
+ }
14376
+ }
14377
+ for (const locale of Object.keys(parsed)) {
14378
+ if (!locales.includes(locale)) {
14379
+ const localeMatch = new RegExp(`"${locale}"\\s*:`, "g").exec(i18nContent);
14380
+ if (localeMatch) {
14381
+ const localeOffset = startOffset + localeMatch.index;
14382
+ context.report({
14383
+ data: {
14384
+ allowed: locales.join(", "),
14385
+ locale
14386
+ },
14387
+ loc: {
14388
+ end: context.sourceCode.getLocFromIndex(localeOffset + locale.length + 2),
14389
+ start: context.sourceCode.getLocFromIndex(localeOffset)
14390
+ },
14391
+ messageId: "invalidLocale"
14392
+ });
14393
+ }
14394
+ }
14395
+ }
14396
+ const allKeys = /* @__PURE__ */ new Set();
14397
+ for (const locale of locales) {
14398
+ if (parsed[locale]) {
14399
+ const keys = getAllKeys(parsed[locale]);
14400
+ keys.forEach((key) => allKeys.add(key));
14401
+ }
14402
+ }
14403
+ for (const locale of locales) {
14404
+ if (parsed[locale]) {
14405
+ const localeKeys = getAllKeys(parsed[locale]);
14406
+ const missingKeys = [...allKeys].filter((key) => !localeKeys.includes(key));
14407
+ if (missingKeys.length > 0) {
14408
+ const localeMatch = new RegExp(`"${locale}"\\s*:\\s*{`, "g").exec(i18nContent);
14409
+ if (localeMatch) {
14410
+ const localeOffset = startOffset + localeMatch.index;
14411
+ context.report({
14412
+ data: {
14413
+ locale,
14414
+ missing: missingKeys.join(", ")
14415
+ },
14416
+ loc: {
14417
+ end: context.sourceCode.getLocFromIndex(localeOffset + locale.length + 2),
14418
+ start: context.sourceCode.getLocFromIndex(localeOffset)
14419
+ },
14420
+ messageId: "missingTranslations"
14421
+ });
14422
+ }
14423
+ }
14424
+ }
14425
+ }
14426
+ } catch (error) {
14427
+ const errorMessage = error instanceof Error ? error.message : String(error);
14428
+ context.report({
14429
+ data: { error: errorMessage },
14430
+ loc: {
14431
+ end: context.sourceCode.getLocFromIndex(startOffset + i18nContent.length),
14432
+ start: context.sourceCode.getLocFromIndex(startOffset)
14433
+ },
14434
+ messageId: "invalidJson"
14435
+ });
14436
+ }
14437
+ }
14438
+ function getAllKeys(object, prefix = "") {
14439
+ let keys = [];
14440
+ for (const key in object) {
14441
+ const newPrefix = prefix ? `${prefix}.${key}` : key;
14442
+ if (typeof object[key] === "object" && object[key] !== null) {
14443
+ keys = [...keys, ...getAllKeys(object[key], newPrefix)];
14444
+ } else {
14445
+ keys.push(newPrefix);
14446
+ }
14447
+ }
14448
+ return keys;
14449
+ }
14450
+ }
14451
+ };
14452
+ }
14453
+ };
14454
+
14455
+ const rule$2 = {
14456
+ meta: {
14457
+ docs: {
14458
+ category: "Best Practices",
14459
+ description: "Enforce using t() instead of $t() in Vue templates",
14460
+ recommended: true
14461
+ },
14462
+ fixable: "code",
14463
+ messages: { useT: "Use t() instead of $t() in Vue templates as it does not work with <i18n> tags." },
14464
+ schema: [],
14465
+ type: "problem"
14466
+ },
14467
+ create(context) {
14468
+ if (!context.filename.endsWith(".vue")) {
14469
+ return {};
14470
+ }
14471
+ return {
14472
+ Program() {
14473
+ const sourceCode = context.sourceCode;
14474
+ const source = sourceCode.getText();
14475
+ const templateMatch = source.match(/<template>([\s\S]*)<\/template>/i);
14476
+ if (!templateMatch || templateMatch.index === void 0) {
14477
+ return;
14478
+ }
14479
+ const templateContent = templateMatch[1];
14480
+ const tPatterns = [
14481
+ {
14482
+ pattern: / \$t\(/g,
14483
+ quote: " "
14484
+ },
14485
+ {
14486
+ pattern: /"\$t\(/g,
14487
+ quote: '"'
14488
+ },
14489
+ {
14490
+ pattern: /`\$t\(/g,
14491
+ quote: "`"
14492
+ },
14493
+ {
14494
+ pattern: /\{\$t\(/g,
14495
+ quote: "{"
14496
+ },
14497
+ {
14498
+ pattern: /\[\$t\(/g,
14499
+ quote: "["
14500
+ }
14501
+ ];
14502
+ for (const { pattern, quote } of tPatterns) {
14503
+ let match;
14504
+ while ((match = pattern.exec(templateContent)) !== null) {
14505
+ const templateStart = templateMatch.index + templateMatch[0].indexOf(templateContent);
14506
+ const start = templateStart + match.index;
14507
+ const end = start + 4;
14508
+ context.report({
14509
+ fix(fixer) {
14510
+ return fixer.replaceTextRange([start, end], `${quote}t(`);
14511
+ },
14512
+ loc: {
14513
+ end: sourceCode.getLocFromIndex(end),
14514
+ start: sourceCode.getLocFromIndex(start)
14515
+ },
14516
+ messageId: "useT"
14517
+ });
14518
+ }
14519
+ }
14520
+ }
14521
+ };
14522
+ }
14523
+ };
14524
+
14525
+ const rule$1 = {
14526
+ meta: {
14527
+ docs: {
14528
+ category: "Best Practices",
14529
+ description: "Enforce consistent indentation and sorted keys in i18n blocks",
14530
+ recommended: true
14531
+ },
14532
+ fixable: "whitespace",
14533
+ messages: {
14534
+ indentError: "Invalid indentation for i18n content. Expected {{expected}} spaces.",
14535
+ invalidJson: "Invalid JSON in i18n block: {{error}}",
14536
+ sortError: "Keys in i18n block should be sorted alphabetically."
14537
+ },
14538
+ schema: [],
14539
+ type: "problem"
14540
+ },
14541
+ create(context) {
14542
+ if (!context.filename.endsWith(".vue")) {
14543
+ return {};
14544
+ }
14545
+ function sortObjectKeys(object) {
14546
+ if (Array.isArray(object)) {
14547
+ return object.map(sortObjectKeys);
14548
+ }
14549
+ if (object && typeof object === "object") {
14550
+ const sortedKeys = Object.keys(object).sort();
14551
+ const result = {};
14552
+ const obj = object;
14553
+ for (const key of sortedKeys) {
14554
+ result[key] = sortObjectKeys(obj[key]);
14555
+ }
14556
+ return result;
14557
+ }
14558
+ return object;
14559
+ }
14560
+ return {
14561
+ Program() {
14562
+ const source = context.sourceCode.getText();
14563
+ const i18nMatch = source.match(/(<i18n\s+lang=["']json["']>)([\s\S]*?)(<\/i18n>)/i);
14564
+ if (i18nMatch) {
14565
+ const startOffset = i18nMatch.index + i18nMatch[1].length;
14566
+ const i18nContent = i18nMatch[2];
14567
+ try {
14568
+ const parsed = JSON.parse(i18nContent.trim());
14569
+ const totalSpaces = 2;
14570
+ const sortedParsed = sortObjectKeys(parsed);
14571
+ const formattedContent = `
14572
+ ${JSON.stringify(sortedParsed, null, totalSpaces).replace(/\n\n+/g, "\n")}
14573
+ `;
14574
+ if (formattedContent.trim() !== i18nContent.trim()) {
14575
+ const currentKeys = JSON.stringify(parsed, null, totalSpaces).trim();
14576
+ const sortedKeys = JSON.stringify(sortedParsed, null, totalSpaces).trim();
14577
+ context.report({
14578
+ data: { expected: String(totalSpaces) },
14579
+ fix(fixer) {
14580
+ return fixer.replaceTextRange(
14581
+ [startOffset, startOffset + i18nContent.length],
14582
+ formattedContent
14583
+ );
14584
+ },
14585
+ loc: {
14586
+ end: context.sourceCode.getLocFromIndex(startOffset + i18nContent.length),
14587
+ start: context.sourceCode.getLocFromIndex(startOffset)
14588
+ },
14589
+ messageId: currentKeys !== sortedKeys ? "sortError" : "indentError"
14590
+ });
14591
+ }
14592
+ } catch (error) {
14593
+ const errorMessage = error instanceof Error ? error.message : String(error);
14594
+ context.report({
14595
+ data: { error: errorMessage },
14596
+ loc: {
14597
+ end: context.sourceCode.getLocFromIndex(startOffset + i18nContent.length),
14598
+ start: context.sourceCode.getLocFromIndex(startOffset)
14599
+ },
14600
+ messageId: "invalidJson"
14601
+ });
14602
+ }
14603
+ }
14604
+ }
14605
+ };
14606
+ }
14607
+ };
14608
+
14609
+ const rule = {
14610
+ meta: {
14611
+ docs: {
14612
+ category: "Best Practices",
14613
+ description: "Detect unused strings in the English locale",
14614
+ recommended: true
14615
+ },
14616
+ messages: {
14617
+ invalidJson: "Invalid JSON in i18n block: {{error}}",
14618
+ unusedString: 'Unused string in English locale: "{{key}}"'
14619
+ },
14620
+ schema: [],
14621
+ type: "problem"
14622
+ },
14623
+ create(context) {
14624
+ if (!context.filename.endsWith(".vue")) {
14625
+ return {};
14626
+ }
14627
+ return {
14628
+ Program() {
14629
+ const source = context.sourceCode.getText();
14630
+ const i18nMatch = source.match(/(<i18n\s+lang=["']json["']>)([\s\S]*?)(<\/i18n>)/i);
14631
+ if (!i18nMatch) {
14632
+ return;
14633
+ }
14634
+ const startOffset = i18nMatch.index + i18nMatch[1].length;
14635
+ const i18nContent = i18nMatch[2].trim();
14636
+ try {
14637
+ const parsed = JSON.parse(i18nContent);
14638
+ if (!parsed.en) {
14639
+ return;
14640
+ }
14641
+ const templateMatch = source.match(/<template>([\s\S]*)<\/template>/i);
14642
+ const templateContent = templateMatch ? templateMatch[1] : "";
14643
+ const scriptMatch = source.match(/<script[^>]*>([\s\S]*?)<\/script>/i);
14644
+ const scriptContent = scriptMatch ? scriptMatch[1] : "";
14645
+ const enKeys = getAllKeys(parsed.en);
14646
+ for (const key of enKeys) {
14647
+ const isUsed = templateContent.includes(key) || scriptContent.includes(key);
14648
+ if (!isUsed) {
14649
+ const keyMatch = new RegExp(`"${key}"\\s*:`, "g").exec(i18nContent);
14650
+ if (keyMatch) {
14651
+ const keyOffset = startOffset + keyMatch.index;
14652
+ context.report({
14653
+ data: { key },
14654
+ loc: {
14655
+ end: context.sourceCode.getLocFromIndex(keyOffset + key.length + 2),
14656
+ start: context.sourceCode.getLocFromIndex(keyOffset)
14657
+ },
14658
+ messageId: "unusedString"
14659
+ });
14660
+ }
14661
+ }
14662
+ }
14663
+ } catch (error) {
14664
+ const errorMessage = error instanceof Error ? error.message : String(error);
14665
+ context.report({
14666
+ data: { error: errorMessage },
14667
+ loc: {
14668
+ end: context.sourceCode.getLocFromIndex(startOffset + i18nContent.length),
14669
+ start: context.sourceCode.getLocFromIndex(startOffset)
14670
+ },
14671
+ messageId: "invalidJson"
14672
+ });
14673
+ }
14674
+ }
14675
+ };
14676
+ function getAllKeys(object, prefix = "") {
14677
+ let keys = [];
14678
+ for (const key in object) {
14679
+ const newPrefix = prefix ? `${prefix}.${key}` : key;
14680
+ if (typeof object[key] === "object" && object[key] !== null) {
14681
+ keys = [...keys, ...getAllKeys(object[key], newPrefix)];
14682
+ } else {
14683
+ keys.push(newPrefix);
14684
+ }
14685
+ }
14686
+ return keys;
14687
+ }
14688
+ }
14689
+ };
14690
+
14324
14691
  const index = {
14325
14692
  rules: {
14326
- "ts-multiline-ternary": rule$2,
14327
- "ts-multiline-union": rule$1,
14328
- "ts-sort-tests": rule
14329
- // 'vue-i18n-consistent-locales': vueI18nConsistentLocales,
14330
- // 'vue-i18n-consistent-t': vueI18nConsistentT,
14331
- // 'vue-i18n-sort-keys': vueI18nSortKeys,
14332
- // 'vue-i18n-unused-strings': vueI18nUnusedStrings,
14693
+ "ts-multiline-ternary": rule$6,
14694
+ "ts-multiline-union": rule$5,
14695
+ "ts-sort-tests": rule$4,
14696
+ "vue-i18n-consistent-locales": rule$3,
14697
+ "vue-i18n-consistent-t": rule$2,
14698
+ "vue-i18n-sort-keys": rule$1,
14699
+ "vue-i18n-unused-strings": rule
14333
14700
  // 'vue-script-format-computed': vueScriptFormatComputed,
14334
14701
  // 'vue-script-format-emits': vueScriptFormatEmits,
14335
14702
  // 'vue-script-format-props': vueScriptFormatProps,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@saasmakers/eslint",
3
3
  "type": "module",
4
- "version": "0.1.3",
4
+ "version": "0.1.4",
5
5
  "private": false,
6
6
  "description": "Shared ESLint config for SaaS Makers projects",
7
7
  "license": "MIT",