electronjs.org-new/scripts/update-crowdin-glossary.ts

201 строка
6.2 KiB
TypeScript

import fs from 'fs-extra';
import path from 'path';
import assert from 'assert';
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 = fs.readFileSync(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));