react-autolocale 0.3.0-beta.1 → 0.5.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.
- package/README.md +110 -32
- package/dist/cli.js +12 -10
- package/dist/{config-U-SpTtCW.js → config-ChdN65Rg.js} +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/index.d.ts +13 -1
- package/dist/index.js +47 -4
- package/dist/{project-BazDFtnp.js → project-DcYRIvym.js} +953 -93
- package/dist/vite.js +34 -6
- package/package.json +3 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { t as BANNER_EN } from "./suggestion-CkZQqMvZ.js";
|
|
2
|
-
import { r as PROVIDER_NAMES } from "./config-
|
|
3
|
-
import path from "node:path";
|
|
2
|
+
import { r as PROVIDER_NAMES } from "./config-ChdN65Rg.js";
|
|
4
3
|
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
5
|
import { spawnSync } from "node:child_process";
|
|
6
6
|
import { createRequire } from "node:module";
|
|
7
7
|
import os from "node:os";
|
|
@@ -10,8 +10,8 @@ import { createHash } from "node:crypto";
|
|
|
10
10
|
import { JSDOM } from "jsdom";
|
|
11
11
|
import { parse } from "@babel/parser";
|
|
12
12
|
import _traverse from "@babel/traverse";
|
|
13
|
-
import _generate from "@babel/generator";
|
|
14
13
|
import * as t from "@babel/types";
|
|
14
|
+
import _generate from "@babel/generator";
|
|
15
15
|
const byCode = new Map([
|
|
16
16
|
{
|
|
17
17
|
code: "en",
|
|
@@ -661,8 +661,208 @@ function translateHead(doc, dict) {
|
|
|
661
661
|
}
|
|
662
662
|
}
|
|
663
663
|
//#endregion
|
|
664
|
+
//#region src/core/walk.ts
|
|
665
|
+
const EXTENSIONS = /* @__PURE__ */ new Set([
|
|
666
|
+
".tsx",
|
|
667
|
+
".jsx",
|
|
668
|
+
".ts",
|
|
669
|
+
".js"
|
|
670
|
+
]);
|
|
671
|
+
const IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
672
|
+
"node_modules",
|
|
673
|
+
"dist",
|
|
674
|
+
"build",
|
|
675
|
+
"coverage"
|
|
676
|
+
]);
|
|
677
|
+
function isSourceFile(file) {
|
|
678
|
+
return EXTENSIONS.has(path.extname(file)) && !file.endsWith(".d.ts") && !/\.(test|spec|stories|story)\./.test(file);
|
|
679
|
+
}
|
|
680
|
+
/** Source files under `dir`. `skip` lists absolute folders to leave out (the generated catalogs, internal metadata). */
|
|
681
|
+
function* walk(dir, skip = [], matches = isSourceFile) {
|
|
682
|
+
if (!fs.existsSync(dir)) return;
|
|
683
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
684
|
+
if (entry.name.startsWith(".") || IGNORED_DIRS.has(entry.name)) continue;
|
|
685
|
+
const full = path.join(dir, entry.name);
|
|
686
|
+
if (skip.some((d) => path.resolve(d) === full)) continue;
|
|
687
|
+
if (entry.isDirectory()) yield* walk(full, skip, matches);
|
|
688
|
+
else if (matches(full)) yield full;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
//#endregion
|
|
692
|
+
//#region src/core/classify.ts
|
|
693
|
+
/**
|
|
694
|
+
* Decides whether a string written in code is text a person reads, or a code value (a URL, a class name, a CSS
|
|
695
|
+
* value, an identifier, ...). Text in JSX and in known display attributes is always translated; this is for
|
|
696
|
+
* strings found in data, props, ternaries and the like, where being wrong in either direction matters.
|
|
697
|
+
*/
|
|
698
|
+
const NOT_COPY_WORDS = /* @__PURE__ */ new Set([
|
|
699
|
+
"GET",
|
|
700
|
+
"POST",
|
|
701
|
+
"PUT",
|
|
702
|
+
"PATCH",
|
|
703
|
+
"DELETE",
|
|
704
|
+
"HEAD",
|
|
705
|
+
"OPTIONS",
|
|
706
|
+
"JSON",
|
|
707
|
+
"UTF-8",
|
|
708
|
+
"ASC",
|
|
709
|
+
"DESC",
|
|
710
|
+
"NULL",
|
|
711
|
+
"TRUE",
|
|
712
|
+
"FALSE",
|
|
713
|
+
"LTR",
|
|
714
|
+
"RTL",
|
|
715
|
+
"CSS",
|
|
716
|
+
"HTML",
|
|
717
|
+
"NONE",
|
|
718
|
+
"AUTO",
|
|
719
|
+
"DEFAULT",
|
|
720
|
+
"UNKNOWN",
|
|
721
|
+
"ERROR",
|
|
722
|
+
"SUCCESS",
|
|
723
|
+
"WARNING",
|
|
724
|
+
"INFO",
|
|
725
|
+
"DEBUG"
|
|
726
|
+
]);
|
|
727
|
+
/** Letters in any script. */
|
|
728
|
+
const HAS_LETTER = /\p{L}/u;
|
|
729
|
+
/** A single token: no spaces. */
|
|
730
|
+
function singleToken(text) {
|
|
731
|
+
if (/^\p{Lu}\p{Ll}+$/u.test(text)) return true;
|
|
732
|
+
if (/^\p{Lu}{2,}$/u.test(text) && !NOT_COPY_WORDS.has(text)) return true;
|
|
733
|
+
if (/[^\x00-\x7f]/.test(text) && !/[_$\-./:]/.test(text) && !/\d/.test(text)) return true;
|
|
734
|
+
return false;
|
|
735
|
+
}
|
|
736
|
+
function looksLikeCopy(raw) {
|
|
737
|
+
const text = raw.trim();
|
|
738
|
+
if (text.length < 2 || !HAS_LETTER.test(text)) return false;
|
|
739
|
+
if (/^(?:https?:|ftp:|mailto:|tel:|sms:|data:|blob:|file:|javascript:|\/\/|\/|\.{1,2}\/|#|@|~\/)/i.test(text)) return false;
|
|
740
|
+
if (/:\/\//.test(text)) return false;
|
|
741
|
+
if (/^[\w.+-]+@[\w-]+\.[\w.-]+$/.test(text)) return false;
|
|
742
|
+
if (!/\s/.test(text) && /^[\w\-./@]+\.[a-z0-9]{1,5}$/i.test(text)) return false;
|
|
743
|
+
if (/^[a-z]+\/[a-z0-9.+-]+$/i.test(text)) return false;
|
|
744
|
+
if (/^[a-z]{2,3}-[A-Z][A-Za-z]{1,3}$/.test(text)) return false;
|
|
745
|
+
if (/^(?:rgba?|hsla?|var|calc|url|translate[XYZ3d]*|rotate[XYZ]?|scale[XYZ]?|matrix|linear-gradient|radial-gradient|conic-gradient|cubic-bezier|steps|polygon|inset|circle|ellipse)\(/i.test(text)) return false;
|
|
746
|
+
if (/(?:sans-serif|serif|monospace|system-ui|cursive)\b/i.test(text)) return false;
|
|
747
|
+
if (/^[\d.]+(?:px|rem|em|vh|vw|vmin|vmax|ms|s|deg|fr|ch|pt|cm|mm)(?:\s|$)/i.test(text)) return false;
|
|
748
|
+
if (/^[MmLlHhVvCcSsQqTtAaZz]\s*-?[\d.]/.test(text)) return false;
|
|
749
|
+
if (/[\w-]+\s*:\s*[^;]+;/.test(text)) return false;
|
|
750
|
+
if (/=>|<\/?[a-z][\w-]*[\s>/]|\{\s*\}|\(\s*\)\s*;|^function\b|^import\s|^const\s/.test(text)) return false;
|
|
751
|
+
if (/^[YMDHhmsaAZ\-/:.\s,]+$/.test(text)) return false;
|
|
752
|
+
if (!/\s/.test(text)) return singleToken(text.replace(/^[("“'\[]+|[.…!?:;,)"”'\]]+$/g, ""));
|
|
753
|
+
const tokens = text.split(/\s+/);
|
|
754
|
+
if (tokens.every((w) => /^[a-z][a-z0-9_:\-\/\[\].%!#]*$/.test(w)) && tokens.some((w) => /[-:_\[\]\/%!#\d]/.test(w))) return false;
|
|
755
|
+
if (tokens.length <= 3 && tokens.every((w) => /^[a-z][a-z0-9]*$|[a-z][A-Z]|_/.test(w) && /^[A-Za-z_$][\w$.]*$/.test(w)) && tokens.some((w) => /[a-z][A-Z]|_/.test(w))) return false;
|
|
756
|
+
return true;
|
|
757
|
+
}
|
|
758
|
+
/** Attributes on any element or component whose value is code, never text. */
|
|
759
|
+
const NON_DISPLAY_ATTRS = /* @__PURE__ */ new Set([
|
|
760
|
+
"className",
|
|
761
|
+
"class",
|
|
762
|
+
"classes",
|
|
763
|
+
"style",
|
|
764
|
+
"id",
|
|
765
|
+
"key",
|
|
766
|
+
"ref",
|
|
767
|
+
"href",
|
|
768
|
+
"src",
|
|
769
|
+
"srcSet",
|
|
770
|
+
"srcset",
|
|
771
|
+
"to",
|
|
772
|
+
"path",
|
|
773
|
+
"htmlFor",
|
|
774
|
+
"type",
|
|
775
|
+
"rel",
|
|
776
|
+
"target",
|
|
777
|
+
"method",
|
|
778
|
+
"action",
|
|
779
|
+
"lang",
|
|
780
|
+
"dir",
|
|
781
|
+
"charSet",
|
|
782
|
+
"value",
|
|
783
|
+
"defaultValue",
|
|
784
|
+
"checked",
|
|
785
|
+
"d",
|
|
786
|
+
"viewBox",
|
|
787
|
+
"fill",
|
|
788
|
+
"stroke",
|
|
789
|
+
"transform",
|
|
790
|
+
"xmlns",
|
|
791
|
+
"width",
|
|
792
|
+
"height",
|
|
793
|
+
"size",
|
|
794
|
+
"variant",
|
|
795
|
+
"color",
|
|
796
|
+
"align",
|
|
797
|
+
"position",
|
|
798
|
+
"mode",
|
|
799
|
+
"kind",
|
|
800
|
+
"intent",
|
|
801
|
+
"tone",
|
|
802
|
+
"as",
|
|
803
|
+
"icon",
|
|
804
|
+
"iconName",
|
|
805
|
+
"image",
|
|
806
|
+
"img",
|
|
807
|
+
"src2",
|
|
808
|
+
"autoComplete",
|
|
809
|
+
"inputMode",
|
|
810
|
+
"accept",
|
|
811
|
+
"pattern",
|
|
812
|
+
"media",
|
|
813
|
+
"sizes",
|
|
814
|
+
"loading",
|
|
815
|
+
"crossOrigin",
|
|
816
|
+
"decoding",
|
|
817
|
+
"preserveAspectRatio",
|
|
818
|
+
"fontFamily",
|
|
819
|
+
"queryKey",
|
|
820
|
+
"testId",
|
|
821
|
+
"locale",
|
|
822
|
+
"format",
|
|
823
|
+
"selector",
|
|
824
|
+
"event"
|
|
825
|
+
]);
|
|
826
|
+
/** Object keys whose value is code, never text. */
|
|
827
|
+
const NON_COPY_KEYS = /* @__PURE__ */ new Set([
|
|
828
|
+
...NON_DISPLAY_ATTRS,
|
|
829
|
+
"url",
|
|
830
|
+
"uri",
|
|
831
|
+
"route",
|
|
832
|
+
"slug",
|
|
833
|
+
"logo",
|
|
834
|
+
"bg",
|
|
835
|
+
"background",
|
|
836
|
+
"colour",
|
|
837
|
+
"font",
|
|
838
|
+
"transition",
|
|
839
|
+
"animation",
|
|
840
|
+
"display",
|
|
841
|
+
"cursor",
|
|
842
|
+
"overflow",
|
|
843
|
+
"justify",
|
|
844
|
+
"points",
|
|
845
|
+
"mask",
|
|
846
|
+
"filter",
|
|
847
|
+
"clipPath",
|
|
848
|
+
"mime",
|
|
849
|
+
"regex",
|
|
850
|
+
"hash",
|
|
851
|
+
"token",
|
|
852
|
+
"secret",
|
|
853
|
+
"apiKey",
|
|
854
|
+
"env",
|
|
855
|
+
"mutationKey",
|
|
856
|
+
"values",
|
|
857
|
+
"option",
|
|
858
|
+
"initialValue",
|
|
859
|
+
"labelKey",
|
|
860
|
+
"valueKey",
|
|
861
|
+
"imageUrl"
|
|
862
|
+
]);
|
|
863
|
+
//#endregion
|
|
664
864
|
//#region src/core/transform.ts
|
|
665
|
-
const traverse$
|
|
865
|
+
const traverse$2 = _traverse.default ?? _traverse;
|
|
666
866
|
const generate = _generate.default ?? _generate;
|
|
667
867
|
/** Attributes whose string value is user-visible text. */
|
|
668
868
|
const TRANSLATABLE_ATTRS = /* @__PURE__ */ new Set([
|
|
@@ -688,7 +888,155 @@ const T_IMPORT = "__RLT";
|
|
|
688
888
|
const TD_IMPORT = "__RLTD";
|
|
689
889
|
const HOOK_IMPORT = "__rlUseT";
|
|
690
890
|
const HOOK_VAR = "__rl";
|
|
891
|
+
const GLOBAL_TEXT = "__rlText";
|
|
892
|
+
const GLOBAL_FMT = "__rlFmt";
|
|
691
893
|
const RUNTIME = "react-autolocale";
|
|
894
|
+
/** Calls whose string arguments are code: selectors, event names, storage keys, paths, class helpers, logging, analytics. */
|
|
895
|
+
const NON_COPY_CALLS = /* @__PURE__ */ new Set([
|
|
896
|
+
"require",
|
|
897
|
+
"import",
|
|
898
|
+
"fetch",
|
|
899
|
+
"axios",
|
|
900
|
+
"get",
|
|
901
|
+
"post",
|
|
902
|
+
"put",
|
|
903
|
+
"patch",
|
|
904
|
+
"delete",
|
|
905
|
+
"request",
|
|
906
|
+
"querySelector",
|
|
907
|
+
"querySelectorAll",
|
|
908
|
+
"getElementById",
|
|
909
|
+
"getElementsByClassName",
|
|
910
|
+
"getElementsByTagName",
|
|
911
|
+
"getAttribute",
|
|
912
|
+
"setAttribute",
|
|
913
|
+
"removeAttribute",
|
|
914
|
+
"hasAttribute",
|
|
915
|
+
"closest",
|
|
916
|
+
"matches",
|
|
917
|
+
"matchMedia",
|
|
918
|
+
"addEventListener",
|
|
919
|
+
"removeEventListener",
|
|
920
|
+
"dispatchEvent",
|
|
921
|
+
"emit",
|
|
922
|
+
"on",
|
|
923
|
+
"off",
|
|
924
|
+
"once",
|
|
925
|
+
"createElementNS",
|
|
926
|
+
"classList",
|
|
927
|
+
"add",
|
|
928
|
+
"remove",
|
|
929
|
+
"toggle",
|
|
930
|
+
"contains",
|
|
931
|
+
"clsx",
|
|
932
|
+
"classnames",
|
|
933
|
+
"classNames",
|
|
934
|
+
"cn",
|
|
935
|
+
"cx",
|
|
936
|
+
"twMerge",
|
|
937
|
+
"tw",
|
|
938
|
+
"css",
|
|
939
|
+
"styled",
|
|
940
|
+
"keyframes",
|
|
941
|
+
"createContext",
|
|
942
|
+
"getItem",
|
|
943
|
+
"setItem",
|
|
944
|
+
"removeItem",
|
|
945
|
+
"stringify",
|
|
946
|
+
"parse",
|
|
947
|
+
"parseInt",
|
|
948
|
+
"parseFloat",
|
|
949
|
+
"Number",
|
|
950
|
+
"String",
|
|
951
|
+
"Boolean",
|
|
952
|
+
"Date",
|
|
953
|
+
"URL",
|
|
954
|
+
"URLSearchParams",
|
|
955
|
+
"RegExp",
|
|
956
|
+
"Symbol",
|
|
957
|
+
"Intl",
|
|
958
|
+
"NumberFormat",
|
|
959
|
+
"DateTimeFormat",
|
|
960
|
+
"split",
|
|
961
|
+
"join",
|
|
962
|
+
"replace",
|
|
963
|
+
"replaceAll",
|
|
964
|
+
"includes",
|
|
965
|
+
"indexOf",
|
|
966
|
+
"lastIndexOf",
|
|
967
|
+
"startsWith",
|
|
968
|
+
"endsWith",
|
|
969
|
+
"match",
|
|
970
|
+
"matchAll",
|
|
971
|
+
"search",
|
|
972
|
+
"test",
|
|
973
|
+
"exec",
|
|
974
|
+
"localeCompare",
|
|
975
|
+
"padStart",
|
|
976
|
+
"padEnd",
|
|
977
|
+
"repeat",
|
|
978
|
+
"normalize",
|
|
979
|
+
"charAt",
|
|
980
|
+
"log",
|
|
981
|
+
"warn",
|
|
982
|
+
"error",
|
|
983
|
+
"info",
|
|
984
|
+
"debug",
|
|
985
|
+
"trace",
|
|
986
|
+
"assert",
|
|
987
|
+
"table",
|
|
988
|
+
"group",
|
|
989
|
+
"defineProperty",
|
|
990
|
+
"hasOwnProperty",
|
|
991
|
+
"has",
|
|
992
|
+
"set",
|
|
993
|
+
"oneOf",
|
|
994
|
+
"oneOfType",
|
|
995
|
+
"enum",
|
|
996
|
+
"literal",
|
|
997
|
+
"nativeEnum",
|
|
998
|
+
"useQuery",
|
|
999
|
+
"useQueries",
|
|
1000
|
+
"useInfiniteQuery",
|
|
1001
|
+
"useMutation",
|
|
1002
|
+
"invalidateQueries",
|
|
1003
|
+
"setQueryData",
|
|
1004
|
+
"getQueryData",
|
|
1005
|
+
"prefetchQuery",
|
|
1006
|
+
"track",
|
|
1007
|
+
"trackEvent",
|
|
1008
|
+
"logEvent",
|
|
1009
|
+
"capture",
|
|
1010
|
+
"captureMessage",
|
|
1011
|
+
"captureException",
|
|
1012
|
+
"gtag",
|
|
1013
|
+
"navigate",
|
|
1014
|
+
"redirect",
|
|
1015
|
+
"createSlice",
|
|
1016
|
+
"createAction",
|
|
1017
|
+
"createAsyncThunk",
|
|
1018
|
+
"registerPlugin",
|
|
1019
|
+
"lazy",
|
|
1020
|
+
"loadScript",
|
|
1021
|
+
"useRef",
|
|
1022
|
+
"useId",
|
|
1023
|
+
"useMedia"
|
|
1024
|
+
]);
|
|
1025
|
+
/** Variable names that hold code. */
|
|
1026
|
+
const NON_COPY_VARS = /(?:class|style|url|href|src|path|color|font|slug|route|selector|regex|pattern|format|mime|token|secret|env|event|icon|image|img|logo|key|id)s?$/i;
|
|
1027
|
+
const COMPARISON_OPS = /* @__PURE__ */ new Set([
|
|
1028
|
+
"===",
|
|
1029
|
+
"!==",
|
|
1030
|
+
"==",
|
|
1031
|
+
"!=",
|
|
1032
|
+
"<",
|
|
1033
|
+
">",
|
|
1034
|
+
"<=",
|
|
1035
|
+
">=",
|
|
1036
|
+
"in",
|
|
1037
|
+
"instanceof"
|
|
1038
|
+
]);
|
|
1039
|
+
const IGNORE_COMMENT = /(?:i18n|autolocale|auto-locale)[- ]ignore|translate\s*:\s*no/i;
|
|
692
1040
|
function parserPlugins(file) {
|
|
693
1041
|
if (/\.tsx$/.test(file)) return ["typescript", "jsx"];
|
|
694
1042
|
if (/\.ts$/.test(file)) return ["typescript"];
|
|
@@ -718,25 +1066,137 @@ function isTranslatable(text) {
|
|
|
718
1066
|
function isComponentName(name) {
|
|
719
1067
|
return !!name && /^[A-Z]/.test(name);
|
|
720
1068
|
}
|
|
721
|
-
/**
|
|
1069
|
+
/** A function that may call hooks: a component, or a custom hook named useX. */
|
|
1070
|
+
function canCallHooks(name) {
|
|
1071
|
+
return isComponentName(name) || !!name && /^use[A-Z0-9]/.test(name);
|
|
1072
|
+
}
|
|
1073
|
+
function functionName(fn) {
|
|
1074
|
+
if ((fn.node.type === "FunctionDeclaration" || fn.node.type === "FunctionExpression") && fn.node.id) return fn.node.id.name;
|
|
1075
|
+
let up = fn.parentPath;
|
|
1076
|
+
while (up && up.isCallExpression()) up = up.parentPath;
|
|
1077
|
+
if (up?.isVariableDeclarator() && t.isIdentifier(up.node.id)) return up.node.id.name;
|
|
1078
|
+
}
|
|
1079
|
+
/**
|
|
1080
|
+
* The component (or custom hook) whose body contains `path`, so a translation hook can be added to it. Callbacks
|
|
1081
|
+
* inside a component count; a default parameter value does not (it runs before the hook line), and neither does
|
|
1082
|
+
* module-level code: those use the global lookup instead.
|
|
1083
|
+
*/
|
|
722
1084
|
function enclosingComponent(path) {
|
|
723
|
-
let
|
|
724
|
-
|
|
725
|
-
if (p.isFunction())
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
if ((fn.node.type === "FunctionDeclaration" || fn.node.type === "FunctionExpression") && fn.node.id) name = fn.node.id.name;
|
|
729
|
-
if (!name) {
|
|
730
|
-
let up = fn.parentPath;
|
|
731
|
-
while (up && up.isCallExpression()) up = up.parentPath;
|
|
732
|
-
if (up?.isVariableDeclarator() && t.isIdentifier(up.node.id)) name = up.node.id.name;
|
|
733
|
-
}
|
|
734
|
-
if (isComponentName(name)) return fn;
|
|
735
|
-
}
|
|
736
|
-
p = p.parentPath;
|
|
1085
|
+
let prev = path;
|
|
1086
|
+
for (let p = path.parentPath; p; prev = p, p = p.parentPath) {
|
|
1087
|
+
if (!p.isFunction()) continue;
|
|
1088
|
+
if (prev.listKey === "params") return null;
|
|
1089
|
+
if (canCallHooks(functionName(p))) return p;
|
|
737
1090
|
}
|
|
738
1091
|
return null;
|
|
739
1092
|
}
|
|
1093
|
+
function calleeName(callee) {
|
|
1094
|
+
if (t.isIdentifier(callee)) return callee.name;
|
|
1095
|
+
if ((t.isMemberExpression(callee) || t.isOptionalMemberExpression(callee)) && !callee.computed && t.isIdentifier(callee.property)) return callee.property.name;
|
|
1096
|
+
return "";
|
|
1097
|
+
}
|
|
1098
|
+
const HTML_DISPLAY_ATTRS = /* @__PURE__ */ new Set([
|
|
1099
|
+
...TRANSLATABLE_ATTRS,
|
|
1100
|
+
"aria-roledescription",
|
|
1101
|
+
"aria-valuetext",
|
|
1102
|
+
"label"
|
|
1103
|
+
]);
|
|
1104
|
+
/** data-* attributes that tooltip libraries show to people. */
|
|
1105
|
+
const DISPLAY_DATA_ATTR = /^data-(?:[\w-]*(?:tooltip|tip|title|label|content))/;
|
|
1106
|
+
const isDisplayAttr = (name, html) => (html ? HTML_DISPLAY_ATTRS.has(name) : false) || DISPLAY_DATA_ATTR.test(name);
|
|
1107
|
+
/** True where the code needs the exact value of a string: comparisons, keys, types, selectors, paths, class helpers... */
|
|
1108
|
+
function inCodePosition(path) {
|
|
1109
|
+
const parent = path.parent;
|
|
1110
|
+
if (path.findParent((p) => p.isTSType() || p.isTSTypeAnnotation() || p.isTSInterfaceDeclaration() || p.isTSTypeAliasDeclaration() || p.isTSEnumDeclaration() || p.isTSModuleDeclaration())) return true;
|
|
1111
|
+
if (t.isImportDeclaration(parent) || t.isExportNamedDeclaration(parent) || t.isExportAllDeclaration(parent)) return true;
|
|
1112
|
+
if ((t.isObjectProperty(parent) || t.isObjectMethod(parent) || t.isClassProperty(parent) || t.isClassMethod(parent)) && path.key === "key" && !parent.computed) return true;
|
|
1113
|
+
if (t.isMemberExpression(parent) && parent.computed && path.key === "property") return true;
|
|
1114
|
+
if (t.isBinaryExpression(parent) && COMPARISON_OPS.has(parent.operator)) return true;
|
|
1115
|
+
if (t.isSwitchCase(parent) && path.key === "test") return true;
|
|
1116
|
+
if (t.isTaggedTemplateExpression(parent)) return true;
|
|
1117
|
+
if (t.isAssignmentExpression(parent) && path.key === "right") {
|
|
1118
|
+
const l = parent.left;
|
|
1119
|
+
const nm = t.isMemberExpression(l) && !l.computed && t.isIdentifier(l.property) ? l.property.name : "";
|
|
1120
|
+
if (NON_COPY_KEYS.has(nm) || nm === "cookie") return true;
|
|
1121
|
+
}
|
|
1122
|
+
let prev = path;
|
|
1123
|
+
for (let p = path.parentPath; p; prev = p, p = p.parentPath) {
|
|
1124
|
+
if (p.isFunction()) break;
|
|
1125
|
+
if (p.isJSXAttribute()) {
|
|
1126
|
+
const n = t.isJSXIdentifier(p.node.name) ? p.node.name.name : "";
|
|
1127
|
+
const opening = p.parent;
|
|
1128
|
+
const html = t.isJSXIdentifier(opening.name) && /^[a-z]/.test(opening.name.name);
|
|
1129
|
+
if (NON_DISPLAY_ATTRS.has(n) || /^on[A-Z]/.test(n)) return true;
|
|
1130
|
+
if (n.startsWith("data-") && !DISPLAY_DATA_ATTR.test(n)) return true;
|
|
1131
|
+
if (html && !isDisplayAttr(n, true)) return true;
|
|
1132
|
+
break;
|
|
1133
|
+
}
|
|
1134
|
+
if ((p.isCallExpression() || p.isNewExpression()) && prev.listKey === "arguments" && NON_COPY_CALLS.has(calleeName(p.node.callee))) return true;
|
|
1135
|
+
if (p.isThrowStatement()) return true;
|
|
1136
|
+
if (p.isObjectProperty() && prev.key === "value") {
|
|
1137
|
+
const k = p.node.key;
|
|
1138
|
+
const name = t.isIdentifier(k) ? k.name : t.isStringLiteral(k) ? k.value : "";
|
|
1139
|
+
if (NON_COPY_KEYS.has(name)) return true;
|
|
1140
|
+
}
|
|
1141
|
+
if (p.isVariableDeclarator() && prev.key === "init" && t.isIdentifier(p.node.id) && NON_COPY_VARS.test(p.node.id.name)) return true;
|
|
1142
|
+
if (p.isBinaryExpression() && COMPARISON_OPS.has(p.node.operator)) return true;
|
|
1143
|
+
}
|
|
1144
|
+
return false;
|
|
1145
|
+
}
|
|
1146
|
+
/** Hook calls whose arguments are read once or kept between renders, so a hook-based lookup would go stale. */
|
|
1147
|
+
const DEFERRED_HOOKS = /* @__PURE__ */ new Set([
|
|
1148
|
+
"useState",
|
|
1149
|
+
"useReducer",
|
|
1150
|
+
"useRef",
|
|
1151
|
+
"useMemo",
|
|
1152
|
+
"useCallback",
|
|
1153
|
+
"useEffect",
|
|
1154
|
+
"useLayoutEffect",
|
|
1155
|
+
"useInsertionEffect",
|
|
1156
|
+
"useImperativeHandle",
|
|
1157
|
+
"useSyncExternalStore",
|
|
1158
|
+
"useTransition",
|
|
1159
|
+
"useDeferredValue",
|
|
1160
|
+
"createContext",
|
|
1161
|
+
"createRef",
|
|
1162
|
+
"memo",
|
|
1163
|
+
"forwardRef"
|
|
1164
|
+
]);
|
|
1165
|
+
/** Is the string inside the arguments of one of those calls (up to the component that owns it)? */
|
|
1166
|
+
function inDeferredHookArgs(path) {
|
|
1167
|
+
for (let p = path.parentPath; p; p = p.parentPath) {
|
|
1168
|
+
if (p.isCallExpression() && DEFERRED_HOOKS.has(calleeName(p.node.callee))) return true;
|
|
1169
|
+
if (p.isFunction() && canCallHooks(functionName(p))) break;
|
|
1170
|
+
}
|
|
1171
|
+
return false;
|
|
1172
|
+
}
|
|
1173
|
+
/** `// i18n-ignore` (or autolocale-ignore) on the string or its statement leaves it alone. */
|
|
1174
|
+
function hasIgnoreComment(path) {
|
|
1175
|
+
for (let p = path; p; p = p.parentPath) {
|
|
1176
|
+
const n = p.node;
|
|
1177
|
+
if ([...n.leadingComments ?? [], ...n.trailingComments ?? []].some((c) => IGNORE_COMMENT.test(c.value))) return true;
|
|
1178
|
+
if (p.isStatement()) break;
|
|
1179
|
+
}
|
|
1180
|
+
return false;
|
|
1181
|
+
}
|
|
1182
|
+
/** The element, attribute, property or call a string belongs to, so engines can translate it with context. */
|
|
1183
|
+
function contextOf(path) {
|
|
1184
|
+
for (let p = path.parentPath; p; p = p.parentPath) {
|
|
1185
|
+
if (p.isJSXAttribute() && t.isJSXIdentifier(p.node.name)) return p.node.name.name;
|
|
1186
|
+
if (p.isObjectProperty()) {
|
|
1187
|
+
const k = p.node.key;
|
|
1188
|
+
if (t.isIdentifier(k)) return k.name;
|
|
1189
|
+
if (t.isStringLiteral(k)) return k.value;
|
|
1190
|
+
}
|
|
1191
|
+
if (p.isJSXElement() && t.isJSXIdentifier(p.node.openingElement.name)) return p.node.openingElement.name.name;
|
|
1192
|
+
if (p.isCallExpression()) {
|
|
1193
|
+
const n = calleeName(p.node.callee);
|
|
1194
|
+
if (n) return n;
|
|
1195
|
+
}
|
|
1196
|
+
if (p.isFunction()) break;
|
|
1197
|
+
}
|
|
1198
|
+
return "text";
|
|
1199
|
+
}
|
|
740
1200
|
/** `translate="no"` on an element (or any ancestor) keeps its text and attributes exactly as written. */
|
|
741
1201
|
function hasNoTranslate(opening) {
|
|
742
1202
|
return opening.attributes.some((a) => t.isJSXAttribute(a) && t.isJSXIdentifier(a.name, { name: "translate" }) && (t.isStringLiteral(a.value, { value: "no" }) || t.isJSXExpressionContainer(a.value) && t.isStringLiteral(a.value.expression, { value: "no" })));
|
|
@@ -745,43 +1205,78 @@ function insideNoTranslate(path) {
|
|
|
745
1205
|
for (let p = path; p; p = p.parentPath) if (p.isJSXElement() && hasNoTranslate(p.node.openingElement)) return true;
|
|
746
1206
|
return false;
|
|
747
1207
|
}
|
|
748
|
-
/** A
|
|
1208
|
+
/** A readable placeholder name for `name`, `user.name`, `this.props.count`; null for anything else. */
|
|
749
1209
|
function simpleValueName(node) {
|
|
750
1210
|
const root = (n) => t.isIdentifier(n) || t.isThisExpression(n) || (t.isMemberExpression(n) || t.isOptionalMemberExpression(n)) && root(n.object) && (n.computed ? t.isNumericLiteral(n.property) || t.isStringLiteral(n.property) : t.isIdentifier(n.property));
|
|
751
1211
|
if (!root(node)) return null;
|
|
752
1212
|
if (t.isIdentifier(node)) return node.name;
|
|
753
1213
|
if ((t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) && !node.computed && t.isIdentifier(node.property)) return node.property.name;
|
|
754
|
-
return
|
|
1214
|
+
return null;
|
|
1215
|
+
}
|
|
1216
|
+
/** Hands out placeholder names: readable ones where possible ({name}), otherwise {1}, {2}, ... */
|
|
1217
|
+
function namer() {
|
|
1218
|
+
const used = /* @__PURE__ */ new Set();
|
|
1219
|
+
let n = 0;
|
|
1220
|
+
return (expr) => {
|
|
1221
|
+
const base = simpleValueName(expr);
|
|
1222
|
+
if (base) {
|
|
1223
|
+
let name = base;
|
|
1224
|
+
for (let i = 2; used.has(name); i++) name = `${base}_${i}`;
|
|
1225
|
+
used.add(name);
|
|
1226
|
+
return name;
|
|
1227
|
+
}
|
|
1228
|
+
let name;
|
|
1229
|
+
do
|
|
1230
|
+
name = String(++n);
|
|
1231
|
+
while (used.has(name));
|
|
1232
|
+
used.add(name);
|
|
1233
|
+
return name;
|
|
1234
|
+
};
|
|
1235
|
+
}
|
|
1236
|
+
/** Does this JSX element show any text of its own (as opposed to being an icon, image or spacer)? */
|
|
1237
|
+
function carriesText(el) {
|
|
1238
|
+
return el.children.some((c) => t.isJSXText(c) && isTranslatable(cleanJsxText(c.value)) || t.isJSXExpressionContainer(c) && !t.isJSXEmptyExpression(c.expression) && !t.isStringLiteral(c.expression) || t.isJSXElement(c) && carriesText(c));
|
|
755
1239
|
}
|
|
756
1240
|
/**
|
|
757
|
-
*
|
|
758
|
-
*
|
|
1241
|
+
* A sentence made of text plus variables, links, bold words or other elements becomes one message with
|
|
1242
|
+
* placeholders: "Read the {1} now", "Welcome, {name}". The pieces keep their own markup and are filled in at
|
|
1243
|
+
* runtime, so word order can change per language. Elements that only decorate (icons, images) stay out of it.
|
|
759
1244
|
*/
|
|
760
1245
|
function buildTemplate(node) {
|
|
761
1246
|
let text = "";
|
|
762
1247
|
const values = [];
|
|
763
|
-
const
|
|
764
|
-
|
|
765
|
-
|
|
1248
|
+
const name = namer();
|
|
1249
|
+
let hasTextRun = false;
|
|
1250
|
+
let hasMoving = false;
|
|
1251
|
+
for (const child of node.children) if (t.isJSXText(child)) {
|
|
1252
|
+
const run = cleanJsxText(child.value);
|
|
1253
|
+
if (isTranslatable(run)) hasTextRun = true;
|
|
1254
|
+
text += run;
|
|
1255
|
+
} else if (t.isJSXExpressionContainer(child)) {
|
|
766
1256
|
const e = child.expression;
|
|
767
1257
|
if (t.isJSXEmptyExpression(e)) continue;
|
|
768
1258
|
if (t.isStringLiteral(e)) {
|
|
769
1259
|
text += e.value;
|
|
770
1260
|
continue;
|
|
771
1261
|
}
|
|
772
|
-
const
|
|
773
|
-
if (!base) return null;
|
|
774
|
-
let name = base;
|
|
775
|
-
for (let i = 2; used.has(name); i++) name = `${base}_${i}`;
|
|
776
|
-
used.add(name);
|
|
1262
|
+
const n = name(e);
|
|
777
1263
|
values.push({
|
|
778
|
-
name,
|
|
1264
|
+
name: n,
|
|
779
1265
|
expr: e
|
|
780
1266
|
});
|
|
781
|
-
text += `{${
|
|
1267
|
+
text += `{${n}}`;
|
|
1268
|
+
hasMoving = true;
|
|
1269
|
+
} else if (t.isJSXElement(child) || t.isJSXFragment(child)) {
|
|
1270
|
+
const n = name(child);
|
|
1271
|
+
values.push({
|
|
1272
|
+
name: n,
|
|
1273
|
+
expr: child
|
|
1274
|
+
});
|
|
1275
|
+
text += `{${n}}`;
|
|
1276
|
+
if (carriesText(child)) hasMoving = true;
|
|
782
1277
|
} else return null;
|
|
783
1278
|
const letters = text.replace(/\{\w+\}/g, "");
|
|
784
|
-
if (!values.length || !isTranslatable(letters)) return null;
|
|
1279
|
+
if (!values.length || !hasTextRun || !hasMoving || !isTranslatable(letters)) return null;
|
|
785
1280
|
return {
|
|
786
1281
|
text: text.trim(),
|
|
787
1282
|
values,
|
|
@@ -789,10 +1284,11 @@ function buildTemplate(node) {
|
|
|
789
1284
|
trail: /\s$/.test(text)
|
|
790
1285
|
};
|
|
791
1286
|
}
|
|
792
|
-
/** Parses a file, extracts translatable strings, and rewrites
|
|
1287
|
+
/** Parses a file, extracts translatable strings, and rewrites the code to look them up at runtime. */
|
|
793
1288
|
function transformJsx(code, file, opts = {}) {
|
|
794
1289
|
const rewrite = opts.rewrite !== false;
|
|
795
|
-
|
|
1290
|
+
const codeValues = opts.facts?.codeValues ?? /* @__PURE__ */ new Set();
|
|
1291
|
+
if (!/["'`<]/.test(code)) return null;
|
|
796
1292
|
let ast;
|
|
797
1293
|
try {
|
|
798
1294
|
ast = parse(code, {
|
|
@@ -811,33 +1307,78 @@ function transformJsx(code, file, opts = {}) {
|
|
|
811
1307
|
contexts[text] ??= context;
|
|
812
1308
|
};
|
|
813
1309
|
const hookTargets = /* @__PURE__ */ new Set();
|
|
1310
|
+
const generated = /* @__PURE__ */ new WeakSet();
|
|
814
1311
|
let usedT = false;
|
|
815
1312
|
let usedTD = false;
|
|
1313
|
+
let usedGlobal = false;
|
|
1314
|
+
let usedGlobalFmt = false;
|
|
816
1315
|
const space = () => t.jsxExpressionContainer(t.stringLiteral(" "));
|
|
817
|
-
|
|
1316
|
+
/** Local names of `msg` imported from react-autolocale: msg("text") always means "translate this". */
|
|
1317
|
+
const msgNames = /* @__PURE__ */ new Set();
|
|
1318
|
+
for (const node of ast.program.body) {
|
|
1319
|
+
if (!t.isImportDeclaration(node) || node.source.value !== RUNTIME) continue;
|
|
1320
|
+
for (const sp of node.specifiers) if (t.isImportSpecifier(sp) && (t.isIdentifier(sp.imported, { name: "msg" }) || t.isStringLiteral(sp.imported, { value: "msg" }))) msgNames.add(sp.local.name);
|
|
1321
|
+
}
|
|
1322
|
+
const lit = (v) => {
|
|
1323
|
+
const n = t.stringLiteral(v);
|
|
1324
|
+
generated.add(n);
|
|
1325
|
+
return n;
|
|
1326
|
+
};
|
|
1327
|
+
/** `__rl(id, text)` inside a component (follows language changes), `__rlText(id, text)` anywhere else. */
|
|
1328
|
+
const lookup = (path, text) => {
|
|
1329
|
+
const host = inDeferredHookArgs(path) ? null : enclosingComponent(path);
|
|
1330
|
+
if (host) {
|
|
1331
|
+
hookTargets.add(host);
|
|
1332
|
+
return t.callExpression(t.identifier(HOOK_VAR), [lit(messageId(text)), lit(text)]);
|
|
1333
|
+
}
|
|
1334
|
+
usedGlobal = true;
|
|
1335
|
+
return t.callExpression(t.identifier(GLOBAL_TEXT), [lit(messageId(text)), lit(text)]);
|
|
1336
|
+
};
|
|
1337
|
+
const lookupFmt = (path, text, values) => {
|
|
1338
|
+
const vars = t.objectExpression(values.map((v) => t.objectProperty(/^\d+$/.test(v.name) ? t.numericLiteral(Number(v.name)) : t.identifier(v.name), v.expr)));
|
|
1339
|
+
const host = inDeferredHookArgs(path) ? null : enclosingComponent(path);
|
|
1340
|
+
if (host) {
|
|
1341
|
+
hookTargets.add(host);
|
|
1342
|
+
return t.callExpression(t.memberExpression(t.identifier(HOOK_VAR), t.identifier("f")), [
|
|
1343
|
+
lit(messageId(text)),
|
|
1344
|
+
lit(text),
|
|
1345
|
+
vars
|
|
1346
|
+
]);
|
|
1347
|
+
}
|
|
1348
|
+
usedGlobalFmt = true;
|
|
1349
|
+
return t.callExpression(t.identifier(GLOBAL_FMT), [
|
|
1350
|
+
lit(messageId(text)),
|
|
1351
|
+
lit(text),
|
|
1352
|
+
vars
|
|
1353
|
+
]);
|
|
1354
|
+
};
|
|
1355
|
+
/** Common gate for a string found in code (not JSX text, which is always text). */
|
|
1356
|
+
const isCopyHere = (path, text) => looksLikeCopy(text) && !codeValues.has(text.trim()) && !inCodePosition(path) && !hasIgnoreComment(path) && !insideNoTranslate(path);
|
|
1357
|
+
traverse$2(ast, {
|
|
818
1358
|
"JSXElement|JSXFragment"(path) {
|
|
819
1359
|
const node = path.node;
|
|
820
1360
|
const name = t.isJSXElement(node) && t.isJSXIdentifier(node.openingElement.name) ? node.openingElement.name.name : null;
|
|
821
|
-
if (name && SKIP_ELEMENTS.has(name)) return;
|
|
1361
|
+
if (name && (SKIP_ELEMENTS.has(name) || name === T_IMPORT || name === TD_IMPORT)) return;
|
|
822
1362
|
if (insideNoTranslate(path)) return;
|
|
823
1363
|
const context = name ?? "text";
|
|
824
|
-
if (node.children.some((c) => t.isJSXExpressionContainer(c) && !t.isJSXEmptyExpression(c.expression) && !t.isStringLiteral(c.expression))) {
|
|
1364
|
+
if (node.children.some((c) => t.isJSXElement(c) || t.isJSXFragment(c) || t.isJSXExpressionContainer(c) && !t.isJSXEmptyExpression(c.expression) && !t.isStringLiteral(c.expression))) {
|
|
825
1365
|
const tpl = buildTemplate(node);
|
|
826
|
-
if (
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
1366
|
+
if (tpl) {
|
|
1367
|
+
record(tpl.text, context);
|
|
1368
|
+
if (!rewrite) return;
|
|
1369
|
+
const valueObject = t.objectExpression(tpl.values.map((v) => t.objectProperty(/^\d+$/.test(v.name) ? t.numericLiteral(Number(v.name)) : t.identifier(v.name), t.cloneNode(v.expr))));
|
|
1370
|
+
node.children = [
|
|
1371
|
+
...tpl.lead ? [space()] : [],
|
|
1372
|
+
t.jsxElement(t.jsxOpeningElement(t.jsxIdentifier(TD_IMPORT), [
|
|
1373
|
+
t.jsxAttribute(t.jsxIdentifier("i"), t.stringLiteral(messageId(tpl.text))),
|
|
1374
|
+
t.jsxAttribute(t.jsxIdentifier("s"), t.stringLiteral(tpl.text)),
|
|
1375
|
+
t.jsxAttribute(t.jsxIdentifier("v"), t.jsxExpressionContainer(valueObject))
|
|
1376
|
+
], true), null, []),
|
|
1377
|
+
...tpl.trail ? [space()] : []
|
|
1378
|
+
];
|
|
1379
|
+
usedTD = true;
|
|
1380
|
+
return;
|
|
1381
|
+
}
|
|
841
1382
|
}
|
|
842
1383
|
const children = [];
|
|
843
1384
|
let changed = false;
|
|
@@ -863,19 +1404,130 @@ function transformJsx(code, file, opts = {}) {
|
|
|
863
1404
|
},
|
|
864
1405
|
JSXAttribute(path) {
|
|
865
1406
|
const { name, value } = path.node;
|
|
866
|
-
if (!t.isJSXIdentifier(name) || !
|
|
867
|
-
if (!t.isStringLiteral(value)) return;
|
|
1407
|
+
if (!t.isJSXIdentifier(name) || !t.isStringLiteral(value)) return;
|
|
868
1408
|
const opening = path.parent;
|
|
869
|
-
if (!t.isJSXIdentifier(opening.name)
|
|
870
|
-
|
|
1409
|
+
if (!t.isJSXIdentifier(opening.name)) return;
|
|
1410
|
+
const tag = opening.name.name;
|
|
1411
|
+
if (tag === T_IMPORT || tag === TD_IMPORT) return;
|
|
1412
|
+
if (insideNoTranslate(path.parentPath.parentPath) || hasIgnoreComment(path)) return;
|
|
1413
|
+
const html = /^[a-z]/.test(tag);
|
|
871
1414
|
const text = value.value.trim();
|
|
872
1415
|
if (!text || !isTranslatable(text)) return;
|
|
873
|
-
const
|
|
874
|
-
|
|
1416
|
+
const attr = (n) => opening.attributes.find((a) => t.isJSXAttribute(a) && t.isJSXIdentifier(a.name, { name: n }));
|
|
1417
|
+
const literalOf = (a) => a && t.isStringLiteral(a.value) ? a.value.value : void 0;
|
|
1418
|
+
let translate = false;
|
|
1419
|
+
if (html) {
|
|
1420
|
+
translate = isDisplayAttr(name.name, true);
|
|
1421
|
+
if (name.name === "value" && tag === "input" && [
|
|
1422
|
+
"submit",
|
|
1423
|
+
"button",
|
|
1424
|
+
"reset"
|
|
1425
|
+
].includes(literalOf(attr("type")) ?? "")) translate = true;
|
|
1426
|
+
if (name.name === "content" && tag === "meta") {
|
|
1427
|
+
const who = literalOf(attr("name")) ?? literalOf(attr("property")) ?? "";
|
|
1428
|
+
translate = [
|
|
1429
|
+
"description",
|
|
1430
|
+
"og:title",
|
|
1431
|
+
"og:description",
|
|
1432
|
+
"twitter:title",
|
|
1433
|
+
"twitter:description"
|
|
1434
|
+
].includes(who);
|
|
1435
|
+
}
|
|
1436
|
+
} else if (!NON_DISPLAY_ATTRS.has(name.name) && !/^on[A-Z]/.test(name.name) && !(name.name.startsWith("data-") && !DISPLAY_DATA_ATTR.test(name.name))) translate = looksLikeCopy(text) && !codeValues.has(text);
|
|
1437
|
+
if (!translate) return;
|
|
875
1438
|
record(text, name.name);
|
|
876
1439
|
if (!rewrite) return;
|
|
877
|
-
path.node.value = t.jsxExpressionContainer(
|
|
878
|
-
|
|
1440
|
+
path.node.value = t.jsxExpressionContainer(lookup(path, text));
|
|
1441
|
+
},
|
|
1442
|
+
CallExpression(path) {
|
|
1443
|
+
const callee = path.node.callee;
|
|
1444
|
+
if (!t.isIdentifier(callee) || !msgNames.has(callee.name)) return;
|
|
1445
|
+
const [arg, vars] = path.node.arguments;
|
|
1446
|
+
let text = null;
|
|
1447
|
+
let values = [];
|
|
1448
|
+
let object = t.isObjectExpression(vars) ? vars : null;
|
|
1449
|
+
if (t.isStringLiteral(arg)) text = arg.value;
|
|
1450
|
+
else if (t.isTemplateLiteral(arg)) {
|
|
1451
|
+
const name = namer();
|
|
1452
|
+
text = arg.quasis[0].value.cooked ?? "";
|
|
1453
|
+
arg.expressions.forEach((e, i) => {
|
|
1454
|
+
const n = name(e);
|
|
1455
|
+
values.push({
|
|
1456
|
+
name: n,
|
|
1457
|
+
expr: e
|
|
1458
|
+
});
|
|
1459
|
+
text += `{${n}}` + (arg.quasis[i + 1].value.cooked ?? "");
|
|
1460
|
+
});
|
|
1461
|
+
}
|
|
1462
|
+
if (text === null || !/\S/.test(text)) return;
|
|
1463
|
+
generated.add(arg);
|
|
1464
|
+
record(text, "msg");
|
|
1465
|
+
if (!rewrite) return;
|
|
1466
|
+
const vs = object ? object.properties.flatMap((p) => t.isObjectProperty(p) && !p.computed && (t.isIdentifier(p.key) || t.isStringLiteral(p.key)) ? [{
|
|
1467
|
+
name: t.isIdentifier(p.key) ? p.key.name : p.key.value,
|
|
1468
|
+
expr: p.value
|
|
1469
|
+
}] : []) : values;
|
|
1470
|
+
path.replaceWith(vs.length ? lookupFmt(path, text, vs) : lookup(path, text));
|
|
1471
|
+
},
|
|
1472
|
+
StringLiteral(path) {
|
|
1473
|
+
if (generated.has(path.node) || t.isJSXAttribute(path.parent)) return;
|
|
1474
|
+
const text = path.node.value;
|
|
1475
|
+
if (text !== text.trim() || !isCopyHere(path, text)) return;
|
|
1476
|
+
record(text, contextOf(path));
|
|
1477
|
+
if (rewrite) path.replaceWith(lookup(path, text));
|
|
1478
|
+
},
|
|
1479
|
+
TemplateLiteral(path) {
|
|
1480
|
+
if (generated.has(path.node)) return;
|
|
1481
|
+
const { quasis, expressions } = path.node;
|
|
1482
|
+
const name = namer();
|
|
1483
|
+
let text = quasis[0].value.cooked ?? "";
|
|
1484
|
+
const values = [];
|
|
1485
|
+
expressions.forEach((e, i) => {
|
|
1486
|
+
if (t.isJSXElement(e) || t.isJSXFragment(e) || t.isTSType(e)) return;
|
|
1487
|
+
const n = name(e);
|
|
1488
|
+
values.push({
|
|
1489
|
+
name: n,
|
|
1490
|
+
expr: e
|
|
1491
|
+
});
|
|
1492
|
+
text += `{${n}}` + (quasis[i + 1].value.cooked ?? "");
|
|
1493
|
+
});
|
|
1494
|
+
if (values.length !== expressions.length) return;
|
|
1495
|
+
if (text !== text.trim() || !isCopyHere(path, text.replace(/\{\w+\}/g, "0"))) return;
|
|
1496
|
+
record(text, contextOf(path));
|
|
1497
|
+
if (rewrite) path.replaceWith(values.length ? lookupFmt(path, text, values) : lookup(path, text));
|
|
1498
|
+
},
|
|
1499
|
+
BinaryExpression(path) {
|
|
1500
|
+
if (path.node.operator !== "+" || generated.has(path.node)) return;
|
|
1501
|
+
if (t.isBinaryExpression(path.parent, { operator: "+" })) return;
|
|
1502
|
+
const leaves = [];
|
|
1503
|
+
const flatten = (n) => {
|
|
1504
|
+
if (t.isBinaryExpression(n, { operator: "+" })) {
|
|
1505
|
+
flatten(n.left);
|
|
1506
|
+
flatten(n.right);
|
|
1507
|
+
} else leaves.push(n);
|
|
1508
|
+
};
|
|
1509
|
+
flatten(path.node);
|
|
1510
|
+
const isText = (n) => t.isStringLiteral(n) || t.isTemplateLiteral(n) && n.expressions.length === 0;
|
|
1511
|
+
const firstText = leaves.findIndex(isText);
|
|
1512
|
+
if (firstText < 0 || firstText > 1) return;
|
|
1513
|
+
if (leaves.some((n) => t.isJSXElement(n) || t.isJSXFragment(n))) return;
|
|
1514
|
+
const name = namer();
|
|
1515
|
+
let text = "";
|
|
1516
|
+
const values = [];
|
|
1517
|
+
for (const leaf of leaves) if (t.isStringLiteral(leaf)) text += leaf.value;
|
|
1518
|
+
else if (t.isTemplateLiteral(leaf) && leaf.expressions.length === 0) text += leaf.quasis[0].value.cooked ?? "";
|
|
1519
|
+
else {
|
|
1520
|
+
const n = name(leaf);
|
|
1521
|
+
values.push({
|
|
1522
|
+
name: n,
|
|
1523
|
+
expr: leaf
|
|
1524
|
+
});
|
|
1525
|
+
text += `{${n}}`;
|
|
1526
|
+
}
|
|
1527
|
+
if (text !== text.trim() || !isCopyHere(path, text.replace(/\{\w+\}/g, "0"))) return;
|
|
1528
|
+
record(text, contextOf(path));
|
|
1529
|
+
leaves.forEach((l) => generated.add(l));
|
|
1530
|
+
if (rewrite) path.replaceWith(values.length ? lookupFmt(path, text, values) : lookup(path, text));
|
|
879
1531
|
}
|
|
880
1532
|
});
|
|
881
1533
|
if (strings.size === 0) return null;
|
|
@@ -893,6 +1545,8 @@ function transformJsx(code, file, opts = {}) {
|
|
|
893
1545
|
if (usedT) specifiers.push(t.importSpecifier(t.identifier(T_IMPORT), t.identifier("__T")));
|
|
894
1546
|
if (usedTD) specifiers.push(t.importSpecifier(t.identifier(TD_IMPORT), t.identifier("__TD")));
|
|
895
1547
|
if (hookTargets.size) specifiers.push(t.importSpecifier(t.identifier(HOOK_IMPORT), t.identifier("__useT")));
|
|
1548
|
+
if (usedGlobal) specifiers.push(t.importSpecifier(t.identifier(GLOBAL_TEXT), t.identifier("__rlText")));
|
|
1549
|
+
if (usedGlobalFmt) specifiers.push(t.importSpecifier(t.identifier(GLOBAL_FMT), t.identifier("__rlFmt")));
|
|
896
1550
|
ast.program.body.unshift(t.importDeclaration(specifiers, t.stringLiteral(RUNTIME)));
|
|
897
1551
|
const out = generate(ast, {
|
|
898
1552
|
sourceMaps: true,
|
|
@@ -906,55 +1560,261 @@ function transformJsx(code, file, opts = {}) {
|
|
|
906
1560
|
};
|
|
907
1561
|
}
|
|
908
1562
|
/** Strings (and where they appear), without rewriting. */
|
|
909
|
-
function extractItems(code, file) {
|
|
910
|
-
const r = transformJsx(code, file, {
|
|
1563
|
+
function extractItems(code, file, facts) {
|
|
1564
|
+
const r = transformJsx(code, file, {
|
|
1565
|
+
rewrite: false,
|
|
1566
|
+
facts
|
|
1567
|
+
});
|
|
911
1568
|
return {
|
|
912
1569
|
strings: r?.strings ?? [],
|
|
913
1570
|
contexts: r?.contexts ?? {}
|
|
914
1571
|
};
|
|
915
1572
|
}
|
|
916
1573
|
//#endregion
|
|
917
|
-
//#region src/core/
|
|
918
|
-
const
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
"
|
|
922
|
-
"
|
|
1574
|
+
//#region src/core/facts.ts
|
|
1575
|
+
const traverse$1 = _traverse.default ?? _traverse;
|
|
1576
|
+
/** Methods whose string argument is compared against something. */
|
|
1577
|
+
const COMPARING_METHODS = /* @__PURE__ */ new Set([
|
|
1578
|
+
"includes",
|
|
1579
|
+
"indexOf",
|
|
1580
|
+
"lastIndexOf",
|
|
1581
|
+
"startsWith",
|
|
1582
|
+
"endsWith",
|
|
1583
|
+
"has",
|
|
1584
|
+
"equals",
|
|
1585
|
+
"localeCompare"
|
|
923
1586
|
]);
|
|
924
|
-
const
|
|
925
|
-
"
|
|
926
|
-
"
|
|
927
|
-
"
|
|
928
|
-
"
|
|
1587
|
+
const COMPARISONS = /* @__PURE__ */ new Set([
|
|
1588
|
+
"===",
|
|
1589
|
+
"!==",
|
|
1590
|
+
"==",
|
|
1591
|
+
"!=",
|
|
1592
|
+
"in",
|
|
1593
|
+
"instanceof"
|
|
929
1594
|
]);
|
|
930
|
-
|
|
931
|
-
|
|
1595
|
+
/** String values this file uses as code. */
|
|
1596
|
+
function codeValuesIn(code, file) {
|
|
1597
|
+
const out = /* @__PURE__ */ new Set();
|
|
1598
|
+
let ast;
|
|
1599
|
+
try {
|
|
1600
|
+
ast = parse(code, {
|
|
1601
|
+
sourceType: "module",
|
|
1602
|
+
plugins: parserPlugins(file)
|
|
1603
|
+
});
|
|
1604
|
+
} catch {
|
|
1605
|
+
return out;
|
|
1606
|
+
}
|
|
1607
|
+
const add = (n) => {
|
|
1608
|
+
if (t.isStringLiteral(n)) out.add(n.value);
|
|
1609
|
+
else if (t.isTemplateLiteral(n) && n.expressions.length === 0) out.add(n.quasis[0].value.cooked ?? "");
|
|
1610
|
+
};
|
|
1611
|
+
traverse$1(ast, {
|
|
1612
|
+
BinaryExpression(p) {
|
|
1613
|
+
if (COMPARISONS.has(p.node.operator)) {
|
|
1614
|
+
add(p.node.left);
|
|
1615
|
+
add(p.node.right);
|
|
1616
|
+
}
|
|
1617
|
+
},
|
|
1618
|
+
SwitchCase(p) {
|
|
1619
|
+
add(p.node.test);
|
|
1620
|
+
},
|
|
1621
|
+
ObjectProperty(p) {
|
|
1622
|
+
const k = p.node.key;
|
|
1623
|
+
if (!p.node.computed && t.isIdentifier(k)) out.add(k.name);
|
|
1624
|
+
else add(k);
|
|
1625
|
+
},
|
|
1626
|
+
ObjectMethod(p) {
|
|
1627
|
+
if (!p.node.computed && t.isIdentifier(p.node.key)) out.add(p.node.key.name);
|
|
1628
|
+
},
|
|
1629
|
+
MemberExpression(p) {
|
|
1630
|
+
if (p.node.computed) add(p.node.property);
|
|
1631
|
+
},
|
|
1632
|
+
CallExpression(p) {
|
|
1633
|
+
const c = p.node.callee;
|
|
1634
|
+
const name = t.isIdentifier(c) ? c.name : t.isMemberExpression(c) && t.isIdentifier(c.property) ? c.property.name : "";
|
|
1635
|
+
if (COMPARING_METHODS.has(name)) p.node.arguments.forEach((a) => add(a));
|
|
1636
|
+
if (t.isMemberExpression(c) && t.isIdentifier(c.property) && COMPARING_METHODS.has(c.property.name) && t.isArrayExpression(c.object)) c.object.elements.forEach((e) => add(e));
|
|
1637
|
+
},
|
|
1638
|
+
TSLiteralType(p) {
|
|
1639
|
+
add(p.node.literal);
|
|
1640
|
+
},
|
|
1641
|
+
TSEnumMember(p) {
|
|
1642
|
+
add(p.node.initializer);
|
|
1643
|
+
if (t.isIdentifier(p.node.id)) out.add(p.node.id.name);
|
|
1644
|
+
},
|
|
1645
|
+
TSAsExpression(p) {
|
|
1646
|
+
if (t.isTSTypeReference(p.node.typeAnnotation) && t.isIdentifier(p.node.typeAnnotation.typeName, { name: "const" })) {
|
|
1647
|
+
const e = p.node.expression;
|
|
1648
|
+
if (t.isArrayExpression(e)) e.elements.forEach((x) => add(x));
|
|
1649
|
+
if (t.isObjectExpression(e)) e.properties.forEach((pr) => t.isObjectProperty(pr) && add(pr.value));
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
});
|
|
1653
|
+
return out;
|
|
932
1654
|
}
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
1655
|
+
/** Per-file code values, so one edited file can be re-read without scanning the project again. */
|
|
1656
|
+
var FactsIndex = class {
|
|
1657
|
+
perFile = /* @__PURE__ */ new Map();
|
|
1658
|
+
union = null;
|
|
1659
|
+
set(file, code) {
|
|
1660
|
+
this.perFile.set(file, codeValuesIn(code, file));
|
|
1661
|
+
this.union = null;
|
|
1662
|
+
}
|
|
1663
|
+
delete(file) {
|
|
1664
|
+
this.perFile.delete(file);
|
|
1665
|
+
this.union = null;
|
|
1666
|
+
}
|
|
1667
|
+
facts() {
|
|
1668
|
+
this.union ??= new Set([...this.perFile.values()].flatMap((s) => [...s]));
|
|
1669
|
+
return { codeValues: this.union };
|
|
1670
|
+
}
|
|
1671
|
+
};
|
|
1672
|
+
/** Reads every source file under `include` once. */
|
|
1673
|
+
function buildFactsIndex(root, include) {
|
|
1674
|
+
const index = new FactsIndex();
|
|
1675
|
+
for (const dir of include) for (const file of walk(path.resolve(root, dir))) index.set(file, fs.readFileSync(file, "utf8"));
|
|
1676
|
+
return index;
|
|
1677
|
+
}
|
|
1678
|
+
function collectProjectFacts(root, include) {
|
|
1679
|
+
return buildFactsIndex(root, include).facts();
|
|
1680
|
+
}
|
|
1681
|
+
//#endregion
|
|
1682
|
+
//#region src/core/json.ts
|
|
1683
|
+
/**
|
|
1684
|
+
* Text kept in JSON data files (`import content from "./content.json"`): products, FAQs, team members, page copy.
|
|
1685
|
+
* A string value is translated when it reads like text and its key is not a code key (id, url, type, ...).
|
|
1686
|
+
*/
|
|
1687
|
+
/** JSON files that are configuration, not content. */
|
|
1688
|
+
const NOT_CONTENT = /(?:^|\/)(?:package(?:-lock)?|tsconfig[\w.-]*|jsconfig|composer|\.?eslintrc|\.?prettierrc|vercel|netlify|firebase|\w+\.config|[\w.-]+\.schema|manifest)\.json$/i;
|
|
1689
|
+
function isContentJson(file, skipDirs = []) {
|
|
1690
|
+
const norm = file.split(path.sep).join("/");
|
|
1691
|
+
if (!norm.endsWith(".json") || NOT_CONTENT.test(norm) || norm.includes("/node_modules/")) return false;
|
|
1692
|
+
return !skipDirs.some((d) => norm.startsWith(d.split(path.sep).join("/").replace(/\/?$/, "/")));
|
|
1693
|
+
}
|
|
1694
|
+
function eligible(text, key, facts) {
|
|
1695
|
+
return text === text.trim() && !NON_COPY_KEYS.has(key) && !key.startsWith("$") && looksLikeCopy(text) && !facts?.codeValues.has(text);
|
|
1696
|
+
}
|
|
1697
|
+
/** The translatable strings in a JSON document, in document order. */
|
|
1698
|
+
function jsonStrings(raw, facts) {
|
|
1699
|
+
let data;
|
|
1700
|
+
try {
|
|
1701
|
+
data = JSON.parse(raw);
|
|
1702
|
+
} catch {
|
|
1703
|
+
return [];
|
|
1704
|
+
}
|
|
1705
|
+
const out = [];
|
|
1706
|
+
const visit = (v, key) => {
|
|
1707
|
+
if (typeof v === "string") {
|
|
1708
|
+
if (eligible(v, key, facts)) out.push({
|
|
1709
|
+
text: v,
|
|
1710
|
+
key
|
|
1711
|
+
});
|
|
1712
|
+
} else if (Array.isArray(v)) v.forEach((x) => visit(x, key));
|
|
1713
|
+
else if (v && typeof v === "object") for (const [k, x] of Object.entries(v)) visit(x, k);
|
|
1714
|
+
};
|
|
1715
|
+
visit(data, "");
|
|
1716
|
+
return out;
|
|
1717
|
+
}
|
|
1718
|
+
const IDENT = /^[A-Za-z_$][\w$]*$/;
|
|
1719
|
+
const RESERVED = /* @__PURE__ */ new Set([
|
|
1720
|
+
"default",
|
|
1721
|
+
"class",
|
|
1722
|
+
"function",
|
|
1723
|
+
"var",
|
|
1724
|
+
"let",
|
|
1725
|
+
"const",
|
|
1726
|
+
"new",
|
|
1727
|
+
"delete",
|
|
1728
|
+
"return",
|
|
1729
|
+
"if",
|
|
1730
|
+
"else",
|
|
1731
|
+
"for",
|
|
1732
|
+
"while",
|
|
1733
|
+
"do",
|
|
1734
|
+
"switch",
|
|
1735
|
+
"case",
|
|
1736
|
+
"this",
|
|
1737
|
+
"typeof",
|
|
1738
|
+
"void",
|
|
1739
|
+
"with",
|
|
1740
|
+
"yield",
|
|
1741
|
+
"import",
|
|
1742
|
+
"export",
|
|
1743
|
+
"enum",
|
|
1744
|
+
"null",
|
|
1745
|
+
"true",
|
|
1746
|
+
"false"
|
|
1747
|
+
]);
|
|
1748
|
+
/**
|
|
1749
|
+
* A JavaScript module equivalent to Vite's own JSON import (default export plus a named export per key), with
|
|
1750
|
+
* translatable strings wrapped in lookups that run when the module loads.
|
|
1751
|
+
*/
|
|
1752
|
+
function jsonModule(raw, facts) {
|
|
1753
|
+
let data;
|
|
1754
|
+
try {
|
|
1755
|
+
data = JSON.parse(raw);
|
|
1756
|
+
} catch {
|
|
1757
|
+
return null;
|
|
940
1758
|
}
|
|
1759
|
+
if (/"__proto__"\s*:/.test(raw)) return null;
|
|
1760
|
+
let used = false;
|
|
1761
|
+
const write = (v, key) => {
|
|
1762
|
+
if (typeof v === "string") {
|
|
1763
|
+
if (!eligible(v, key, facts)) return JSON.stringify(v);
|
|
1764
|
+
used = true;
|
|
1765
|
+
return `__rlText(${JSON.stringify(messageId(v))},${JSON.stringify(v)})`;
|
|
1766
|
+
}
|
|
1767
|
+
if (Array.isArray(v)) return `[${v.map((x) => write(x, key)).join(",")}]`;
|
|
1768
|
+
if (v && typeof v === "object") return `{${Object.entries(v).map(([k, x]) => `${JSON.stringify(k)}:${write(x, k)}`).join(",")}}`;
|
|
1769
|
+
return JSON.stringify(v);
|
|
1770
|
+
};
|
|
1771
|
+
const body = write(data, "");
|
|
1772
|
+
if (!used) return null;
|
|
1773
|
+
return `import { __rlText } from "react-autolocale";\nconst __data = ${body};\nexport default __data;\n${(data && typeof data === "object" && !Array.isArray(data) ? Object.keys(data).filter((k) => IDENT.test(k) && !RESERVED.has(k)).map((k) => `export const ${k} = __data[${JSON.stringify(k)}];`) : []).join("\n")}\n`;
|
|
941
1774
|
}
|
|
1775
|
+
//#endregion
|
|
1776
|
+
//#region src/core/scan.ts
|
|
1777
|
+
/** `from "./data.json"`, `import("./data.json")`, `require("./data.json")` */
|
|
1778
|
+
const JSON_IMPORT = /(?:from\s*|import\s*\(?\s*|require\s*\(\s*)["']([^"']+\.json)["']/g;
|
|
942
1779
|
/** Every translatable string under the configured folders, in stable file order, and where each appears. */
|
|
943
|
-
function scanProjectDetailed(root, include) {
|
|
1780
|
+
function scanProjectDetailed(root, include, opts = {}) {
|
|
1781
|
+
const skip = opts.skip ?? [];
|
|
944
1782
|
const found = /* @__PURE__ */ new Set();
|
|
945
1783
|
const contexts = {};
|
|
946
1784
|
const files = {};
|
|
947
1785
|
let fileCount = 0;
|
|
948
|
-
|
|
1786
|
+
const facts = collectProjectFacts(root, include);
|
|
1787
|
+
const add = (text, context, rel) => {
|
|
1788
|
+
found.add(text);
|
|
1789
|
+
contexts[text] ??= context;
|
|
1790
|
+
const list = files[text] ??= [];
|
|
1791
|
+
if (!list.includes(rel)) list.push(rel);
|
|
1792
|
+
};
|
|
1793
|
+
const sourceFiles = [];
|
|
1794
|
+
for (const dir of include) sourceFiles.push(...walk(path.resolve(root, dir), skip));
|
|
1795
|
+
const jsonOnDisk = include.flatMap((dir) => [...walk(path.resolve(root, dir), skip, (f) => isContentJson(f, skip))]);
|
|
1796
|
+
const importedJson = /* @__PURE__ */ new Set();
|
|
1797
|
+
for (const file of sourceFiles) {
|
|
949
1798
|
fileCount++;
|
|
1799
|
+
const code = fs.readFileSync(file, "utf8");
|
|
950
1800
|
const rel = path.relative(root, file).split(path.sep).join("/");
|
|
951
|
-
const items = extractItems(
|
|
952
|
-
for (const s of items.strings)
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
(
|
|
1801
|
+
const items = extractItems(code, file, facts);
|
|
1802
|
+
for (const s of items.strings) add(s, items.contexts[s] ?? "", rel);
|
|
1803
|
+
for (const m of code.matchAll(JSON_IMPORT)) {
|
|
1804
|
+
const spec = m[1];
|
|
1805
|
+
if (spec.startsWith(".")) {
|
|
1806
|
+
const abs = path.resolve(path.dirname(file), spec);
|
|
1807
|
+
if (jsonOnDisk.includes(abs)) importedJson.add(abs);
|
|
1808
|
+
} else {
|
|
1809
|
+
const tail = "/" + spec.replace(/^[@~]\/?|^\/+/, "");
|
|
1810
|
+
for (const j of jsonOnDisk) if (j.split(path.sep).join("/").endsWith(tail)) importedJson.add(j);
|
|
1811
|
+
}
|
|
956
1812
|
}
|
|
957
1813
|
}
|
|
1814
|
+
for (const file of importedJson) {
|
|
1815
|
+
const rel = path.relative(root, file).split(path.sep).join("/");
|
|
1816
|
+
for (const { text, key } of jsonStrings(fs.readFileSync(file, "utf8"), facts)) add(text, key || "text", rel);
|
|
1817
|
+
}
|
|
958
1818
|
return {
|
|
959
1819
|
strings: [...found],
|
|
960
1820
|
contexts,
|
|
@@ -973,7 +1833,7 @@ function readManifest(project) {
|
|
|
973
1833
|
/** Scans the source, updates .auto-locale/manifest.json and the source-language catalog. Never translates. */
|
|
974
1834
|
function scanProject(project) {
|
|
975
1835
|
const { root, site } = project;
|
|
976
|
-
const scan = scanProjectDetailed(root, project.include);
|
|
1836
|
+
const scan = scanProjectDetailed(root, project.include, { skip: [project.output, project.meta] });
|
|
977
1837
|
if (site.seo) {
|
|
978
1838
|
const html = path.join(root, "index.html");
|
|
979
1839
|
if (fs.existsSync(html)) for (const s of headStrings(fs.readFileSync(html, "utf8"))) {
|
|
@@ -1397,4 +2257,4 @@ async function loadProject(root, opts = {}) {
|
|
|
1397
2257
|
};
|
|
1398
2258
|
}
|
|
1399
2259
|
//#endregion
|
|
1400
|
-
export {
|
|
2260
|
+
export { walk as A, getLanguage as B, isContentJson as C, parserPlugins as D, buildFactsIndex as E, writeJson as F, TranslationError as I, createLocalTranslator as L, messageId as M, ensureBanner as N, transformJsx as O, readBanner as P, ollama as R, scanProject as S, FactsIndex as T, loadDicts as _, findSite as a, manifestFile as b, hasRootSrc as c, OVERRIDES_FILE as d, exportTranslations as f, importLegacyCache as g, catalogFile as h, defaultInclude as i, translateHead as j, isSourceFile as k, resolveOutput as l, readOverrides as m, loadConfigFile as n, META_DIR as o, mergeOverrides as p, loadProject as r, defaultOutput as s, findConfigFile as t, toProjectPath as u, removeFromCatalogs as v, jsonModule as w, readManifest as x, translateMissing as y, toTranslator as z };
|