Skip to content

Commit bcaacce

Browse files
committed
feat: configure CLI, write quick extractor
1 parent d1c38f7 commit bcaacce

File tree

6 files changed

+703
-5
lines changed

6 files changed

+703
-5
lines changed

email/.config/extractor.ts

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
/*
2+
* Copyright (C) 2024 Tolgee s.r.o. and contributors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import type { ExtractedKey, ExtractionResult, Warning } from "@tolgee/cli/extractor";
18+
import type { CallExpression, Expression, JSXAttribute, JSXAttrValue, JSXOpeningElement, Node, Span } from "@swc/types";
19+
import { walk } from "estree-walker";
20+
import { parse } from "@swc/core";
21+
22+
type AnyExpr = Expression | JSXAttrValue;
23+
24+
const isCallExpression = (node: Node): node is CallExpression =>
25+
node.type === 'CallExpression';
26+
const isJsxOpeningElement = (node: Node): node is JSXOpeningElement =>
27+
node.type === 'JSXOpeningElement';
28+
29+
function spanToLine(code: string, span: Span) {
30+
return code.slice(0, span.start).split('\n').length;
31+
}
32+
33+
function toString(node: Expression | JSXAttrValue): string | null {
34+
switch (node.type) {
35+
case 'StringLiteral':
36+
return node.value;
37+
case 'JSXText':
38+
return node.value.trim();
39+
case 'JSXExpressionContainer':
40+
return toString(node.expression);
41+
}
42+
43+
return null;
44+
}
45+
46+
function processTranslateCall(
47+
keyNameExpr: AnyExpr,
48+
defaultValueExpr: AnyExpr | null,
49+
line: number,
50+
code: string
51+
): {
52+
key: ExtractedKey | null;
53+
warning: Warning | null;
54+
} {
55+
const keyName = toString(keyNameExpr);
56+
const defaultValue = defaultValueExpr ? toString(defaultValueExpr) : null;
57+
58+
if (!keyName) {
59+
return {
60+
key: null,
61+
warning: {
62+
warning: `Failed to extract the key. It may be dynamic or not yet recognized during parse. AST node was a \`${keyNameExpr.type}\``,
63+
line: 'span' in keyNameExpr ? spanToLine(code, keyNameExpr.span) : line,
64+
},
65+
};
66+
}
67+
68+
return {
69+
key: {
70+
keyName,
71+
defaultValue,
72+
line,
73+
},
74+
warning:
75+
!defaultValue && defaultValueExpr
76+
? {
77+
warning: `Failed to extract the default value. It may be dynamic or not yet recognized during parse. AST node was a \`${keyNameExpr.type}\``,
78+
line:
79+
'span' in defaultValueExpr
80+
? spanToLine(code, defaultValueExpr.span)
81+
: line,
82+
}
83+
: null,
84+
};
85+
}
86+
87+
export default async function extractor(
88+
code: string
89+
): Promise<ExtractionResult> {
90+
const module = await parse(code, {
91+
syntax: 'typescript',
92+
tsx: true,
93+
});
94+
95+
const keys: ExtractedKey[] = [];
96+
const warnings: Warning[] = [];
97+
walk(module as any, {
98+
enter(node: Node) {
99+
if (
100+
isCallExpression(node) &&
101+
node.callee.type === 'Identifier' &&
102+
node.callee.value === 't'
103+
) {
104+
const line = spanToLine(code, node.span);
105+
const res = processTranslateCall(
106+
node.arguments[0].expression,
107+
node.arguments[1].expression,
108+
line,
109+
code
110+
);
111+
112+
if (res.key) keys.push(res.key)
113+
if (res.warning) warnings.push(res.warning)
114+
}
115+
116+
if (
117+
isJsxOpeningElement(node) &&
118+
node.name.type === 'Identifier' &&
119+
node.name.value === 'LocalizedText'
120+
) {
121+
const line = spanToLine(code, node.span);
122+
const keyName = node.attributes.find(
123+
(e): e is JSXAttribute =>
124+
e.type === 'JSXAttribute' &&
125+
e.name.type === 'Identifier' &&
126+
e.name.value === 'keyName'
127+
);
128+
const defaultValue = node.attributes.find(
129+
(e): e is JSXAttribute =>
130+
e.type === 'JSXAttribute' &&
131+
e.name.type === 'Identifier' &&
132+
e.name.value === 'defaultValue'
133+
);
134+
135+
if (!keyName) {
136+
warnings.push({
137+
warning:
138+
'Found <LocalizedText/> but could not find its `keyName` attribute. This likely means the extraction will be incomplete.',
139+
line: spanToLine(code, node.span),
140+
});
141+
142+
// We can't proceed, abort here
143+
return;
144+
}
145+
146+
if (!defaultValue) {
147+
warnings.push({
148+
warning:
149+
'Found <LocalizedText/> but could not find its `defaultValue` attribute.',
150+
line: spanToLine(code, node.span),
151+
});
152+
}
153+
154+
const res = processTranslateCall(
155+
keyName.value,
156+
defaultValue.value,
157+
line,
158+
code
159+
);
160+
161+
if (res.key) keys.push(res.key)
162+
if (res.warning) warnings.push(res.warning)
163+
}
164+
},
165+
});
166+
167+
return { keys, warnings };
168+
}

email/.config/tolgeerc.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"$schema": "https://docs.tolgee.io/cli-schema.json",
3+
"projectId": 1,
4+
"format": "PROPERTIES_JAVA",
5+
"patterns": ["./emails/**/*.ts?(x)", "./components/**/*.ts?(x)"],
6+
"extractor": "./.config/extractor.ts",
7+
"pull": {
8+
"path": "./src/i18n",
9+
"tags": ["email"]
10+
},
11+
"push": {
12+
"tagNewKeys": ["email"]
13+
}
14+
}

email/components/LocalizedText.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
import t from './translate';
17+
import _t from './translate.js';
1818

1919
type Props = {
2020
keyName: string;
@@ -23,5 +23,5 @@ type Props = {
2323
};
2424

2525
export default function LocalizedText(props: Props) {
26-
return t(props.keyName, props.defaultValue, props.demoParams);
26+
return _t(props.keyName, props.defaultValue, props.demoParams);
2727
}

0 commit comments

Comments
 (0)