-
Notifications
You must be signed in to change notification settings - Fork 149
/
Copy pathupdate-crowdin-glossary.ts
200 lines (179 loc) · 6.25 KB
/
update-crowdin-glossary.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
import assert from 'node:assert';
import fs from 'node:fs/promises';
import path from 'node:path';
import { Glossaries, UploadStorage } from '@crowdin/crowdin-api-client';
import logger from '@docusaurus/logger';
import {
ParsedDocumentationResult,
ClassDocumentationContainer,
ModuleDocumentationContainer,
StructureDocumentationContainer,
ElementDocumentationContainer,
} from '@electron/docs-parser';
import * as dotenv from 'dotenv';
import latestVersion from 'latest-version';
import { toString } from 'mdast-util-to-string';
import remarkParse from 'remark-parse';
import { unified } from 'unified';
import { convertToCSV } from './utils/csv';
import { Parent, Text } from 'mdast';
dotenv.config();
const GLOSSARY_ID = 66562;
const glossary = new Map<string, string>();
/**
* Narrows whether an API from electron-api.json is of type "Class"
*/
function isClass(
api:
| ModuleDocumentationContainer
| ClassDocumentationContainer
| StructureDocumentationContainer
| ElementDocumentationContainer,
): api is ClassDocumentationContainer {
return api.type === 'Class';
}
/**
* Collects all JavaScript built-ins in the current environment.
*/
function collectGlobals() {
const globals = Object.getOwnPropertyNames(globalThis);
for (const term of globals) {
if (Object.keys(glossary.entries).includes(term)) return;
glossary.set(
term,
'This is a JavaScript built-in and should usually not be translated.',
);
}
}
/**
* Collects all Electron APIs and their respective methods and properties.
*/
async function collectElectronAPI() {
const version = await latestVersion('electron');
const url = `https://github.com/electron/electron/releases/download/v${version}/electron-api.json`;
const apis: ParsedDocumentationResult = await (await fetch(url)).json();
// Electron API names
for (const api of apis) {
glossary.set(
api.name,
`This is an Electron ${api.type} and should usually not be translated.`,
);
}
// Electron class instance methods, properties, and events
for (const api of apis) {
if (isClass(api)) {
const methods = api.instanceMethods || [];
for (const method of methods) {
const term = `${api.instanceName}.${method.name}`;
if (Object.keys(glossary.entries).includes(term)) continue;
glossary.set(
term,
'This is an Electron instance method and should usually not be translated.',
);
}
const props = api.instanceProperties || [];
for (const prop of props) {
const term = `${api.instanceName}.${prop.name}`;
if (Object.keys(glossary.entries).includes(term)) continue;
glossary.set(
term,
'This is an Electron instance property and should usually not be translated.',
);
}
const events = api.instanceEvents || [];
for (const event of events) {
const term = event.name;
// only include multi-word event names because some single-word events
// are just common English words like `close` or `quit`.
if (
Object.keys(glossary.entries).includes(term) ||
!term.includes('-')
) {
continue;
}
glossary.set(
term,
'This is an Electron instance event and should usually not be translated.',
);
}
}
}
}
/**
* Collects all entries in the Electron `glossary.md` doc.
* For the definition, uses the first paragraph from each doc entry for brevity.
*/
async function collectElectronGlossary() {
// Generate a Markdown AST from remark
const source = path.join(__dirname, '..', 'docs', 'latest', 'glossary.md');
const md = await fs.readFile(source, 'utf8');
const syntaxTree = unified().use(remarkParse).parse(md);
// visit each top-level child of the syntax tree.
// in the current doc, each glossary entry is an H3 (###)
(syntaxTree as Parent).children.forEach((val, index, arr) => {
if (val.type === 'heading' && val.depth === 3) {
// value of the h3 text
const headingText = (val.children[0] as Text).value;
// the next element in the MDAST should be the first paragraph
const firstParagraph = toString((arr[index + 1] as Parent).children)
.replace(/"/g, '""') // escape double quotes for CSV " -> ""
.replace(/\n/g, ' '); // replace newlines in text with spaces
glossary.set(headingText, firstParagraph);
}
});
}
async function main() {
const args = process.argv.slice(2);
logger.info('Collecting all JavaScript built-ins');
collectGlobals();
logger.info('Collecting Electron API data');
await collectElectronAPI();
logger.info("Collecting data from Electron's glossary.md doc");
await collectElectronGlossary();
logger.info(
`There are ${logger.green(glossary.size)} glossary entries to upload`,
);
assert(
glossary.size > 500,
'There should be at least 500 values in the Electron glossary',
);
logger.info('Converting glossary to CSV format');
const csv = convertToCSV(Array.from(glossary));
if (!args.includes('--dry-run')) {
// Updating the glossary is a two-step process with the v2 Crowdin API
if (!process.env.CROWDIN_PERSONAL_TOKEN) {
logger.error(
`Missing ${logger.red('CROWDIN_PERSONAL_TOKEN')} environment variable`,
);
}
const glossaries = new Glossaries({
token: process.env.CROWDIN_PERSONAL_TOKEN,
});
const uploadStorage = new UploadStorage({
token: process.env.CROWDIN_PERSONAL_TOKEN,
});
// Step 1: Upload the CSV to the Crowdin server storage
logger.info('Uploading glossary.csv to Crowdin server storage');
const response = await uploadStorage.addStorage(
`glossary-${Date.now()}.csv`,
csv,
);
const { id } = response.data;
// Step 2: Assign the CSV in storage to the Electron glossary
logger.info('Importing glossary.csv into Electron project glossary');
await glossaries.importGlossaryFile(GLOSSARY_ID, {
storageId: id,
scheme: {
term_en: 0,
description_en: 1,
},
});
logger.info(
`✨ Done! See https://crowdin.com/resources/glossaries/${GLOSSARY_ID} for output.`,
);
} else {
logger.info('Dry run triggered, logging CSV output');
console.log(csv);
}
}
main().catch((err) => logger.error(err));