@let-value/translate-extract 1.0.9 → 1.0.10

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/src/index.js CHANGED
@@ -1,16 +1,58 @@
1
- import { run } from "../run-BMvkoNJ4.js";
1
+ import { run } from "../run-CMzN4vHt.js";
2
2
  import path, { basename, dirname, extname, join, relative, resolve } from "node:path";
3
- import { globSync } from "glob";
4
3
  import fs, { readFile } from "node:fs/promises";
4
+ import * as gettextParser from "gettext-parser";
5
5
  import fs$1 from "node:fs";
6
- import { memo } from "radash";
7
6
  import Parser from "tree-sitter";
8
7
  import JavaScript from "tree-sitter-javascript";
9
8
  import TS from "tree-sitter-typescript";
10
9
  import { ResolverFactory } from "oxc-resolver";
11
- import * as gettextParser from "gettext-parser";
12
10
  import { getFormula, getNPlurals } from "plural-forms";
13
11
 
12
+ //#region src/plugins/cleanup/cleanup.ts
13
+ function cleanup() {
14
+ return {
15
+ name: "cleanup",
16
+ setup(build) {
17
+ build.context.logger?.debug("cleanup plugin initialized");
18
+ const processedDirs = /* @__PURE__ */ new Set();
19
+ const generated = /* @__PURE__ */ new Set();
20
+ build.onResolve({
21
+ namespace: "cleanup",
22
+ filter: /.*/
23
+ }, (args) => {
24
+ generated.add(args.path);
25
+ return args;
26
+ });
27
+ build.onProcess({
28
+ namespace: "cleanup",
29
+ filter: /.*/
30
+ }, async ({ path: path$1 }) => {
31
+ await build.defer("translate");
32
+ const dir = dirname(path$1);
33
+ if (processedDirs.has(dir)) return void 0;
34
+ processedDirs.add(dir);
35
+ const files = await fs.readdir(dir).catch(() => []);
36
+ for (const f of files.filter((p) => p.endsWith(".po"))) {
37
+ const full = join(dir, f);
38
+ if (generated.has(full)) continue;
39
+ const contents = await fs.readFile(full).catch(() => void 0);
40
+ if (!contents) continue;
41
+ const parsed = gettextParser.po.parse(contents);
42
+ const hasTranslations = Object.entries(parsed.translations || {}).some(([ctx, msgs]) => Object.keys(msgs).some((id) => !(ctx === "" && id === "")));
43
+ if (hasTranslations) build.context.logger?.warn({ path: full }, "stray translation file");
44
+ else {
45
+ await fs.unlink(full);
46
+ build.context.logger?.info({ path: full }, "removed empty translation file");
47
+ }
48
+ }
49
+ return void 0;
50
+ });
51
+ }
52
+ };
53
+ }
54
+
55
+ //#endregion
14
56
  //#region src/plugins/core/queries/comment.ts
15
57
  function getReference(node, { path: path$1 }) {
16
58
  const line = node.startPosition.row + 1;
@@ -390,7 +432,7 @@ const pluralQuery$1 = withComment({
390
432
 
391
433
  //#endregion
392
434
  //#region src/plugins/core/queries/index.ts
393
- const queries = [
435
+ const queries$1 = [
394
436
  messageQuery$1,
395
437
  messageInvalidQuery,
396
438
  gettextQuery,
@@ -413,28 +455,52 @@ function getLanguage(ext) {
413
455
  default: return JavaScript;
414
456
  }
415
457
  }
416
- const getCachedParser = memo(function getCachedParser$1(ext) {
417
- const parser = new Parser();
418
- const language = getLanguage(ext);
419
- parser.setLanguage(language);
420
- return {
421
- parser,
422
- language
423
- };
424
- });
458
+ const parserCache = /* @__PURE__ */ new Map();
459
+ const queryCache = /* @__PURE__ */ new WeakMap();
460
+ function getCachedParser(ext) {
461
+ let cached = parserCache.get(ext);
462
+ if (!cached) {
463
+ const parser = new Parser();
464
+ const language = getLanguage(ext);
465
+ parser.setLanguage(language);
466
+ cached = {
467
+ parser,
468
+ language
469
+ };
470
+ parserCache.set(ext, cached);
471
+ }
472
+ return cached;
473
+ }
474
+ function getCachedQuery(language, pattern) {
475
+ let cache = queryCache.get(language);
476
+ if (!cache) {
477
+ cache = /* @__PURE__ */ new Map();
478
+ queryCache.set(language, cache);
479
+ }
480
+ let query = cache.get(pattern);
481
+ if (!query) {
482
+ query = new Parser.Query(language, pattern);
483
+ cache.set(pattern, query);
484
+ }
485
+ return query;
486
+ }
425
487
  function getParser(path$1) {
426
488
  const ext = extname(path$1);
427
489
  return getCachedParser(ext);
428
490
  }
491
+ function getQuery(language, pattern) {
492
+ return getCachedQuery(language, pattern);
493
+ }
429
494
  function parseSource$1(source, path$1) {
430
495
  const context = { path: path$1 };
431
496
  const { parser, language } = getParser(path$1);
432
497
  const tree = parser.parse(source);
433
498
  const translations = [];
499
+ const warnings = [];
434
500
  const imports = [];
435
501
  const seen = /* @__PURE__ */ new Set();
436
- for (const spec of queries) {
437
- const query = new Parser.Query(language, spec.pattern);
502
+ for (const spec of queries$1) {
503
+ const query = getCachedQuery(language, spec.pattern);
438
504
  for (const match of query.matches(tree.rootNode)) {
439
505
  const message = spec.extract(match);
440
506
  if (!message) continue;
@@ -449,17 +515,21 @@ function parseSource$1(source, path$1) {
449
515
  reference
450
516
  }
451
517
  });
452
- if (error) console.warn(`Parsing error at ${reference}: ${error}`);
518
+ if (error) warnings.push({
519
+ error,
520
+ reference
521
+ });
453
522
  }
454
523
  }
455
- const importTreeQuery = new Parser.Query(language, importQuery.pattern);
524
+ const importTreeQuery = getCachedQuery(language, importQuery.pattern);
456
525
  for (const match of importTreeQuery.matches(tree.rootNode)) {
457
526
  const imp = importQuery.extract(match);
458
527
  if (imp) imports.push(imp);
459
528
  }
460
529
  return {
461
530
  translations,
462
- imports
531
+ imports,
532
+ warnings
463
533
  };
464
534
  }
465
535
 
@@ -516,39 +586,55 @@ function resolveImports(file, imports) {
516
586
  //#endregion
517
587
  //#region src/plugins/core/core.ts
518
588
  const filter$1 = /\.([cm]?tsx?|jsx?)$/;
589
+ const namespace$1 = "source";
519
590
  function core() {
520
591
  return {
521
592
  name: "core",
522
593
  setup(build) {
523
594
  build.context.logger?.debug("core plugin initialized");
524
- build.onResolve({ filter: /.*/ }, ({ entrypoint, path: path$1 }) => {
595
+ build.onResolve({
596
+ filter: filter$1,
597
+ namespace: namespace$1
598
+ }, ({ entrypoint, path: path$1 }) => {
525
599
  return {
526
600
  entrypoint,
601
+ namespace: namespace$1,
527
602
  path: resolve(path$1)
528
603
  };
529
604
  });
530
- build.onLoad({ filter: filter$1 }, async ({ entrypoint, path: path$1 }) => {
531
- const contents = await readFile(path$1, "utf8");
605
+ build.onLoad({
606
+ filter: filter$1,
607
+ namespace: namespace$1
608
+ }, async ({ entrypoint, path: path$1 }) => {
609
+ const data = await readFile(path$1, "utf8");
532
610
  return {
533
611
  entrypoint,
534
612
  path: path$1,
535
- contents
613
+ namespace: namespace$1,
614
+ data
536
615
  };
537
616
  });
538
- build.onExtract({ filter: filter$1 }, ({ entrypoint, path: path$1, contents }) => {
539
- const { translations, imports } = parseSource$1(contents, path$1);
617
+ build.onProcess({
618
+ filter: filter$1,
619
+ namespace: namespace$1
620
+ }, ({ entrypoint, path: path$1, data }) => {
621
+ const { translations, imports, warnings } = parseSource$1(data, path$1);
540
622
  if (build.context.config.walk) {
541
623
  const paths = resolveImports(path$1, imports);
542
- for (const path$2 of paths) build.resolvePath({
624
+ for (const path$2 of paths) build.resolve({
543
625
  entrypoint,
544
- path: path$2
626
+ path: path$2,
627
+ namespace: namespace$1
545
628
  });
546
629
  }
547
- return {
630
+ for (const warning of warnings) build.context.logger?.warn(`${warning.error} at ${warning.reference}`);
631
+ build.resolve({
548
632
  entrypoint,
549
633
  path: path$1,
550
- translations
551
- };
634
+ namespace: "translate",
635
+ data: translations
636
+ });
637
+ return void 0;
552
638
  });
553
639
  }
554
640
  };
@@ -682,26 +768,74 @@ function merge(sources, existing, obsolete, locale, generatedAt) {
682
768
  };
683
769
  return gettextParser.po.compile(poObj).toString();
684
770
  }
771
+ const namespace = "translate";
685
772
  function po() {
686
773
  return {
687
774
  name: "po",
688
775
  setup(build) {
689
776
  build.context.logger?.debug("po plugin initialized");
690
- build.onCollect({ filter: /.*/ }, ({ entrypoint, translations, destination,...rest }, ctx) => {
691
- const record = collect(translations, ctx.locale);
692
- const redirected = join(dirname(destination), `${basename(destination, extname(destination))}.po`);
777
+ const collections = /* @__PURE__ */ new Map();
778
+ let dispatched = false;
779
+ build.onResolve({
780
+ filter: /.*/,
781
+ namespace
782
+ }, async ({ entrypoint, path: path$1, data }) => {
783
+ if (!data || !Array.isArray(data)) return void 0;
784
+ for (const locale of build.context.config.locales) {
785
+ const destination = build.context.config.destination({
786
+ entrypoint,
787
+ locale,
788
+ path: path$1
789
+ });
790
+ if (!collections.has(destination)) collections.set(destination, {
791
+ locale,
792
+ translations: []
793
+ });
794
+ collections.get(destination)?.translations.push(...data);
795
+ }
796
+ build.defer("source").then(() => {
797
+ if (dispatched) return;
798
+ dispatched = true;
799
+ for (const path$2 of collections.keys()) build.load({
800
+ entrypoint,
801
+ path: path$2,
802
+ namespace
803
+ });
804
+ });
805
+ return void 0;
806
+ });
807
+ build.onLoad({
808
+ filter: /.*\.po$/,
809
+ namespace
810
+ }, async ({ entrypoint, path: path$1 }) => {
811
+ const data = await fs.readFile(path$1).catch(() => void 0);
693
812
  return {
694
- ...rest,
695
813
  entrypoint,
696
- destination: redirected,
697
- translations: record
814
+ path: path$1,
815
+ namespace,
816
+ data
698
817
  };
699
818
  });
700
- build.onGenerate({ filter: /\.po$/ }, async ({ path: path$1, collected }, ctx) => {
701
- const existing = await fs.readFile(path$1).catch(() => void 0);
702
- const out = merge(collected, existing, ctx.config.obsolete, ctx.locale, ctx.generatedAt);
819
+ build.onProcess({
820
+ filter: /.*\.po$/,
821
+ namespace
822
+ }, async ({ entrypoint, path: path$1, data }) => {
823
+ const collected = collections.get(path$1);
824
+ if (!collected) {
825
+ build.context.logger?.warn({ path: path$1 }, "no translations collected for this path");
826
+ return void 0;
827
+ }
828
+ const { locale, translations } = collected;
829
+ const record = collect(translations, locale);
830
+ const out = merge([{ translations: record }], data, build.context.config.obsolete, locale, build.context.generatedAt);
703
831
  await fs.mkdir(dirname(path$1), { recursive: true });
704
832
  await fs.writeFile(path$1, out);
833
+ build.resolve({
834
+ entrypoint,
835
+ path: path$1,
836
+ namespace: "cleanup",
837
+ data: translations
838
+ });
705
839
  });
706
840
  }
707
841
  };
@@ -711,7 +845,8 @@ function po() {
711
845
  //#region src/configuration.ts
712
846
  const defaultPlugins = {
713
847
  core,
714
- po
848
+ po,
849
+ cleanup
715
850
  };
716
851
  const defaultDestination = ({ entrypoint, locale }) => join(dirname(entrypoint), "translations", `${basename(entrypoint, extname(entrypoint))}.${locale}.po`);
717
852
  const defaultExclude = [
@@ -723,52 +858,40 @@ function normalizeExclude(exclude) {
723
858
  if (!exclude) return [];
724
859
  return Array.isArray(exclude) ? exclude : [exclude];
725
860
  }
861
+ function resolveEntrypoint(ep) {
862
+ if (typeof ep === "string") return { entrypoint: ep };
863
+ const { entrypoint, destination, obsolete, exclude } = ep;
864
+ return {
865
+ entrypoint,
866
+ destination,
867
+ obsolete,
868
+ exclude: exclude ? normalizeExclude(exclude) : void 0
869
+ };
870
+ }
871
+ function resolvePlugins(user) {
872
+ if (typeof user === "function") return user(defaultPlugins);
873
+ if (Array.isArray(user)) return [...Object.values(defaultPlugins).map((plugin) => plugin()), ...user];
874
+ return Object.values(defaultPlugins).map((plugin) => plugin());
875
+ }
876
+ /**
877
+ * Type helper to make it easier to use translate.config.ts
878
+ * @param config - {@link UserConfig}.
879
+ */
726
880
  function defineConfig(config) {
727
- let plugins;
728
- const user = config.plugins;
729
- if (typeof user === "function") plugins = user(defaultPlugins);
730
- else if (Array.isArray(user)) plugins = [...Object.values(defaultPlugins).map((plugin) => plugin()), ...user];
731
- else plugins = Object.values(defaultPlugins).map((plugin) => plugin());
732
- const raw = Array.isArray(config.entrypoints) ? config.entrypoints : [config.entrypoints];
733
- const entrypoints = [];
734
- for (const ep of raw) if (typeof ep === "string") {
735
- const paths = globSync(ep, { nodir: true });
736
- if (paths.length === 0) entrypoints.push({ entrypoint: ep });
737
- else for (const path$1 of paths) entrypoints.push({ entrypoint: path$1 });
738
- } else {
739
- const { entrypoint, destination: destination$1, obsolete: obsolete$1, exclude: exclude$1 } = ep;
740
- const paths = globSync(entrypoint, { nodir: true });
741
- const epExclude = exclude$1 ? [...defaultExclude, ...normalizeExclude(exclude$1)] : void 0;
742
- if (paths.length === 0) entrypoints.push({
743
- entrypoint,
744
- destination: destination$1,
745
- obsolete: obsolete$1,
746
- exclude: epExclude
747
- });
748
- else for (const path$1 of paths) entrypoints.push({
749
- entrypoint: path$1,
750
- destination: destination$1,
751
- obsolete: obsolete$1,
752
- exclude: epExclude
753
- });
754
- }
755
881
  const defaultLocale = config.defaultLocale ?? "en";
756
- const locales = config.locales ?? [defaultLocale];
757
- const destination = config.destination ?? defaultDestination;
758
- const obsolete = config.obsolete ?? "mark";
759
- const walk = config.walk ?? true;
760
- const logLevel = config.logLevel ?? "info";
761
- const exclude = [...defaultExclude, ...normalizeExclude(config.exclude)];
882
+ const plugins = resolvePlugins(config.plugins);
883
+ const raw = Array.isArray(config.entrypoints) ? config.entrypoints : [config.entrypoints];
884
+ const entrypoints = raw.map(resolveEntrypoint);
762
885
  return {
763
886
  plugins,
764
887
  entrypoints,
765
888
  defaultLocale,
766
- locales,
767
- destination,
768
- obsolete,
769
- walk,
770
- logLevel,
771
- exclude
889
+ locales: config.locales ?? [defaultLocale],
890
+ destination: config.destination ?? defaultDestination,
891
+ obsolete: config.obsolete ?? "mark",
892
+ walk: config.walk ?? true,
893
+ logLevel: config.logLevel ?? "info",
894
+ exclude: config.exclude ? normalizeExclude(config.exclude) : defaultExclude
772
895
  };
773
896
  }
774
897
 
@@ -787,12 +910,26 @@ function buildTemplate(node) {
787
910
  if (text$1) strings[strings.length - 1] += text$1;
788
911
  } else if (child.type === "jsx_expression") {
789
912
  const expr = child.namedChildren[0];
790
- if (!expr || expr.type !== "identifier") return {
913
+ if (!expr) return {
791
914
  text: "",
792
- error: "JSX expressions must be simple identifiers"
915
+ error: "Empty JSX expression"
916
+ };
917
+ if (expr.type === "identifier") {
918
+ values.push(expr.text);
919
+ strings.push("");
920
+ } else if (expr.type === "string") strings[strings.length - 1] += expr.text.slice(1, -1);
921
+ else if (expr.type === "template_string") {
922
+ const hasSubstitutions = expr.children.some((c) => c.type === "template_substitution");
923
+ if (hasSubstitutions) return {
924
+ text: "",
925
+ error: "JSX expressions with template substitutions are not supported"
926
+ };
927
+ const content = expr.text.slice(1, -1);
928
+ strings[strings.length - 1] += content;
929
+ } else return {
930
+ text: "",
931
+ error: "JSX expressions must be simple identifiers, strings, or template literals"
793
932
  };
794
- values.push(expr.text);
795
- strings.push("");
796
933
  } else if (child.type === "string") strings[strings.length - 1] += child.text.slice(1, -1);
797
934
  else return {
798
935
  text: "",
@@ -810,11 +947,24 @@ function buildAttrValue(node) {
810
947
  if (node.type === "string") return { text: node.text.slice(1, -1) };
811
948
  if (node.type === "jsx_expression") {
812
949
  const expr = node.namedChildren[0];
813
- if (!expr || expr.type !== "identifier") return {
950
+ if (!expr) return {
814
951
  text: "",
815
- error: "JSX expressions must be simple identifiers"
952
+ error: "Empty JSX expression"
953
+ };
954
+ if (expr.type === "identifier") return { text: `\${${expr.text}}` };
955
+ else if (expr.type === "string") return { text: expr.text.slice(1, -1) };
956
+ else if (expr.type === "template_string") {
957
+ const hasSubstitutions = expr.children.some((c) => c.type === "template_substitution");
958
+ if (hasSubstitutions) return {
959
+ text: "",
960
+ error: "JSX expressions with template substitutions are not supported"
961
+ };
962
+ const content = expr.text.slice(1, -1);
963
+ return { text: content };
964
+ } else return {
965
+ text: "",
966
+ error: "JSX expressions must be simple identifiers, strings, or template literals"
816
967
  };
817
- return { text: `\${${expr.text}}` };
818
968
  }
819
969
  return {
820
970
  text: "",
@@ -827,9 +977,17 @@ function buildAttrValue(node) {
827
977
  const messageQuery = withComment({
828
978
  pattern: `(
829
979
  [
830
- (jsx_element (jsx_opening_element name: (identifier) @name))
831
- (jsx_self_closing_element name: (identifier) @name)
832
- ] @call
980
+ (jsx_element (jsx_opening_element name: (identifier) @name)) @call
981
+ (jsx_self_closing_element name: (identifier) @name) @call
982
+ (lexical_declaration
983
+ (variable_declarator
984
+ value: [
985
+ (jsx_element (jsx_opening_element name: (identifier) @name)) @call
986
+ (jsx_self_closing_element name: (identifier) @name) @call
987
+ ]
988
+ )
989
+ )
990
+ ]
833
991
  (#eq? @name "Message")
834
992
  )`,
835
993
  extract(match) {
@@ -942,7 +1100,7 @@ const pluralQuery = withComment({
942
1100
 
943
1101
  //#endregion
944
1102
  //#region src/plugins/react/queries/index.ts
945
- const queries$1 = [messageQuery, pluralQuery];
1103
+ const queries = [messageQuery, pluralQuery];
946
1104
 
947
1105
  //#endregion
948
1106
  //#region src/plugins/react/parse.ts
@@ -951,10 +1109,10 @@ function parseSource(source, path$1) {
951
1109
  const { parser, language } = getParser(path$1);
952
1110
  const tree = parser.parse(source);
953
1111
  const translations = [];
954
- const imports = [];
1112
+ const warnings = [];
955
1113
  const seen = /* @__PURE__ */ new Set();
956
- for (const spec of [...queries, ...queries$1]) {
957
- const query = new Parser.Query(language, spec.pattern);
1114
+ for (const spec of queries) {
1115
+ const query = getQuery(language, spec.pattern);
958
1116
  for (const match of query.matches(tree.rootNode)) {
959
1117
  const message = spec.extract(match);
960
1118
  if (!message) continue;
@@ -969,17 +1127,15 @@ function parseSource(source, path$1) {
969
1127
  reference
970
1128
  }
971
1129
  });
972
- if (error) console.warn(`Parsing error at ${reference}: ${error}`);
1130
+ if (error) warnings.push({
1131
+ error,
1132
+ reference
1133
+ });
973
1134
  }
974
1135
  }
975
- const importTreeQuery = new Parser.Query(language, importQuery.pattern);
976
- for (const match of importTreeQuery.matches(tree.rootNode)) {
977
- const imp = importQuery.extract(match);
978
- if (imp) imports.push(imp);
979
- }
980
1136
  return {
981
1137
  translations,
982
- imports
1138
+ warnings
983
1139
  };
984
1140
  }
985
1141
 
@@ -991,39 +1147,46 @@ function react() {
991
1147
  name: "react",
992
1148
  setup(build) {
993
1149
  build.context.logger?.debug("react plugin initialized");
994
- build.onResolve({ filter: /.*/ }, ({ entrypoint, path: path$1 }) => {
1150
+ build.onResolve({
1151
+ filter: /.*/,
1152
+ namespace: "source"
1153
+ }, ({ entrypoint, path: path$1, namespace: namespace$2 }) => {
995
1154
  return {
996
1155
  entrypoint,
1156
+ namespace: namespace$2,
997
1157
  path: resolve(path$1)
998
1158
  };
999
1159
  });
1000
- build.onLoad({ filter }, async ({ entrypoint, path: path$1 }) => {
1001
- const contents = await readFile(path$1, "utf8");
1160
+ build.onLoad({
1161
+ filter,
1162
+ namespace: "source"
1163
+ }, async ({ entrypoint, path: path$1, namespace: namespace$2 }) => {
1164
+ const data = await readFile(path$1, "utf8");
1002
1165
  return {
1003
1166
  entrypoint,
1004
1167
  path: path$1,
1005
- contents
1168
+ namespace: namespace$2,
1169
+ data
1006
1170
  };
1007
1171
  });
1008
- build.onExtract({ filter }, ({ entrypoint, path: path$1, contents }) => {
1009
- const { translations, imports } = parseSource(contents, path$1);
1010
- if (build.context.config.walk) {
1011
- const paths = resolveImports(path$1, imports);
1012
- for (const p of paths) build.resolvePath({
1013
- entrypoint,
1014
- path: p
1015
- });
1016
- }
1017
- return {
1172
+ build.onProcess({
1173
+ filter,
1174
+ namespace: "source"
1175
+ }, ({ entrypoint, path: path$1, data }) => {
1176
+ const { translations, warnings } = parseSource(data, path$1);
1177
+ for (const warning of warnings) build.context.logger?.warn(`${warning.error} at ${warning.reference}`);
1178
+ build.resolve({
1018
1179
  entrypoint,
1019
1180
  path: path$1,
1020
- translations
1021
- };
1181
+ namespace: "translate",
1182
+ data: translations
1183
+ });
1184
+ return void 0;
1022
1185
  });
1023
1186
  }
1024
1187
  };
1025
1188
  }
1026
1189
 
1027
1190
  //#endregion
1028
- export { collect, core, defineConfig, formatDate, merge, po, react, run };
1191
+ export { cleanup, collect, core, defineConfig, formatDate, merge, po, react, run };
1029
1192
  //# sourceMappingURL=index.js.map