prettier-plugin-intlayer 0.0.0-stage → 9.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -2
- package/dist/cjs/@intlayer/core/dist/esm/interpreter/getEnumeration.cjs +42 -0
- package/dist/cjs/@intlayer/core/dist/esm/interpreter/getEnumeration.cjs.map +1 -0
- package/dist/cjs/collapseTranslations.cjs +73 -0
- package/dist/cjs/collapseTranslations.cjs.map +1 -0
- package/dist/cjs/index.cjs +84 -0
- package/dist/cjs/index.cjs.map +1 -0
- package/dist/cjs/keyOrderings.cjs +135 -0
- package/dist/cjs/keyOrderings.cjs.map +1 -0
- package/dist/cjs/sortContentKeys.cjs +86 -0
- package/dist/cjs/sortContentKeys.cjs.map +1 -0
- package/dist/cjs/sortContentNodes.cjs +131 -0
- package/dist/cjs/sortContentNodes.cjs.map +1 -0
- package/dist/esm/@intlayer/core/dist/esm/interpreter/getEnumeration.mjs +42 -0
- package/dist/esm/@intlayer/core/dist/esm/interpreter/getEnumeration.mjs.map +1 -0
- package/dist/esm/collapseTranslations.mjs +72 -0
- package/dist/esm/collapseTranslations.mjs.map +1 -0
- package/dist/esm/index.mjs +70 -0
- package/dist/esm/index.mjs.map +1 -0
- package/dist/esm/keyOrderings.mjs +128 -0
- package/dist/esm/keyOrderings.mjs.map +1 -0
- package/dist/esm/sortContentKeys.mjs +84 -0
- package/dist/esm/sortContentKeys.mjs.map +1 -0
- package/dist/esm/sortContentNodes.mjs +123 -0
- package/dist/esm/sortContentNodes.mjs.map +1 -0
- package/dist/types/collapseTranslations.d.ts +8 -0
- package/dist/types/collapseTranslations.d.ts.map +1 -0
- package/dist/types/index.d.ts +12 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/keyOrderings.d.ts +36 -0
- package/dist/types/keyOrderings.d.ts.map +1 -0
- package/dist/types/sortContentKeys.d.ts +8 -0
- package/dist/types/sortContentKeys.d.ts.map +1 -0
- package/dist/types/sortContentNodes.d.ts +46 -0
- package/dist/types/sortContentNodes.d.ts.map +1 -0
- package/package.json +94 -4
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
+
const require_keyOrderings = require("./keyOrderings.cjs");
|
|
3
|
+
|
|
4
|
+
// src/sortContentNodes.ts
|
|
5
|
+
/** Helper function name → content node type it declares. */
|
|
6
|
+
const NODE_TYPE_BY_FUNCTION_NAME = {
|
|
7
|
+
t: "translation",
|
|
8
|
+
plural: "plural",
|
|
9
|
+
enu: "enumeration",
|
|
10
|
+
select: "select"
|
|
11
|
+
};
|
|
12
|
+
const SORTED_NODE_TYPES = new Set(Object.values(NODE_TYPE_BY_FUNCTION_NAME));
|
|
13
|
+
/** Fields never walked: positions, comments and back-references. */
|
|
14
|
+
const IGNORED_FIELDS = /* @__PURE__ */ new Set([
|
|
15
|
+
"loc",
|
|
16
|
+
"range",
|
|
17
|
+
"start",
|
|
18
|
+
"end",
|
|
19
|
+
"extra",
|
|
20
|
+
"parent",
|
|
21
|
+
"comments",
|
|
22
|
+
"leadingComments",
|
|
23
|
+
"trailingComments",
|
|
24
|
+
"innerComments",
|
|
25
|
+
"tokens"
|
|
26
|
+
]);
|
|
27
|
+
const isAstNode = (value) => typeof value === "object" && value !== null && typeof value.type === "string";
|
|
28
|
+
const isObjectExpression = (node) => isAstNode(node) && node.type === "ObjectExpression";
|
|
29
|
+
const getStringLiteralValue = (node) => isAstNode(node) && (node.type === "StringLiteral" || node.type === "Literal") && typeof node.value === "string" ? node.value : void 0;
|
|
30
|
+
const getNumericLiteralKey = (node) => (node.type === "NumericLiteral" || node.type === "Literal") && typeof node.value === "number" ? String(node.value) : void 0;
|
|
31
|
+
/**
|
|
32
|
+
* Static name of an object property key (`en`, `'en-GB'`, `2`), or
|
|
33
|
+
* `undefined` when the key is computed.
|
|
34
|
+
*/
|
|
35
|
+
const getStaticKeyName = (property) => {
|
|
36
|
+
if (property.computed) return void 0;
|
|
37
|
+
const key = property.key;
|
|
38
|
+
if (!isAstNode(key)) return void 0;
|
|
39
|
+
if (key.type === "Identifier") return key.name;
|
|
40
|
+
return getStringLiteralValue(key) ?? getNumericLiteralKey(key);
|
|
41
|
+
};
|
|
42
|
+
const isPlainProperty = (property) => isAstNode(property) && (property.type === "Property" || property.type === "ObjectProperty") && property.kind !== "get" && property.kind !== "set" && !property.method;
|
|
43
|
+
/**
|
|
44
|
+
* Reorders the properties of a content map in place. Objects holding a
|
|
45
|
+
* spread, a computed key or a method are left untouched: their order may be
|
|
46
|
+
* meaningful.
|
|
47
|
+
*/
|
|
48
|
+
const sortObjectProperties = (objectNode, orderKeys) => {
|
|
49
|
+
const properties = objectNode.properties;
|
|
50
|
+
if (!Array.isArray(properties) || properties.length < 2) return;
|
|
51
|
+
if (!properties.every(isPlainProperty)) return;
|
|
52
|
+
const propertyByName = /* @__PURE__ */ new Map();
|
|
53
|
+
for (const property of properties) {
|
|
54
|
+
const name = getStaticKeyName(property);
|
|
55
|
+
if (name === void 0 || propertyByName.has(name)) return;
|
|
56
|
+
propertyByName.set(name, property);
|
|
57
|
+
}
|
|
58
|
+
const orderedNames = orderKeys([...propertyByName.keys()]);
|
|
59
|
+
if (!orderedNames) return;
|
|
60
|
+
objectNode.properties = orderedNames.map((name) => propertyByName.get(name));
|
|
61
|
+
};
|
|
62
|
+
/** `t({...})`, `plural({...})`, `enu({...})`, `select({...})`, `x.t({...})`. */
|
|
63
|
+
const getHelperCallContent = (node) => {
|
|
64
|
+
if (node.type !== "CallExpression") return void 0;
|
|
65
|
+
const callee = node.callee;
|
|
66
|
+
if (!isAstNode(callee)) return void 0;
|
|
67
|
+
const calleeName = callee.type === "Identifier" ? callee.name : callee.type === "MemberExpression" && !callee.computed && isAstNode(callee.property) ? callee.property.name : void 0;
|
|
68
|
+
const nodeType = typeof calleeName === "string" ? NODE_TYPE_BY_FUNCTION_NAME[calleeName] : void 0;
|
|
69
|
+
const [firstArgument] = node.arguments;
|
|
70
|
+
if (!nodeType || !isObjectExpression(firstArgument)) return void 0;
|
|
71
|
+
return {
|
|
72
|
+
nodeType,
|
|
73
|
+
content: firstArgument
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* `{ nodeType: 'plural', plural: {...} }`, the serialized form used by
|
|
78
|
+
* `.content.json` files and the CMS.
|
|
79
|
+
*/
|
|
80
|
+
const getSerializedNodeContent = (node) => {
|
|
81
|
+
if (!isObjectExpression(node)) return void 0;
|
|
82
|
+
const properties = node.properties.filter(isPlainProperty);
|
|
83
|
+
const findPropertyValue = (name) => properties.find((property) => getStaticKeyName(property) === name)?.value;
|
|
84
|
+
const nodeType = getStringLiteralValue(findPropertyValue("nodeType"));
|
|
85
|
+
if (!nodeType || !SORTED_NODE_TYPES.has(nodeType)) return void 0;
|
|
86
|
+
const content = findPropertyValue(nodeType);
|
|
87
|
+
return isObjectExpression(content) ? {
|
|
88
|
+
nodeType,
|
|
89
|
+
content
|
|
90
|
+
} : void 0;
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* Walks a Prettier AST and sorts, in place, the keys of every Intlayer
|
|
94
|
+
* translation (locales), plural (CLDR categories), enumeration (thresholds)
|
|
95
|
+
* and select (cases).
|
|
96
|
+
*/
|
|
97
|
+
const sortContentNodesInAst = (rootNode, pinnedLocales) => {
|
|
98
|
+
const orderingByNodeType = {
|
|
99
|
+
translation: require_keyOrderings.createLocaleOrdering(pinnedLocales),
|
|
100
|
+
plural: require_keyOrderings.orderPluralCategories,
|
|
101
|
+
enumeration: require_keyOrderings.orderEnumerationConditions,
|
|
102
|
+
select: require_keyOrderings.orderSelectCases
|
|
103
|
+
};
|
|
104
|
+
const visitedNodes = /* @__PURE__ */ new WeakSet();
|
|
105
|
+
const pendingValues = [rootNode];
|
|
106
|
+
while (pendingValues.length > 0) {
|
|
107
|
+
const value = pendingValues.pop();
|
|
108
|
+
if (typeof value !== "object" || value === null) continue;
|
|
109
|
+
if (visitedNodes.has(value)) continue;
|
|
110
|
+
visitedNodes.add(value);
|
|
111
|
+
if (Array.isArray(value)) {
|
|
112
|
+
pendingValues.push(...value);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (isAstNode(value)) {
|
|
116
|
+
const contentNode = getHelperCallContent(value) ?? getSerializedNodeContent(value);
|
|
117
|
+
if (contentNode) sortObjectProperties(contentNode.content, orderingByNodeType[contentNode.nodeType]);
|
|
118
|
+
}
|
|
119
|
+
for (const [field, child] of Object.entries(value)) if (!IGNORED_FIELDS.has(field)) pendingValues.push(child);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
exports.getHelperCallContent = getHelperCallContent;
|
|
124
|
+
exports.getSerializedNodeContent = getSerializedNodeContent;
|
|
125
|
+
exports.getStaticKeyName = getStaticKeyName;
|
|
126
|
+
exports.isAstNode = isAstNode;
|
|
127
|
+
exports.isObjectExpression = isObjectExpression;
|
|
128
|
+
exports.isPlainProperty = isPlainProperty;
|
|
129
|
+
exports.sortContentNodesInAst = sortContentNodesInAst;
|
|
130
|
+
exports.sortObjectProperties = sortObjectProperties;
|
|
131
|
+
//# sourceMappingURL=sortContentNodes.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sortContentNodes.cjs","names":["createLocaleOrdering","orderPluralCategories","orderEnumerationConditions","orderSelectCases"],"sources":["../../src/sortContentNodes.ts"],"sourcesContent":["import {\n createLocaleOrdering,\n type KeyOrdering,\n orderEnumerationConditions,\n orderPluralCategories,\n orderSelectCases,\n} from './keyOrderings';\n\n/**\n * Minimal shape shared by the Babel, TypeScript-ESTree, Acorn and JSON ASTs\n * Prettier produces. Only the fields this module reads are typed.\n */\nexport type AstNode = {\n type: string;\n [field: string]: unknown;\n};\n\n/** Content node types whose keys get sorted. */\ntype SortedNodeType = 'translation' | 'plural' | 'enumeration' | 'select';\n\n/** Helper function name → content node type it declares. */\nconst NODE_TYPE_BY_FUNCTION_NAME: Record<string, SortedNodeType> = {\n t: 'translation',\n plural: 'plural',\n enu: 'enumeration',\n select: 'select',\n};\n\nconst SORTED_NODE_TYPES = new Set<string>(\n Object.values(NODE_TYPE_BY_FUNCTION_NAME)\n);\n\n/** Fields never walked: positions, comments and back-references. */\nconst IGNORED_FIELDS = new Set([\n 'loc',\n 'range',\n 'start',\n 'end',\n 'extra',\n 'parent',\n 'comments',\n 'leadingComments',\n 'trailingComments',\n 'innerComments',\n 'tokens',\n]);\n\nexport const isAstNode = (value: unknown): value is AstNode =>\n typeof value === 'object' &&\n value !== null &&\n typeof (value as AstNode).type === 'string';\n\nexport const isObjectExpression = (node: unknown): node is AstNode =>\n isAstNode(node) && node.type === 'ObjectExpression';\n\nconst getStringLiteralValue = (node: unknown): string | undefined =>\n isAstNode(node) &&\n (node.type === 'StringLiteral' || node.type === 'Literal') &&\n typeof node.value === 'string'\n ? node.value\n : undefined;\n\nconst getNumericLiteralKey = (node: AstNode): string | undefined =>\n (node.type === 'NumericLiteral' || node.type === 'Literal') &&\n typeof node.value === 'number'\n ? String(node.value)\n : undefined;\n\n/**\n * Static name of an object property key (`en`, `'en-GB'`, `2`), or\n * `undefined` when the key is computed.\n */\nexport const getStaticKeyName = (property: AstNode): string | undefined => {\n if (property.computed) return undefined;\n\n const key = property.key;\n\n if (!isAstNode(key)) return undefined;\n if (key.type === 'Identifier') return key.name as string;\n\n return getStringLiteralValue(key) ?? getNumericLiteralKey(key);\n};\n\nexport const isPlainProperty = (property: unknown): property is AstNode =>\n isAstNode(property) &&\n (property.type === 'Property' || property.type === 'ObjectProperty') &&\n property.kind !== 'get' &&\n property.kind !== 'set' &&\n !property.method;\n\n/**\n * Reorders the properties of a content map in place. Objects holding a\n * spread, a computed key or a method are left untouched: their order may be\n * meaningful.\n */\nexport const sortObjectProperties = (\n objectNode: AstNode,\n orderKeys: KeyOrdering\n): void => {\n const properties = objectNode.properties;\n\n if (!Array.isArray(properties) || properties.length < 2) return;\n if (!properties.every(isPlainProperty)) return;\n\n const propertyByName = new Map<string, AstNode>();\n\n for (const property of properties) {\n const name = getStaticKeyName(property);\n\n if (name === undefined || propertyByName.has(name)) return;\n\n propertyByName.set(name, property);\n }\n\n const orderedNames = orderKeys([...propertyByName.keys()]);\n\n if (!orderedNames) return;\n\n objectNode.properties = orderedNames.map(\n (name) => propertyByName.get(name) as AstNode\n );\n};\n\n/** `t({...})`, `plural({...})`, `enu({...})`, `select({...})`, `x.t({...})`. */\nexport const getHelperCallContent = (\n node: AstNode\n): { nodeType: SortedNodeType; content: AstNode } | undefined => {\n if (node.type !== 'CallExpression') return undefined;\n\n const callee = node.callee;\n\n if (!isAstNode(callee)) return undefined;\n\n const calleeName =\n callee.type === 'Identifier'\n ? callee.name\n : callee.type === 'MemberExpression' &&\n !callee.computed &&\n isAstNode(callee.property)\n ? callee.property.name\n : undefined;\n const nodeType =\n typeof calleeName === 'string'\n ? NODE_TYPE_BY_FUNCTION_NAME[calleeName]\n : undefined;\n const [firstArgument] = node.arguments as unknown[];\n\n if (!nodeType || !isObjectExpression(firstArgument)) return undefined;\n\n return { nodeType, content: firstArgument };\n};\n\n/**\n * `{ nodeType: 'plural', plural: {...} }`, the serialized form used by\n * `.content.json` files and the CMS.\n */\nexport const getSerializedNodeContent = (\n node: AstNode\n): { nodeType: SortedNodeType; content: AstNode } | undefined => {\n if (!isObjectExpression(node)) return undefined;\n\n const properties = (node.properties as unknown[]).filter(isPlainProperty);\n const findPropertyValue = (name: string): unknown =>\n properties.find((property) => getStaticKeyName(property) === name)?.value;\n const nodeType = getStringLiteralValue(findPropertyValue('nodeType'));\n\n if (!nodeType || !SORTED_NODE_TYPES.has(nodeType)) return undefined;\n\n const content = findPropertyValue(nodeType);\n\n return isObjectExpression(content)\n ? { nodeType: nodeType as SortedNodeType, content }\n : undefined;\n};\n\n/**\n * Walks a Prettier AST and sorts, in place, the keys of every Intlayer\n * translation (locales), plural (CLDR categories), enumeration (thresholds)\n * and select (cases).\n */\nexport const sortContentNodesInAst = (\n rootNode: unknown,\n pinnedLocales: readonly string[]\n): void => {\n const orderingByNodeType: Record<SortedNodeType, KeyOrdering> = {\n translation: createLocaleOrdering(pinnedLocales),\n plural: orderPluralCategories,\n enumeration: orderEnumerationConditions,\n select: orderSelectCases,\n };\n const visitedNodes = new WeakSet<object>();\n const pendingValues: unknown[] = [rootNode];\n\n while (pendingValues.length > 0) {\n const value = pendingValues.pop();\n\n if (typeof value !== 'object' || value === null) continue;\n if (visitedNodes.has(value)) continue;\n\n visitedNodes.add(value);\n\n if (Array.isArray(value)) {\n pendingValues.push(...value);\n continue;\n }\n\n if (isAstNode(value)) {\n const contentNode =\n getHelperCallContent(value) ?? getSerializedNodeContent(value);\n\n if (contentNode) {\n sortObjectProperties(\n contentNode.content,\n orderingByNodeType[contentNode.nodeType]\n );\n }\n }\n\n for (const [field, child] of Object.entries(value)) {\n if (!IGNORED_FIELDS.has(field)) pendingValues.push(child);\n }\n }\n};\n"],"mappings":"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"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// ../@intlayer/core/dist/esm/interpreter/getEnumeration.mjs
|
|
2
|
+
/**
|
|
3
|
+
* Find the matching condition for a quantity.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* const key = findMatchingCondition({
|
|
9
|
+
* '<=-2.3': 'You have less than -2.3',
|
|
10
|
+
* '<1': 'You have less than one',
|
|
11
|
+
* '2': 'You have two',
|
|
12
|
+
* '>=3': 'You have three or more',
|
|
13
|
+
* }, 2);
|
|
14
|
+
* // '2'
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* The order of the keys will define the priority of the content.
|
|
18
|
+
*
|
|
19
|
+
* ```ts
|
|
20
|
+
* const key = findMatchingCondition({
|
|
21
|
+
* '<4': 'You have less than four',
|
|
22
|
+
* '2': 'You have two',
|
|
23
|
+
* }, 2);
|
|
24
|
+
* // '<4'
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* If no keys match, the default key is '1'.
|
|
28
|
+
*/
|
|
29
|
+
const findMatchingCondition = (enumerationContent, quantity) => {
|
|
30
|
+
const numericKeys = Object.keys(enumerationContent);
|
|
31
|
+
for (const key of numericKeys) {
|
|
32
|
+
const isEqual = !key.startsWith(">") && !key.startsWith("<") && !key.startsWith("=") && parseFloat(key) === quantity || key.startsWith("=") && parseFloat(key.slice(1)) === quantity;
|
|
33
|
+
const isSuperior = key.startsWith(">") && quantity > parseFloat(key.slice(1));
|
|
34
|
+
const isSuperiorOrEqual = key.startsWith(">=") && quantity >= parseFloat(key.slice(2));
|
|
35
|
+
const isInferior = key.startsWith("<") && quantity < parseFloat(key.slice(1));
|
|
36
|
+
const isInferiorOrEqual = key.startsWith("<=") && quantity <= parseFloat(key.slice(2));
|
|
37
|
+
if (isEqual || isSuperior || isSuperiorOrEqual || isInferior || isInferiorOrEqual) return key;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export { findMatchingCondition };
|
|
42
|
+
//# sourceMappingURL=getEnumeration.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"getEnumeration.mjs","names":[],"sources":["../../../../../../../../@intlayer/core/dist/esm/interpreter/getEnumeration.mjs"],"sourcesContent":["// src/interpreter/getEnumeration.ts\n/**\n* Find the matching condition for a quantity.\n*\n* Usage:\n*\n* ```ts\n* const key = findMatchingCondition({\n* '<=-2.3': 'You have less than -2.3',\n* '<1': 'You have less than one',\n* '2': 'You have two',\n* '>=3': 'You have three or more',\n* }, 2);\n* // '2'\n* ```\n*\n* The order of the keys will define the priority of the content.\n*\n* ```ts\n* const key = findMatchingCondition({\n* '<4': 'You have less than four',\n* '2': 'You have two',\n* }, 2);\n* // '<4'\n* ```\n*\n* If no keys match, the default key is '1'.\n*/\nconst findMatchingCondition = (enumerationContent, quantity) => {\n\tconst numericKeys = Object.keys(enumerationContent);\n\tfor (const key of numericKeys) {\n\t\tconst isEqual = !key.startsWith(\">\") && !key.startsWith(\"<\") && !key.startsWith(\"=\") && parseFloat(key) === quantity || key.startsWith(\"=\") && parseFloat(key.slice(1)) === quantity;\n\t\tconst isSuperior = key.startsWith(\">\") && quantity > parseFloat(key.slice(1));\n\t\tconst isSuperiorOrEqual = key.startsWith(\">=\") && quantity >= parseFloat(key.slice(2));\n\t\tconst isInferior = key.startsWith(\"<\") && quantity < parseFloat(key.slice(1));\n\t\tconst isInferiorOrEqual = key.startsWith(\"<=\") && quantity <= parseFloat(key.slice(2));\n\t\tif (isEqual || isSuperior || isSuperiorOrEqual || isInferior || isInferiorOrEqual) return key;\n\t}\n};\n/**\n* Picks content from an enumeration map based on a provided quantity.\n*\n* Supported keys in the enumeration map:\n* - Specific numbers: '0', '1', '2'\n* - Comparison operators: '<5', '>=10', '<=2'\n* - Fallback: 'fallback'\n*\n* The first matching key in the object's iteration order will be selected.\n*\n* @param enumerationContent - A map of conditions/quantities to content.\n* @param quantity - The number to match against the conditions.\n* @returns The matching content.\n*\n* @example\n* ```ts\n* const content = getEnumeration({\n* '0': 'No items',\n* '1': 'One item',\n* '>1': 'Many items',\n* }, 5);\n* // 'Many items'\n* ```\n*/\nconst getEnumeration = (enumerationContent, quantity) => {\n\treturn enumerationContent[findMatchingCondition(enumerationContent, quantity) ?? \"fallback\"];\n};\n\nexport { findMatchingCondition, getEnumeration };\n//# sourceMappingURL=getEnumeration.mjs.map"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BA,MAAM,yBAAyB,oBAAoB,aAAa;CAC/D,MAAM,cAAc,OAAO,KAAK,kBAAkB;CAClD,KAAK,MAAM,OAAO,aAAa;EAC9B,MAAM,UAAU,CAAC,IAAI,WAAW,GAAG,KAAK,CAAC,IAAI,WAAW,GAAG,KAAK,CAAC,IAAI,WAAW,GAAG,KAAK,WAAW,GAAG,MAAM,YAAY,IAAI,WAAW,GAAG,KAAK,WAAW,IAAI,MAAM,CAAC,CAAC,MAAM;EAC5K,MAAM,aAAa,IAAI,WAAW,GAAG,KAAK,WAAW,WAAW,IAAI,MAAM,CAAC,CAAC;EAC5E,MAAM,oBAAoB,IAAI,WAAW,IAAI,KAAK,YAAY,WAAW,IAAI,MAAM,CAAC,CAAC;EACrF,MAAM,aAAa,IAAI,WAAW,GAAG,KAAK,WAAW,WAAW,IAAI,MAAM,CAAC,CAAC;EAC5E,MAAM,oBAAoB,IAAI,WAAW,IAAI,KAAK,YAAY,WAAW,IAAI,MAAM,CAAC,CAAC;EACrF,IAAI,WAAW,cAAc,qBAAqB,cAAc,mBAAmB,OAAO;CAC3F;AACD"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { getHelperCallContent, getSerializedNodeContent, getStaticKeyName, isAstNode, isPlainProperty } from "./sortContentNodes.mjs";
|
|
2
|
+
|
|
3
|
+
// src/collapseTranslations.ts
|
|
4
|
+
/** Fields of a serialized translation node that may be collapsed. */
|
|
5
|
+
const SERIALIZED_TRANSLATION_FIELDS = /* @__PURE__ */ new Set(["nodeType", "translation"]);
|
|
6
|
+
/** Start / end offsets, from Babel (`start`/`end`) or ESTree (`range`). */
|
|
7
|
+
const getNodeBounds = (node) => {
|
|
8
|
+
if (Array.isArray(node.range)) return node.range;
|
|
9
|
+
if (typeof node.start === "number" && typeof node.end === "number") return [node.start, node.end];
|
|
10
|
+
};
|
|
11
|
+
const isStringLiteral = (node) => isAstNode(node) && (node.type === "StringLiteral" || node.type === "Literal") && typeof node.value === "string";
|
|
12
|
+
/**
|
|
13
|
+
* The string literal every locale shares, or `undefined` when the locales
|
|
14
|
+
* differ, hold anything but a plain string, or there is only one locale.
|
|
15
|
+
*/
|
|
16
|
+
const getSharedStringLiteral = (translationMap) => {
|
|
17
|
+
const properties = translationMap.properties;
|
|
18
|
+
if (properties.length < 2 || !properties.every(isPlainProperty)) return;
|
|
19
|
+
const values = properties.map((property) => property.value);
|
|
20
|
+
if (!values.every(isStringLiteral)) return void 0;
|
|
21
|
+
const [firstValue] = values;
|
|
22
|
+
return values.every((value) => value.value === firstValue.value) ? firstValue : void 0;
|
|
23
|
+
};
|
|
24
|
+
/** Replaces a node in place by another one, keeping the object identity. */
|
|
25
|
+
const replaceNodeInPlace = (target, replacement) => {
|
|
26
|
+
for (const field of Object.keys(target)) delete target[field];
|
|
27
|
+
Object.assign(target, replacement);
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* The node to collapse and the literal replacing it: a `t({...})` call, or a
|
|
31
|
+
* serialized `{ nodeType: 'translation', translation: {...} }` object.
|
|
32
|
+
*/
|
|
33
|
+
const getCollapsibleTranslation = (node) => {
|
|
34
|
+
const helperContent = getHelperCallContent(node);
|
|
35
|
+
if (helperContent) return helperContent.nodeType === "translation" && node.arguments.length === 1 && !node.typeArguments && !node.typeParameters ? getSharedStringLiteral(helperContent.content) : void 0;
|
|
36
|
+
const serializedContent = getSerializedNodeContent(node);
|
|
37
|
+
if (serializedContent?.nodeType !== "translation") return void 0;
|
|
38
|
+
return node.properties.every((property) => isPlainProperty(property) && SERIALIZED_TRANSLATION_FIELDS.has(getStaticKeyName(property) ?? "")) ? getSharedStringLiteral(serializedContent.content) : void 0;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Replaces, in place, every translation whose locales all hold the same
|
|
42
|
+
* string by that string: `t({ en: 'Next.js', fr: 'Next.js' })` → `'Next.js'`.
|
|
43
|
+
*
|
|
44
|
+
* Translations holding a comment are kept, so no comment is lost.
|
|
45
|
+
*/
|
|
46
|
+
const collapseIdenticalTranslationsInAst = (rootNode) => {
|
|
47
|
+
const commentBounds = (isAstNode(rootNode) && Array.isArray(rootNode.comments) ? rootNode.comments : []).map(getNodeBounds).filter((bounds) => bounds !== void 0);
|
|
48
|
+
const containsComment = (node) => {
|
|
49
|
+
const bounds = getNodeBounds(node);
|
|
50
|
+
if (!bounds) return true;
|
|
51
|
+
return commentBounds.some(([commentStart, commentEnd]) => commentStart >= bounds[0] && commentEnd <= bounds[1]);
|
|
52
|
+
};
|
|
53
|
+
const visitedNodes = /* @__PURE__ */ new WeakSet();
|
|
54
|
+
const pendingValues = [rootNode];
|
|
55
|
+
while (pendingValues.length > 0) {
|
|
56
|
+
const value = pendingValues.pop();
|
|
57
|
+
if (typeof value !== "object" || value === null) continue;
|
|
58
|
+
if (visitedNodes.has(value)) continue;
|
|
59
|
+
visitedNodes.add(value);
|
|
60
|
+
if (isAstNode(value)) {
|
|
61
|
+
const sharedLiteral = getCollapsibleTranslation(value);
|
|
62
|
+
if (sharedLiteral && !containsComment(value)) {
|
|
63
|
+
replaceNodeInPlace(value, { ...sharedLiteral });
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
for (const [field, child] of Object.entries(value)) if (field !== "loc" && field !== "range" && field !== "parent") pendingValues.push(child);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export { collapseIdenticalTranslationsInAst };
|
|
72
|
+
//# sourceMappingURL=collapseTranslations.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"collapseTranslations.mjs","names":[],"sources":["../../src/collapseTranslations.ts"],"sourcesContent":["import {\n type AstNode,\n getHelperCallContent,\n getSerializedNodeContent,\n getStaticKeyName,\n isAstNode,\n isPlainProperty,\n} from './sortContentNodes';\n\n/** Fields of a serialized translation node that may be collapsed. */\nconst SERIALIZED_TRANSLATION_FIELDS = new Set(['nodeType', 'translation']);\n\n/** Start / end offsets, from Babel (`start`/`end`) or ESTree (`range`). */\nconst getNodeBounds = (node: AstNode): [number, number] | undefined => {\n if (Array.isArray(node.range)) return node.range as [number, number];\n if (typeof node.start === 'number' && typeof node.end === 'number') {\n return [node.start, node.end];\n }\n\n return undefined;\n};\n\nconst isStringLiteral = (node: unknown): node is AstNode =>\n isAstNode(node) &&\n (node.type === 'StringLiteral' || node.type === 'Literal') &&\n typeof node.value === 'string';\n\n/**\n * The string literal every locale shares, or `undefined` when the locales\n * differ, hold anything but a plain string, or there is only one locale.\n */\nconst getSharedStringLiteral = (\n translationMap: AstNode\n): AstNode | undefined => {\n const properties = translationMap.properties as unknown[];\n\n if (properties.length < 2 || !properties.every(isPlainProperty)) {\n return undefined;\n }\n\n const values = (properties as AstNode[]).map((property) => property.value);\n\n if (!values.every(isStringLiteral)) return undefined;\n\n const [firstValue] = values;\n\n return values.every((value) => value.value === firstValue.value)\n ? firstValue\n : undefined;\n};\n\n/** Replaces a node in place by another one, keeping the object identity. */\nconst replaceNodeInPlace = (target: AstNode, replacement: AstNode): void => {\n for (const field of Object.keys(target)) {\n delete target[field];\n }\n\n Object.assign(target, replacement);\n};\n\n/**\n * The node to collapse and the literal replacing it: a `t({...})` call, or a\n * serialized `{ nodeType: 'translation', translation: {...} }` object.\n */\nconst getCollapsibleTranslation = (node: AstNode): AstNode | undefined => {\n const helperContent = getHelperCallContent(node);\n\n if (helperContent) {\n const isPlainCall =\n helperContent.nodeType === 'translation' &&\n (node.arguments as unknown[]).length === 1 &&\n !node.typeArguments &&\n !node.typeParameters;\n\n return isPlainCall\n ? getSharedStringLiteral(helperContent.content)\n : undefined;\n }\n\n const serializedContent = getSerializedNodeContent(node);\n\n if (serializedContent?.nodeType !== 'translation') return undefined;\n\n const hasOnlyTranslationFields = (node.properties as unknown[]).every(\n (property) =>\n isPlainProperty(property) &&\n SERIALIZED_TRANSLATION_FIELDS.has(getStaticKeyName(property) ?? '')\n );\n\n return hasOnlyTranslationFields\n ? getSharedStringLiteral(serializedContent.content)\n : undefined;\n};\n\n/**\n * Replaces, in place, every translation whose locales all hold the same\n * string by that string: `t({ en: 'Next.js', fr: 'Next.js' })` → `'Next.js'`.\n *\n * Translations holding a comment are kept, so no comment is lost.\n */\nexport const collapseIdenticalTranslationsInAst = (rootNode: unknown): void => {\n const commentBounds = (\n isAstNode(rootNode) && Array.isArray(rootNode.comments)\n ? (rootNode.comments as AstNode[])\n : []\n )\n .map(getNodeBounds)\n .filter((bounds): bounds is [number, number] => bounds !== undefined);\n\n const containsComment = (node: AstNode): boolean => {\n const bounds = getNodeBounds(node);\n\n if (!bounds) return true;\n\n return commentBounds.some(\n ([commentStart, commentEnd]) =>\n commentStart >= bounds[0] && commentEnd <= bounds[1]\n );\n };\n\n const visitedNodes = new WeakSet<object>();\n const pendingValues: unknown[] = [rootNode];\n\n while (pendingValues.length > 0) {\n const value = pendingValues.pop();\n\n if (typeof value !== 'object' || value === null) continue;\n if (visitedNodes.has(value)) continue;\n\n visitedNodes.add(value);\n\n if (isAstNode(value)) {\n const sharedLiteral = getCollapsibleTranslation(value);\n\n if (sharedLiteral && !containsComment(value)) {\n replaceNodeInPlace(value, { ...sharedLiteral });\n continue;\n }\n }\n\n for (const [field, child] of Object.entries(value)) {\n if (field !== 'loc' && field !== 'range' && field !== 'parent') {\n pendingValues.push(child);\n }\n }\n }\n};\n"],"mappings":";;;;AAUA,MAAM,gDAAgC,IAAI,IAAI,CAAC,YAAY,aAAa,CAAC;;AAGzE,MAAM,iBAAiB,SAAgD;CACrE,IAAI,MAAM,QAAQ,KAAK,KAAK,GAAG,OAAO,KAAK;CAC3C,IAAI,OAAO,KAAK,UAAU,YAAY,OAAO,KAAK,QAAQ,UACxD,OAAO,CAAC,KAAK,OAAO,KAAK,GAAG;AAIhC;AAEA,MAAM,mBAAmB,SACvB,UAAU,IAAI,MACb,KAAK,SAAS,mBAAmB,KAAK,SAAS,cAChD,OAAO,KAAK,UAAU;;;;;AAMxB,MAAM,0BACJ,mBACwB;CACxB,MAAM,aAAa,eAAe;CAElC,IAAI,WAAW,SAAS,KAAK,CAAC,WAAW,MAAM,eAAe,GAC5D;CAGF,MAAM,SAAU,WAAyB,KAAK,aAAa,SAAS,KAAK;CAEzE,IAAI,CAAC,OAAO,MAAM,eAAe,GAAG,OAAO;CAE3C,MAAM,CAAC,cAAc;CAErB,OAAO,OAAO,OAAO,UAAU,MAAM,UAAU,WAAW,KAAK,IAC3D,aACA;AACN;;AAGA,MAAM,sBAAsB,QAAiB,gBAA+B;CAC1E,KAAK,MAAM,SAAS,OAAO,KAAK,MAAM,GACpC,OAAO,OAAO;CAGhB,OAAO,OAAO,QAAQ,WAAW;AACnC;;;;;AAMA,MAAM,6BAA6B,SAAuC;CACxE,MAAM,gBAAgB,qBAAqB,IAAI;CAE/C,IAAI,eAOF,OALE,cAAc,aAAa,iBAC1B,KAAK,UAAwB,WAAW,KACzC,CAAC,KAAK,iBACN,CAAC,KAAK,iBAGJ,uBAAuB,cAAc,OAAO,IAC5C;CAGN,MAAM,oBAAoB,yBAAyB,IAAI;CAEvD,IAAI,mBAAmB,aAAa,eAAe,OAAO;CAQ1D,OANkC,KAAK,WAAyB,OAC7D,aACC,gBAAgB,QAAQ,KACxB,8BAA8B,IAAI,iBAAiB,QAAQ,KAAK,EAAE,CAGxC,IAC1B,uBAAuB,kBAAkB,OAAO,IAChD;AACN;;;;;;;AAQA,MAAa,sCAAsC,aAA4B;CAC7E,MAAM,iBACJ,UAAU,QAAQ,KAAK,MAAM,QAAQ,SAAS,QAAQ,IACjD,SAAS,WACV,CAAC,EAAC,CAEL,IAAI,aAAa,CAAC,CAClB,QAAQ,WAAuC,WAAW,MAAS;CAEtE,MAAM,mBAAmB,SAA2B;EAClD,MAAM,SAAS,cAAc,IAAI;EAEjC,IAAI,CAAC,QAAQ,OAAO;EAEpB,OAAO,cAAc,MAClB,CAAC,cAAc,gBACd,gBAAgB,OAAO,MAAM,cAAc,OAAO,EACtD;CACF;CAEA,MAAM,+BAAe,IAAI,QAAgB;CACzC,MAAM,gBAA2B,CAAC,QAAQ;CAE1C,OAAO,cAAc,SAAS,GAAG;EAC/B,MAAM,QAAQ,cAAc,IAAI;EAEhC,IAAI,OAAO,UAAU,YAAY,UAAU,MAAM;EACjD,IAAI,aAAa,IAAI,KAAK,GAAG;EAE7B,aAAa,IAAI,KAAK;EAEtB,IAAI,UAAU,KAAK,GAAG;GACpB,MAAM,gBAAgB,0BAA0B,KAAK;GAErD,IAAI,iBAAiB,CAAC,gBAAgB,KAAK,GAAG;IAC5C,mBAAmB,OAAO,EAAE,GAAG,cAAc,CAAC;IAC9C;GACF;EACF;EAEA,KAAK,MAAM,CAAC,OAAO,UAAU,OAAO,QAAQ,KAAK,GAC/C,IAAI,UAAU,SAAS,UAAU,WAAW,UAAU,UACpD,cAAc,KAAK,KAAK;CAG9B;AACF"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { collapseIdenticalTranslationsInAst } from "./collapseTranslations.mjs";
|
|
2
|
+
import { isContentFile, sortContentKeysInAst } from "./sortContentKeys.mjs";
|
|
3
|
+
import { sortContentNodesInAst } from "./sortContentNodes.mjs";
|
|
4
|
+
import { PLURAL_CATEGORY_ORDER, createLocaleComparator, orderAlphabetically, orderEnumerationConditions, orderPluralCategories, orderSelectCases } from "./keyOrderings.mjs";
|
|
5
|
+
import { parsers as parsers$1 } from "prettier/plugins/acorn";
|
|
6
|
+
import { parsers as parsers$2 } from "prettier/plugins/babel";
|
|
7
|
+
import { parsers as parsers$3 } from "prettier/plugins/flow";
|
|
8
|
+
import { parsers as parsers$4 } from "prettier/plugins/meriyah";
|
|
9
|
+
import { parsers as parsers$5 } from "prettier/plugins/typescript";
|
|
10
|
+
|
|
11
|
+
// src/index.ts
|
|
12
|
+
/** Locales kept at the top of every translation when none are configured. */
|
|
13
|
+
const DEFAULT_PINNED_LOCALES = ["en"];
|
|
14
|
+
const readPinnedLocales = (options) => options.intlayerPinnedLocales ?? DEFAULT_PINNED_LOCALES;
|
|
15
|
+
/** Wraps a built-in parser so the parsed AST gets its content nodes sorted. */
|
|
16
|
+
const withSortedTranslations = (parser) => ({
|
|
17
|
+
...parser,
|
|
18
|
+
parse: async (text, options) => {
|
|
19
|
+
const ast = await parser.parse(text, options);
|
|
20
|
+
const pluginOptions = options;
|
|
21
|
+
const isContentDeclaration = isContentFile(options.filepath);
|
|
22
|
+
if (pluginOptions.intlayerCollapseIdenticalTranslations !== false && isContentDeclaration) collapseIdenticalTranslationsInAst(ast);
|
|
23
|
+
sortContentNodesInAst(ast, readPinnedLocales(options));
|
|
24
|
+
if (pluginOptions.intlayerSortContentKeys && isContentDeclaration) sortContentKeysInAst(ast);
|
|
25
|
+
return ast;
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
const wrapParsers = (sourceParsers, parserNames) => Object.fromEntries(parserNames.map((parserName) => [parserName, withSortedTranslations(sourceParsers[parserName])]));
|
|
29
|
+
const parsers = {
|
|
30
|
+
...wrapParsers(parsers$2, [
|
|
31
|
+
"babel",
|
|
32
|
+
"babel-flow",
|
|
33
|
+
"babel-ts",
|
|
34
|
+
"json",
|
|
35
|
+
"json5",
|
|
36
|
+
"jsonc"
|
|
37
|
+
]),
|
|
38
|
+
...wrapParsers(parsers$5, ["typescript"]),
|
|
39
|
+
...wrapParsers(parsers$1, ["acorn", "espree"]),
|
|
40
|
+
...wrapParsers(parsers$3, ["flow"]),
|
|
41
|
+
...wrapParsers(parsers$4, ["meriyah"])
|
|
42
|
+
};
|
|
43
|
+
const options = {
|
|
44
|
+
intlayerPinnedLocales: {
|
|
45
|
+
type: "string",
|
|
46
|
+
array: true,
|
|
47
|
+
category: "Intlayer",
|
|
48
|
+
default: [{ value: [...DEFAULT_PINNED_LOCALES] }],
|
|
49
|
+
description: "Locales kept first, in this order, in every t() translation. The other locales are sorted alphabetically."
|
|
50
|
+
},
|
|
51
|
+
intlayerCollapseIdenticalTranslations: {
|
|
52
|
+
type: "boolean",
|
|
53
|
+
category: "Intlayer",
|
|
54
|
+
default: true,
|
|
55
|
+
description: "In .content files, replace a t() whose locales all hold the same string by that string: t({ en: 'Next.js', fr: 'Next.js' }) becomes 'Next.js'."
|
|
56
|
+
},
|
|
57
|
+
intlayerSortContentKeys: {
|
|
58
|
+
type: "boolean",
|
|
59
|
+
category: "Intlayer",
|
|
60
|
+
default: false,
|
|
61
|
+
description: "In .content files, also sort the keys of the dictionary content and of its nested objects alphabetically."
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
const plugin = {
|
|
65
|
+
parsers,
|
|
66
|
+
options
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export { DEFAULT_PINNED_LOCALES, PLURAL_CATEGORY_ORDER, collapseIdenticalTranslationsInAst, createLocaleComparator, plugin as default, isContentFile, options, orderAlphabetically, orderEnumerationConditions, orderPluralCategories, orderSelectCases, parsers, sortContentKeysInAst, sortContentNodesInAst };
|
|
70
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["babelParsers","typescriptParsers","acornParsers","flowParsers","meriyahParsers"],"sources":["../../src/index.ts"],"sourcesContent":["import type { Parser, ParserOptions, Plugin, SupportOptions } from 'prettier';\nimport { parsers as acornParsers } from 'prettier/plugins/acorn';\nimport { parsers as babelParsers } from 'prettier/plugins/babel';\nimport { parsers as flowParsers } from 'prettier/plugins/flow';\nimport { parsers as meriyahParsers } from 'prettier/plugins/meriyah';\nimport { parsers as typescriptParsers } from 'prettier/plugins/typescript';\nimport { collapseIdenticalTranslationsInAst } from './collapseTranslations';\nimport { isContentFile, sortContentKeysInAst } from './sortContentKeys';\nimport { sortContentNodesInAst } from './sortContentNodes';\n\nexport { collapseIdenticalTranslationsInAst } from './collapseTranslations';\nexport {\n createLocaleComparator,\n orderAlphabetically,\n orderEnumerationConditions,\n orderPluralCategories,\n orderSelectCases,\n PLURAL_CATEGORY_ORDER,\n} from './keyOrderings';\nexport { isContentFile, sortContentKeysInAst } from './sortContentKeys';\nexport { sortContentNodesInAst } from './sortContentNodes';\n\n/** Locales kept at the top of every translation when none are configured. */\nexport const DEFAULT_PINNED_LOCALES = ['en'] as const;\n\ntype IntlayerPluginOptions = ParserOptions & {\n intlayerPinnedLocales?: string[];\n intlayerSortContentKeys?: boolean;\n intlayerCollapseIdenticalTranslations?: boolean;\n};\n\nconst readPinnedLocales = (options: ParserOptions): readonly string[] =>\n (options as IntlayerPluginOptions).intlayerPinnedLocales ??\n DEFAULT_PINNED_LOCALES;\n\n/** Wraps a built-in parser so the parsed AST gets its content nodes sorted. */\nconst withSortedTranslations = <AstType>(\n parser: Parser<AstType>\n): Parser<AstType> => ({\n ...parser,\n parse: async (text, options) => {\n const ast = await parser.parse(text, options);\n const pluginOptions = options as IntlayerPluginOptions;\n const isContentDeclaration = isContentFile(options.filepath);\n\n if (\n pluginOptions.intlayerCollapseIdenticalTranslations !== false &&\n isContentDeclaration\n ) {\n collapseIdenticalTranslationsInAst(ast);\n }\n\n sortContentNodesInAst(ast, readPinnedLocales(options));\n\n if (pluginOptions.intlayerSortContentKeys && isContentDeclaration) {\n sortContentKeysInAst(ast);\n }\n\n return ast;\n },\n});\n\nconst wrapParsers = (\n sourceParsers: Record<string, Parser>,\n parserNames: readonly string[]\n): Record<string, Parser> =>\n Object.fromEntries(\n parserNames.map((parserName) => [\n parserName,\n withSortedTranslations(sourceParsers[parserName]),\n ])\n );\n\nexport const parsers: Plugin['parsers'] = {\n ...wrapParsers(babelParsers, [\n 'babel',\n 'babel-flow',\n 'babel-ts',\n 'json',\n 'json5',\n 'jsonc',\n ]),\n ...wrapParsers(typescriptParsers, ['typescript']),\n ...wrapParsers(acornParsers, ['acorn', 'espree']),\n ...wrapParsers(flowParsers, ['flow']),\n ...wrapParsers(meriyahParsers, ['meriyah']),\n};\n\nexport const options: SupportOptions = {\n intlayerPinnedLocales: {\n type: 'string',\n array: true,\n category: 'Intlayer',\n default: [{ value: [...DEFAULT_PINNED_LOCALES] }],\n description:\n 'Locales kept first, in this order, in every t() translation. The other locales are sorted alphabetically.',\n },\n intlayerCollapseIdenticalTranslations: {\n type: 'boolean',\n category: 'Intlayer',\n default: true,\n description:\n \"In .content files, replace a t() whose locales all hold the same string by that string: t({ en: 'Next.js', fr: 'Next.js' }) becomes 'Next.js'.\",\n },\n intlayerSortContentKeys: {\n type: 'boolean',\n category: 'Intlayer',\n default: false,\n description:\n 'In .content files, also sort the keys of the dictionary content and of its nested objects alphabetically.',\n },\n};\n\nconst plugin: Plugin = { parsers, options };\n\nexport default plugin;\n"],"mappings":";;;;;;;;;;;;AAuBA,MAAa,yBAAyB,CAAC,IAAI;AAQ3C,MAAM,qBAAqB,YACxB,QAAkC,yBACnC;;AAGF,MAAM,0BACJ,YACqB;CACrB,GAAG;CACH,OAAO,OAAO,MAAM,YAAY;EAC9B,MAAM,MAAM,MAAM,OAAO,MAAM,MAAM,OAAO;EAC5C,MAAM,gBAAgB;EACtB,MAAM,uBAAuB,cAAc,QAAQ,QAAQ;EAE3D,IACE,cAAc,0CAA0C,SACxD,sBAEA,mCAAmC,GAAG;EAGxC,sBAAsB,KAAK,kBAAkB,OAAO,CAAC;EAErD,IAAI,cAAc,2BAA2B,sBAC3C,qBAAqB,GAAG;EAG1B,OAAO;CACT;AACF;AAEA,MAAM,eACJ,eACA,gBAEA,OAAO,YACL,YAAY,KAAK,eAAe,CAC9B,YACA,uBAAuB,cAAc,WAAW,CAClD,CAAC,CACH;AAEF,MAAa,UAA6B;CACxC,GAAG,YAAYA,WAAc;EAC3B;EACA;EACA;EACA;EACA;EACA;CACF,CAAC;CACD,GAAG,YAAYC,WAAmB,CAAC,YAAY,CAAC;CAChD,GAAG,YAAYC,WAAc,CAAC,SAAS,QAAQ,CAAC;CAChD,GAAG,YAAYC,WAAa,CAAC,MAAM,CAAC;CACpC,GAAG,YAAYC,WAAgB,CAAC,SAAS,CAAC;AAC5C;AAEA,MAAa,UAA0B;CACrC,uBAAuB;EACrB,MAAM;EACN,OAAO;EACP,UAAU;EACV,SAAS,CAAC,EAAE,OAAO,CAAC,GAAG,sBAAsB,EAAE,CAAC;EAChD,aACE;CACJ;CACA,uCAAuC;EACrC,MAAM;EACN,UAAU;EACV,SAAS;EACT,aACE;CACJ;CACA,yBAAyB;EACvB,MAAM;EACN,UAAU;EACV,SAAS;EACT,aACE;CACJ;AACF;AAEA,MAAM,SAAiB;CAAE;CAAS;AAAQ"}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { findMatchingCondition } from "./@intlayer/core/dist/esm/interpreter/getEnumeration.mjs";
|
|
2
|
+
|
|
3
|
+
// src/keyOrderings.ts
|
|
4
|
+
const compareStrings = (stringA, stringB) => {
|
|
5
|
+
if (stringA === stringB) return 0;
|
|
6
|
+
return stringA < stringB ? -1 : 1;
|
|
7
|
+
};
|
|
8
|
+
/** Plain alphabetical order, for the keys of a content object. */
|
|
9
|
+
const orderAlphabetically = (keys) => [...keys].sort(compareStrings);
|
|
10
|
+
/**
|
|
11
|
+
* Builds the comparator ordering locales: pinned locales first (in the given
|
|
12
|
+
* order), then every other locale alphabetically.
|
|
13
|
+
*/
|
|
14
|
+
const createLocaleComparator = (pinnedLocales) => {
|
|
15
|
+
const pinnedRank = new Map(pinnedLocales.map((locale, index) => [locale, index]));
|
|
16
|
+
return (localeA, localeB) => {
|
|
17
|
+
const rankA = pinnedRank.get(localeA) ?? Number.POSITIVE_INFINITY;
|
|
18
|
+
const rankB = pinnedRank.get(localeB) ?? Number.POSITIVE_INFINITY;
|
|
19
|
+
if (rankA !== rankB) return rankA < rankB ? -1 : 1;
|
|
20
|
+
return compareStrings(localeA, localeB);
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
/** Locale order for `t()`: pinned locales, then alphabetical. */
|
|
24
|
+
const createLocaleOrdering = (pinnedLocales) => {
|
|
25
|
+
const compareLocales = createLocaleComparator(pinnedLocales);
|
|
26
|
+
return (keys) => [...keys].sort(compareLocales);
|
|
27
|
+
};
|
|
28
|
+
/** CLDR plural categories, from the smallest quantity to the fallback. */
|
|
29
|
+
const PLURAL_CATEGORY_ORDER = [
|
|
30
|
+
"zero",
|
|
31
|
+
"one",
|
|
32
|
+
"two",
|
|
33
|
+
"few",
|
|
34
|
+
"many",
|
|
35
|
+
"other"
|
|
36
|
+
];
|
|
37
|
+
/**
|
|
38
|
+
* CLDR order for `plural()`. Unknown keys leave the object untouched.
|
|
39
|
+
*/
|
|
40
|
+
const orderPluralCategories = (keys) => {
|
|
41
|
+
const categoryRank = new Map(PLURAL_CATEGORY_ORDER.map((category, index) => [category, index]));
|
|
42
|
+
if (!keys.every((key) => categoryRank.has(key))) return void 0;
|
|
43
|
+
return [...keys].sort((keyA, keyB) => categoryRank.get(keyA) - categoryRank.get(keyB));
|
|
44
|
+
};
|
|
45
|
+
const FALLBACK_KEY = "fallback";
|
|
46
|
+
const ENUMERATION_CONDITION_PATTERN = /^(<=|>=|<|>|=)?(-?\d+(?:\.\d+)?)$/;
|
|
47
|
+
/** Rank of each operator for one threshold: `<1`, `<=1`, `1`, `>=1`, `>1`. */
|
|
48
|
+
const OPERATOR_RANK = {
|
|
49
|
+
"<": 0,
|
|
50
|
+
"<=": 1,
|
|
51
|
+
"": 2,
|
|
52
|
+
"=": 2,
|
|
53
|
+
">=": 3,
|
|
54
|
+
">": 4
|
|
55
|
+
};
|
|
56
|
+
const parseEnumerationCondition = (key) => {
|
|
57
|
+
const match = ENUMERATION_CONDITION_PATTERN.exec(key);
|
|
58
|
+
if (!match) return void 0;
|
|
59
|
+
return {
|
|
60
|
+
key,
|
|
61
|
+
threshold: Number(match[2]),
|
|
62
|
+
operatorRank: OPERATOR_RANK[match[1] ?? ""]
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Keys in the order the runtime iterates them: JavaScript lists integer-like
|
|
67
|
+
* keys first, ascending, whatever their position in the source.
|
|
68
|
+
*/
|
|
69
|
+
const toRuntimeKeyOrder = (keys) => Object.keys(Object.fromEntries(keys.map((key) => [key, true])));
|
|
70
|
+
/**
|
|
71
|
+
* Quantities covering every interval the conditions can distinguish: each
|
|
72
|
+
* threshold, the midpoint between two thresholds, and one value beyond each
|
|
73
|
+
* end.
|
|
74
|
+
*/
|
|
75
|
+
const getProbeQuantities = (conditions) => {
|
|
76
|
+
const thresholds = [...new Set(conditions.map((condition) => condition.threshold))].sort((thresholdA, thresholdB) => thresholdA - thresholdB);
|
|
77
|
+
if (thresholds.length === 0) return [];
|
|
78
|
+
const midpoints = thresholds.slice(1).map((threshold, index) => (thresholds[index] + threshold) / 2);
|
|
79
|
+
return [
|
|
80
|
+
thresholds[0] - 1,
|
|
81
|
+
...thresholds,
|
|
82
|
+
...midpoints,
|
|
83
|
+
thresholds[thresholds.length - 1] + 1
|
|
84
|
+
];
|
|
85
|
+
};
|
|
86
|
+
const resolvesIdentically = (originalKeys, reorderedKeys, quantities) => {
|
|
87
|
+
const toContentState = (keys) => Object.fromEntries(toRuntimeKeyOrder(keys).map((key) => [key, key]));
|
|
88
|
+
const originalContent = toContentState(originalKeys);
|
|
89
|
+
const reorderedContent = toContentState(reorderedKeys);
|
|
90
|
+
return quantities.every((quantity) => findMatchingCondition(originalContent, quantity) === findMatchingCondition(reorderedContent, quantity));
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* Ascending threshold order for `enu()`, `fallback` last.
|
|
94
|
+
*
|
|
95
|
+
* The first matching condition wins at runtime, so the new order is applied
|
|
96
|
+
* only when it selects the same branch for every quantity; overlapping
|
|
97
|
+
* conditions whose priority depends on their position are left untouched.
|
|
98
|
+
*/
|
|
99
|
+
const orderEnumerationConditions = (keys) => {
|
|
100
|
+
const conditionKeys = keys.filter((key) => key !== FALLBACK_KEY);
|
|
101
|
+
const conditions = conditionKeys.map(parseEnumerationCondition);
|
|
102
|
+
if (conditions.some((condition) => condition === void 0)) return;
|
|
103
|
+
const sortedConditionKeys = conditions.sort((conditionA, conditionB) => conditionA.threshold - conditionB.threshold || conditionA.operatorRank - conditionB.operatorRank || compareStrings(conditionA.key, conditionB.key)).map((condition) => condition.key);
|
|
104
|
+
if (!resolvesIdentically(conditionKeys, sortedConditionKeys, getProbeQuantities(conditions))) return;
|
|
105
|
+
const fallbackKeys = keys.filter((key) => key === FALLBACK_KEY);
|
|
106
|
+
return [...sortedConditionKeys, ...fallbackKeys];
|
|
107
|
+
};
|
|
108
|
+
/** Keys `getSelect` falls back to, by name, before the last declared key. */
|
|
109
|
+
const SELECT_NAMED_FALLBACK_KEYS = ["fallback", "other"];
|
|
110
|
+
/**
|
|
111
|
+
* Alphabetical order for `select()`, `fallback` last.
|
|
112
|
+
*
|
|
113
|
+
* Without a `fallback` or `other` key, the last key is the implicit fallback,
|
|
114
|
+
* so it stays last and only the keys before it are sorted.
|
|
115
|
+
*/
|
|
116
|
+
const orderSelectCases = (keys) => {
|
|
117
|
+
const hasNamedFallback = keys.some((key) => SELECT_NAMED_FALLBACK_KEYS.includes(key));
|
|
118
|
+
const runtimeKeys = toRuntimeKeyOrder(keys);
|
|
119
|
+
const pinnedLastKey = hasNamedFallback ? keys.includes(FALLBACK_KEY) ? FALLBACK_KEY : void 0 : runtimeKeys[runtimeKeys.length - 1];
|
|
120
|
+
const sortedKeys = keys.filter((key) => key !== pinnedLastKey).sort(compareStrings);
|
|
121
|
+
const orderedKeys = pinnedLastKey === void 0 ? sortedKeys : [...sortedKeys, pinnedLastKey];
|
|
122
|
+
const reorderedRuntimeKeys = toRuntimeKeyOrder(orderedKeys);
|
|
123
|
+
if (!hasNamedFallback && reorderedRuntimeKeys[reorderedRuntimeKeys.length - 1] !== pinnedLastKey) return;
|
|
124
|
+
return orderedKeys;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export { PLURAL_CATEGORY_ORDER, createLocaleComparator, createLocaleOrdering, orderAlphabetically, orderEnumerationConditions, orderPluralCategories, orderSelectCases };
|
|
128
|
+
//# sourceMappingURL=keyOrderings.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keyOrderings.mjs","names":[],"sources":["../../src/keyOrderings.ts"],"sourcesContent":["import { findMatchingCondition } from '@intlayer/core/interpreter';\nimport type { EnumerationContentState } from '@intlayer/core/transpiler';\n\n/**\n * Returns the given keys in their canonical order, or `undefined` when the\n * keys must keep their current order.\n */\nexport type KeyOrdering = (keys: readonly string[]) => string[] | undefined;\n\nconst compareStrings = (stringA: string, stringB: string): number => {\n if (stringA === stringB) return 0;\n\n return stringA < stringB ? -1 : 1;\n};\n\n/** Plain alphabetical order, for the keys of a content object. */\nexport const orderAlphabetically: KeyOrdering = (keys) =>\n [...keys].sort(compareStrings);\n\n/**\n * Builds the comparator ordering locales: pinned locales first (in the given\n * order), then every other locale alphabetically.\n */\nexport const createLocaleComparator = (pinnedLocales: readonly string[]) => {\n const pinnedRank = new Map(\n pinnedLocales.map((locale, index) => [locale, index])\n );\n\n return (localeA: string, localeB: string): number => {\n const rankA = pinnedRank.get(localeA) ?? Number.POSITIVE_INFINITY;\n const rankB = pinnedRank.get(localeB) ?? Number.POSITIVE_INFINITY;\n\n if (rankA !== rankB) return rankA < rankB ? -1 : 1;\n\n return compareStrings(localeA, localeB);\n };\n};\n\n/** Locale order for `t()`: pinned locales, then alphabetical. */\nexport const createLocaleOrdering = (\n pinnedLocales: readonly string[]\n): KeyOrdering => {\n const compareLocales = createLocaleComparator(pinnedLocales);\n\n return (keys) => [...keys].sort(compareLocales);\n};\n\n/** CLDR plural categories, from the smallest quantity to the fallback. */\nexport const PLURAL_CATEGORY_ORDER = [\n 'zero',\n 'one',\n 'two',\n 'few',\n 'many',\n 'other',\n] as const;\n\n/**\n * CLDR order for `plural()`. Unknown keys leave the object untouched.\n */\nexport const orderPluralCategories: KeyOrdering = (keys) => {\n const categoryRank = new Map<string, number>(\n PLURAL_CATEGORY_ORDER.map((category, index) => [category, index])\n );\n\n if (!keys.every((key) => categoryRank.has(key))) return undefined;\n\n return [...keys].sort(\n (keyA, keyB) =>\n (categoryRank.get(keyA) as number) - (categoryRank.get(keyB) as number)\n );\n};\n\nconst FALLBACK_KEY = 'fallback';\n\nconst ENUMERATION_CONDITION_PATTERN = /^(<=|>=|<|>|=)?(-?\\d+(?:\\.\\d+)?)$/;\n\n/** Rank of each operator for one threshold: `<1`, `<=1`, `1`, `>=1`, `>1`. */\nconst OPERATOR_RANK: Record<string, number> = {\n '<': 0,\n '<=': 1,\n '': 2,\n '=': 2,\n '>=': 3,\n '>': 4,\n};\n\ntype EnumerationCondition = {\n key: string;\n threshold: number;\n operatorRank: number;\n};\n\nconst parseEnumerationCondition = (\n key: string\n): EnumerationCondition | undefined => {\n const match = ENUMERATION_CONDITION_PATTERN.exec(key);\n\n if (!match) return undefined;\n\n return {\n key,\n threshold: Number(match[2]),\n operatorRank: OPERATOR_RANK[match[1] ?? ''],\n };\n};\n\n/**\n * Keys in the order the runtime iterates them: JavaScript lists integer-like\n * keys first, ascending, whatever their position in the source.\n */\nconst toRuntimeKeyOrder = (keys: readonly string[]): string[] =>\n Object.keys(Object.fromEntries(keys.map((key) => [key, true])));\n\n/**\n * Quantities covering every interval the conditions can distinguish: each\n * threshold, the midpoint between two thresholds, and one value beyond each\n * end.\n */\nconst getProbeQuantities = (\n conditions: readonly EnumerationCondition[]\n): number[] => {\n const thresholds = [\n ...new Set(conditions.map((condition) => condition.threshold)),\n ].sort((thresholdA, thresholdB) => thresholdA - thresholdB);\n\n if (thresholds.length === 0) return [];\n\n const midpoints = thresholds\n .slice(1)\n .map((threshold, index) => (thresholds[index] + threshold) / 2);\n\n return [\n thresholds[0] - 1,\n ...thresholds,\n ...midpoints,\n thresholds[thresholds.length - 1] + 1,\n ];\n};\n\nconst resolvesIdentically = (\n originalKeys: readonly string[],\n reorderedKeys: readonly string[],\n quantities: readonly number[]\n): boolean => {\n const toContentState = (keys: readonly string[]) =>\n Object.fromEntries(\n toRuntimeKeyOrder(keys).map((key) => [key, key])\n ) as EnumerationContentState<string>;\n const originalContent = toContentState(originalKeys);\n const reorderedContent = toContentState(reorderedKeys);\n\n return quantities.every(\n (quantity) =>\n findMatchingCondition(originalContent, quantity) ===\n findMatchingCondition(reorderedContent, quantity)\n );\n};\n\n/**\n * Ascending threshold order for `enu()`, `fallback` last.\n *\n * The first matching condition wins at runtime, so the new order is applied\n * only when it selects the same branch for every quantity; overlapping\n * conditions whose priority depends on their position are left untouched.\n */\nexport const orderEnumerationConditions: KeyOrdering = (keys) => {\n const conditionKeys = keys.filter((key) => key !== FALLBACK_KEY);\n const conditions = conditionKeys.map(parseEnumerationCondition);\n\n if (conditions.some((condition) => condition === undefined)) {\n return undefined;\n }\n\n const sortedConditionKeys = (conditions as EnumerationCondition[])\n .sort(\n (conditionA, conditionB) =>\n conditionA.threshold - conditionB.threshold ||\n conditionA.operatorRank - conditionB.operatorRank ||\n compareStrings(conditionA.key, conditionB.key)\n )\n .map((condition) => condition.key);\n\n if (\n !resolvesIdentically(\n conditionKeys,\n sortedConditionKeys,\n getProbeQuantities(conditions as EnumerationCondition[])\n )\n ) {\n return undefined;\n }\n\n const fallbackKeys = keys.filter((key) => key === FALLBACK_KEY);\n\n return [...sortedConditionKeys, ...fallbackKeys];\n};\n\n/** Keys `getSelect` falls back to, by name, before the last declared key. */\nconst SELECT_NAMED_FALLBACK_KEYS = ['fallback', 'other'];\n\n/**\n * Alphabetical order for `select()`, `fallback` last.\n *\n * Without a `fallback` or `other` key, the last key is the implicit fallback,\n * so it stays last and only the keys before it are sorted.\n */\nexport const orderSelectCases: KeyOrdering = (keys) => {\n const hasNamedFallback = keys.some((key) =>\n SELECT_NAMED_FALLBACK_KEYS.includes(key)\n );\n const runtimeKeys = toRuntimeKeyOrder(keys);\n const pinnedLastKey = hasNamedFallback\n ? keys.includes(FALLBACK_KEY)\n ? FALLBACK_KEY\n : undefined\n : runtimeKeys[runtimeKeys.length - 1];\n const sortedKeys = keys\n .filter((key) => key !== pinnedLastKey)\n .sort(compareStrings);\n const orderedKeys =\n pinnedLastKey === undefined ? sortedKeys : [...sortedKeys, pinnedLastKey];\n const reorderedRuntimeKeys = toRuntimeKeyOrder(orderedKeys);\n\n // Integer-like keys always iterate first: bail out if that moves the\n // implicit fallback.\n if (\n !hasNamedFallback &&\n reorderedRuntimeKeys[reorderedRuntimeKeys.length - 1] !== pinnedLastKey\n ) {\n return undefined;\n }\n\n return orderedKeys;\n};\n"],"mappings":"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"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { orderAlphabetically } from "./keyOrderings.mjs";
|
|
2
|
+
import { getHelperCallContent, getSerializedNodeContent, getStaticKeyName, isAstNode, isObjectExpression, isPlainProperty, sortObjectProperties } from "./sortContentNodes.mjs";
|
|
3
|
+
|
|
4
|
+
// src/sortContentKeys.ts
|
|
5
|
+
/** `*.content.ts`, `*.content.json`, … — the Intlayer declaration files. */
|
|
6
|
+
const CONTENT_FILE_PATTERN = /\.content\.(?:[cm]?[jt]sx?|json5?|jsonc)$/;
|
|
7
|
+
/** Expression wrappers looked through: `x as T`, `x satisfies T`, `(x)`, `x!`. */
|
|
8
|
+
const EXPRESSION_WRAPPER_TYPES = /* @__PURE__ */ new Set([
|
|
9
|
+
"TSAsExpression",
|
|
10
|
+
"TSSatisfiesExpression",
|
|
11
|
+
"TSNonNullExpression",
|
|
12
|
+
"TSTypeAssertion",
|
|
13
|
+
"TypeCastExpression",
|
|
14
|
+
"ParenthesizedExpression"
|
|
15
|
+
]);
|
|
16
|
+
/** True when Prettier is formatting an Intlayer content declaration file. */
|
|
17
|
+
const isContentFile = (filePath) => typeof filePath === "string" && CONTENT_FILE_PATTERN.test(filePath);
|
|
18
|
+
const getPropertyValues = (objectNode) => objectNode.properties.filter(isPlainProperty).map((property) => property.value);
|
|
19
|
+
/**
|
|
20
|
+
* Sorts a content value alphabetically, recursing through nested objects,
|
|
21
|
+
* arrays and the arguments of every helper call. The maps of `t()` /
|
|
22
|
+
* `plural()` / `enu()` / `select()` keep their own ordering; the argument
|
|
23
|
+
* objects of other helpers keep theirs.
|
|
24
|
+
*/
|
|
25
|
+
const sortContentValue = (value) => {
|
|
26
|
+
if (!isAstNode(value)) return;
|
|
27
|
+
if (EXPRESSION_WRAPPER_TYPES.has(value.type)) {
|
|
28
|
+
sortContentValue(value.expression);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (value.type === "ArrayExpression") {
|
|
32
|
+
for (const element of value.elements) sortContentValue(element);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const helperContent = getHelperCallContent(value) ?? getSerializedNodeContent(value);
|
|
36
|
+
if (helperContent) {
|
|
37
|
+
for (const nestedValue of getPropertyValues(helperContent.content)) sortContentValue(nestedValue);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (value.type === "CallExpression") {
|
|
41
|
+
for (const argument of value.arguments) if (isObjectExpression(argument)) for (const nestedValue of getPropertyValues(argument)) sortContentValue(nestedValue);
|
|
42
|
+
else sortContentValue(argument);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (!isObjectExpression(value)) return;
|
|
46
|
+
if (!value.properties.some((property) => isPlainProperty(property) && getStaticKeyName(property) === "nodeType")) sortObjectProperties(value, orderAlphabetically);
|
|
47
|
+
for (const nestedValue of getPropertyValues(value)) sortContentValue(nestedValue);
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* The `content` of a dictionary declaration: an object holding both a `key`
|
|
51
|
+
* and a `content` property.
|
|
52
|
+
*/
|
|
53
|
+
const getDictionaryContent = (node) => {
|
|
54
|
+
if (!isObjectExpression(node)) return void 0;
|
|
55
|
+
const properties = node.properties.filter(isPlainProperty);
|
|
56
|
+
const findProperty = (name) => properties.find((property) => getStaticKeyName(property) === name);
|
|
57
|
+
if (!findProperty("key")) return void 0;
|
|
58
|
+
return findProperty("content")?.value;
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Sorts, in place, the keys of every dictionary `content` object of the AST,
|
|
62
|
+
* and of their nested objects, alphabetically.
|
|
63
|
+
*/
|
|
64
|
+
const sortContentKeysInAst = (rootNode) => {
|
|
65
|
+
const visitedNodes = /* @__PURE__ */ new WeakSet();
|
|
66
|
+
const pendingValues = [rootNode];
|
|
67
|
+
while (pendingValues.length > 0) {
|
|
68
|
+
const value = pendingValues.pop();
|
|
69
|
+
if (typeof value !== "object" || value === null) continue;
|
|
70
|
+
if (visitedNodes.has(value)) continue;
|
|
71
|
+
visitedNodes.add(value);
|
|
72
|
+
if (isAstNode(value)) {
|
|
73
|
+
const dictionaryContent = getDictionaryContent(value);
|
|
74
|
+
if (dictionaryContent) {
|
|
75
|
+
sortContentValue(dictionaryContent);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
for (const [field, child] of Object.entries(value)) if (field !== "loc" && field !== "range" && field !== "parent") pendingValues.push(child);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export { isContentFile, sortContentKeysInAst };
|
|
84
|
+
//# sourceMappingURL=sortContentKeys.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sortContentKeys.mjs","names":[],"sources":["../../src/sortContentKeys.ts"],"sourcesContent":["import { orderAlphabetically } from './keyOrderings';\nimport {\n type AstNode,\n getHelperCallContent,\n getSerializedNodeContent,\n getStaticKeyName,\n isAstNode,\n isObjectExpression,\n isPlainProperty,\n sortObjectProperties,\n} from './sortContentNodes';\n\n/** `*.content.ts`, `*.content.json`, … — the Intlayer declaration files. */\nconst CONTENT_FILE_PATTERN = /\\.content\\.(?:[cm]?[jt]sx?|json5?|jsonc)$/;\n\n/** Expression wrappers looked through: `x as T`, `x satisfies T`, `(x)`, `x!`. */\nconst EXPRESSION_WRAPPER_TYPES = new Set([\n 'TSAsExpression',\n 'TSSatisfiesExpression',\n 'TSNonNullExpression',\n 'TSTypeAssertion',\n 'TypeCastExpression',\n 'ParenthesizedExpression',\n]);\n\n/** True when Prettier is formatting an Intlayer content declaration file. */\nexport const isContentFile = (filePath: string | undefined): boolean =>\n typeof filePath === 'string' && CONTENT_FILE_PATTERN.test(filePath);\n\nconst getPropertyValues = (objectNode: AstNode): unknown[] =>\n (objectNode.properties as unknown[])\n .filter(isPlainProperty)\n .map((property) => property.value);\n\n/**\n * Sorts a content value alphabetically, recursing through nested objects,\n * arrays and the arguments of every helper call. The maps of `t()` /\n * `plural()` / `enu()` / `select()` keep their own ordering; the argument\n * objects of other helpers keep theirs.\n */\nconst sortContentValue = (value: unknown): void => {\n if (!isAstNode(value)) return;\n\n if (EXPRESSION_WRAPPER_TYPES.has(value.type)) {\n sortContentValue(value.expression);\n return;\n }\n\n if (value.type === 'ArrayExpression') {\n for (const element of value.elements as unknown[]) {\n sortContentValue(element);\n }\n return;\n }\n\n const helperContent =\n getHelperCallContent(value) ?? getSerializedNodeContent(value);\n\n if (helperContent) {\n for (const nestedValue of getPropertyValues(helperContent.content)) {\n sortContentValue(nestedValue);\n }\n return;\n }\n\n // Any other helper (`cond`, `gender`, `md`, `insert`, `nest`, `file`, …):\n // its argument object may carry a meaningful key order (the last `cond` /\n // `gender` key is the fallback), so only the values inside are sorted.\n if (value.type === 'CallExpression') {\n for (const argument of value.arguments as unknown[]) {\n if (isObjectExpression(argument)) {\n for (const nestedValue of getPropertyValues(argument)) {\n sortContentValue(nestedValue);\n }\n } else {\n sortContentValue(argument);\n }\n }\n return;\n }\n\n if (!isObjectExpression(value)) return;\n\n // An unknown serialized node (`{ nodeType: 'condition', … }`) is not a plain\n // content object: its fields are not reordered.\n const isSerializedNode = (value.properties as unknown[]).some(\n (property) =>\n isPlainProperty(property) && getStaticKeyName(property) === 'nodeType'\n );\n\n if (!isSerializedNode) sortObjectProperties(value, orderAlphabetically);\n\n for (const nestedValue of getPropertyValues(value)) {\n sortContentValue(nestedValue);\n }\n};\n\n/**\n * The `content` of a dictionary declaration: an object holding both a `key`\n * and a `content` property.\n */\nconst getDictionaryContent = (node: AstNode): unknown => {\n if (!isObjectExpression(node)) return undefined;\n\n const properties = (node.properties as unknown[]).filter(isPlainProperty);\n const findProperty = (name: string) =>\n properties.find((property) => getStaticKeyName(property) === name);\n\n if (!findProperty('key')) return undefined;\n\n return findProperty('content')?.value;\n};\n\n/**\n * Sorts, in place, the keys of every dictionary `content` object of the AST,\n * and of their nested objects, alphabetically.\n */\nexport const sortContentKeysInAst = (rootNode: unknown): void => {\n const visitedNodes = new WeakSet<object>();\n const pendingValues: unknown[] = [rootNode];\n\n while (pendingValues.length > 0) {\n const value = pendingValues.pop();\n\n if (typeof value !== 'object' || value === null) continue;\n if (visitedNodes.has(value)) continue;\n\n visitedNodes.add(value);\n\n if (isAstNode(value)) {\n const dictionaryContent = getDictionaryContent(value);\n\n if (dictionaryContent) {\n sortContentValue(dictionaryContent);\n continue;\n }\n }\n\n for (const [field, child] of Object.entries(value)) {\n if (field !== 'loc' && field !== 'range' && field !== 'parent') {\n pendingValues.push(child);\n }\n }\n }\n};\n"],"mappings":";;;;;AAaA,MAAM,uBAAuB;;AAG7B,MAAM,2CAA2B,IAAI,IAAI;CACvC;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;;AAGD,MAAa,iBAAiB,aAC5B,OAAO,aAAa,YAAY,qBAAqB,KAAK,QAAQ;AAEpE,MAAM,qBAAqB,eACxB,WAAW,WACT,OAAO,eAAe,CAAC,CACvB,KAAK,aAAa,SAAS,KAAK;;;;;;;AAQrC,MAAM,oBAAoB,UAAyB;CACjD,IAAI,CAAC,UAAU,KAAK,GAAG;CAEvB,IAAI,yBAAyB,IAAI,MAAM,IAAI,GAAG;EAC5C,iBAAiB,MAAM,UAAU;EACjC;CACF;CAEA,IAAI,MAAM,SAAS,mBAAmB;EACpC,KAAK,MAAM,WAAW,MAAM,UAC1B,iBAAiB,OAAO;EAE1B;CACF;CAEA,MAAM,gBACJ,qBAAqB,KAAK,KAAK,yBAAyB,KAAK;CAE/D,IAAI,eAAe;EACjB,KAAK,MAAM,eAAe,kBAAkB,cAAc,OAAO,GAC/D,iBAAiB,WAAW;EAE9B;CACF;CAKA,IAAI,MAAM,SAAS,kBAAkB;EACnC,KAAK,MAAM,YAAY,MAAM,WAC3B,IAAI,mBAAmB,QAAQ,GAC7B,KAAK,MAAM,eAAe,kBAAkB,QAAQ,GAClD,iBAAiB,WAAW;OAG9B,iBAAiB,QAAQ;EAG7B;CACF;CAEA,IAAI,CAAC,mBAAmB,KAAK,GAAG;CAShC,IAAI,CALsB,MAAM,WAAyB,MACtD,aACC,gBAAgB,QAAQ,KAAK,iBAAiB,QAAQ,MAAM,UAG5C,GAAG,qBAAqB,OAAO,mBAAmB;CAEtE,KAAK,MAAM,eAAe,kBAAkB,KAAK,GAC/C,iBAAiB,WAAW;AAEhC;;;;;AAMA,MAAM,wBAAwB,SAA2B;CACvD,IAAI,CAAC,mBAAmB,IAAI,GAAG,OAAO;CAEtC,MAAM,aAAc,KAAK,WAAyB,OAAO,eAAe;CACxE,MAAM,gBAAgB,SACpB,WAAW,MAAM,aAAa,iBAAiB,QAAQ,MAAM,IAAI;CAEnE,IAAI,CAAC,aAAa,KAAK,GAAG,OAAO;CAEjC,OAAO,aAAa,SAAS,CAAC,EAAE;AAClC;;;;;AAMA,MAAa,wBAAwB,aAA4B;CAC/D,MAAM,+BAAe,IAAI,QAAgB;CACzC,MAAM,gBAA2B,CAAC,QAAQ;CAE1C,OAAO,cAAc,SAAS,GAAG;EAC/B,MAAM,QAAQ,cAAc,IAAI;EAEhC,IAAI,OAAO,UAAU,YAAY,UAAU,MAAM;EACjD,IAAI,aAAa,IAAI,KAAK,GAAG;EAE7B,aAAa,IAAI,KAAK;EAEtB,IAAI,UAAU,KAAK,GAAG;GACpB,MAAM,oBAAoB,qBAAqB,KAAK;GAEpD,IAAI,mBAAmB;IACrB,iBAAiB,iBAAiB;IAClC;GACF;EACF;EAEA,KAAK,MAAM,CAAC,OAAO,UAAU,OAAO,QAAQ,KAAK,GAC/C,IAAI,UAAU,SAAS,UAAU,WAAW,UAAU,UACpD,cAAc,KAAK,KAAK;CAG9B;AACF"}
|