Reimplement scripts with TS

This commit is contained in:
ErickSkrauch
2020-01-17 12:44:22 +03:00
committed by SleepWalker
parent 649e7b8e23
commit 10e8b77acf
11 changed files with 343 additions and 188 deletions

View File

@@ -35,6 +35,9 @@
"whatwg-fetch": "^3.0.0"
},
"devDependencies": {
"@types/debounce": "^1.2.0",
"@types/intl": "^1.2.0",
"@types/raf": "^3.4.0",
"@types/react-helmet": "^5.0.15",
"@types/webpack-env": "^1.15.0",
"enzyme": "^3.8.0",

View File

@@ -3,7 +3,7 @@ import options from 'app/services/api/options';
let readyPromise: Promise<void>;
let lang = 'en';
let sitekey;
let sitekey: string;
export type CaptchaID = string;

View File

@@ -17,16 +17,17 @@ export function hasStorage() {
return _hasStorage;
}
class DummyStorage {
getItem(key) {
// TODO: work on
class DummyStorage implements Storage {
getItem(key: string): string | null {
return this[key] || null;
}
setItem(key, value) {
setItem(key: string, value: string): void {
this[key] = value;
}
removeItem(key) {
removeItem(key: string): void {
Reflect.deleteProperty(this, key);
}
}

View File

@@ -12,19 +12,27 @@ const LANG_DIR = `${__dirname}/../app/i18n`;
const DEFAULT_LOCALE = 'en';
const SUPPORTED_LANGS = [DEFAULT_LOCALE, ...Object.keys(localesMap)];
interface MessageDescriptor {
id: string | number;
defaultMessage: string;
}
/**
* Aggregates the default messages that were extracted from the app's
* React components via the React Intl Babel plugin. An error will be thrown if
* there are messages in different components that use the same `id`. The result
* is a flat collection of `id: message` pairs for the app's default locale.
*/
let idToFileMap: { [key: string]: string[] } = {};
let duplicateIds: string[] = [];
let idToFileMap: Record<string, Array<string>> = {};
let duplicateIds: Array<string | number> = [];
const collectedMessages = globSync(MESSAGES_PATTERN)
.map(filename => [filename, JSON.parse(fs.readFileSync(filename, 'utf8'))])
.reduce((collection, [file, descriptors]) => {
.map<[string, Array<MessageDescriptor>]>(filename => [
filename,
JSON.parse(fs.readFileSync(filename, 'utf8')),
])
.reduce<Record<string, string>>((collection, [file, descriptors]) => {
descriptors.forEach(({ id, defaultMessage }) => {
if (collection.hasOwnProperty(id)) {
if (collection[id]) {
duplicateIds.push(id);
}
@@ -52,13 +60,9 @@ idToFileMap = {};
* Making a diff with the previous DEFAULT_LOCALE version
*/
const defaultMessagesPath = `${LANG_DIR}/${DEFAULT_LOCALE}.json`;
let keysToUpdate: string[] = [];
let keysToAdd: string[] = [];
let keysToRemove: string[] = [];
const keysToRename: Array<[string, string]> = [];
const isNotMarked = (value: string) => value.slice(0, 2) !== '--';
const prevMessages: { [key: string]: string } = readJSON(defaultMessagesPath);
const prevMessages = readJSON<Record<string, string>>(defaultMessagesPath);
const prevMessagesMap = Object.entries(prevMessages).reduce(
(acc, [key, value]) => {
if (acc[value]) {
@@ -69,21 +73,24 @@ const prevMessagesMap = Object.entries(prevMessages).reduce(
return acc;
},
{} as { [key: string]: string[] },
{} as Record<string, Array<string>>,
);
keysToAdd = Object.keys(collectedMessages).filter(key => !prevMessages[key]);
keysToRemove = Object.keys(prevMessages)
const keysToAdd = Object.keys(collectedMessages).filter(
key => !prevMessages[key],
);
const keysToRemove: Array<string> = Object.keys(prevMessages)
.filter(key => !collectedMessages[key])
.filter(isNotMarked);
keysToUpdate = Object.entries(prevMessages).reduce(
const keysToUpdate: Array<string> = Object.entries(prevMessages).reduce(
(acc, [key, message]) =>
acc.concat(
collectedMessages[key] && collectedMessages[key] !== message ? key : [],
),
[] as string[],
[] as Array<string>,
);
const keysToRename: Array<[string, string]> = [];
// detect keys to rename, mutating keysToAdd and keysToRemove
[...keysToAdd].forEach(toKey => {
const keys = prevMessagesMap[collectedMessages[toKey]] || [];
@@ -91,7 +98,6 @@ keysToUpdate = Object.entries(prevMessages).reduce(
if (fromKey) {
keysToRename.push([fromKey, toKey]);
keysToRemove.splice(keysToRemove.indexOf(fromKey), 1);
keysToAdd.splice(keysToAdd.indexOf(toKey), 1);
}
@@ -178,7 +184,7 @@ function buildLocales() {
SUPPORTED_LANGS.map(lang => {
const destPath = `${LANG_DIR}/${lang}.json`;
const newMessages = readJSON(destPath);
const newMessages = readJSON<Record<string, string>>(destPath);
keysToRename.forEach(([fromKey, toKey]) => {
newMessages[toKey] = newMessages[fromKey];
@@ -195,18 +201,23 @@ function buildLocales() {
newMessages[key] = collectedMessages[key];
});
const sortedKeys = Object.keys(newMessages).sort((key1, key2) => {
key1 = key1.replace(/^-+/, '');
key2 = key2.replace(/^-+/, '');
const sortedKeys: Array<string> = Object.keys(newMessages).sort(
(key1, key2) => {
key1 = key1.replace(/^-+/, '');
key2 = key2.replace(/^-+/, '');
return key1 < key2 || !isNotMarked(key1) ? -1 : 1;
});
return key1 < key2 || !isNotMarked(key1) ? -1 : 1;
},
);
const sortedNewMessages = sortedKeys.reduce((acc, key) => {
acc[key] = newMessages[key];
const sortedNewMessages = sortedKeys.reduce<typeof newMessages>(
(acc, key) => {
acc[key] = newMessages[key];
return acc;
}, {});
return acc;
},
{},
);
fs.writeFileSync(
destPath,
@@ -215,15 +226,15 @@ function buildLocales() {
});
}
function readJSON(destPath) {
function readJSON<T extends {}>(destPath: string): T {
try {
return JSON.parse(fs.readFileSync(destPath, 'utf8'));
} catch (err) {
console.log(
chalk.yellow(`Can not read ${destPath}. The new file will be created.`),
chalk.yellow(`Can't read ${destPath}. The new file will be created.`),
`(${err.message})`,
);
}
return {};
return {} as T;
}

View File

@@ -1,14 +1,16 @@
/* eslint-env node */
/* eslint-disable no-console */
/* eslint-disable */
import fs from 'fs';
import path from 'path';
import CrowdinApi from 'crowdin-api';
import CrowdinApi, { LanguageStatusNode, LanguageStatusResponse, ProjectInfoResponse } from 'crowdin-api';
import MultiProgress from 'multi-progress';
import ch from 'chalk';
import iso639 from 'iso-639-1';
import prompt from 'prompt';
import { ValuesType } from 'utility-types';
import config from '../../config';
if (!config.crowdinApiKey) {
@@ -24,18 +26,21 @@ const LANG_DIR = path.resolve(`${__dirname}/../app/i18n`);
const INDEX_FILE_NAME = 'index.js';
const MIN_RELEASE_PROGRESS = 80; // Minimal ready percent before translation can be published
const crowdin = new CrowdinApi({ apiKey: PROJECT_KEY });
const crowdin = new CrowdinApi({
apiKey: PROJECT_KEY,
projectName: PROJECT_ID,
});
const progressBar = new MultiProgress();
/**
* Locales that has been verified by core team members
*/
const RELEASED_LOCALES = ['be', 'fr', 'id', 'pt', 'ru', 'uk', 'vi', 'zh'];
const RELEASED_LOCALES: Array<string> = ['be', 'fr', 'id', 'pt', 'ru', 'uk', 'vi', 'zh'];
/**
* Array of Crowdin locales to our internal locales representation
*/
const LOCALES_MAP = {
const LOCALES_MAP: Record<string, string> = {
'pt-BR': 'pt',
'zh-CN': 'zh',
};
@@ -43,7 +48,7 @@ const LOCALES_MAP = {
/**
* This array allows us to customise native languages names, because ISO-639-1 sometimes is strange
*/
const NATIVE_NAMES_MAP = {
const NATIVE_NAMES_MAP: Record<string, string> = {
be: 'Беларуская',
id: 'Bahasa Indonesia',
lt: 'Lietuvių',
@@ -57,7 +62,7 @@ const NATIVE_NAMES_MAP = {
/**
* This arrays allows us to override Crowdin English languages names
*/
const ENGLISH_NAMES_MAP = {
const ENGLISH_NAMES_MAP: Record<string, string> = {
pt: 'Portuguese, Brazilian',
sr: 'Serbian',
zh: 'Simplified Chinese',
@@ -65,9 +70,6 @@ const ENGLISH_NAMES_MAP = {
/**
* Converts Crowdin's language code to our internal value
*
* @param {string} code
* @returns {string}
*/
function toInternalLocale(code: string): string {
return LOCALES_MAP[code] || code;
@@ -76,108 +78,33 @@ function toInternalLocale(code: string): string {
/**
* Форматирует входящий объект с переводами в итоговую строку в том формате, в каком они
* хранятся в самом приложении
*
* @param {object} translates
* @returns {string}
*/
function serializeToFormattedJson(
translates: { [key: string]: any },
{ asModule = false }: { asModule?: boolean } = {},
): string {
function serializeToModule(translates: Record<string, any>): string {
const src = JSON.stringify(sortByKeys(translates), null, 2);
return asModule ? `module.exports = ${src};\n` : `${src}\n`;
return `module.exports = ${src};\n`;
}
/**
* http://stackoverflow.com/a/29622653/5184751
*
* @param {object} object
* @returns {object}
*/
function sortByKeys(object: { [key: string]: any }): { [key: string]: any } {
// http://stackoverflow.com/a/29622653/5184751
function sortByKeys<T extends Record<string, any>>(object: T): T {
return Object.keys(object)
.sort()
.reduce((result, key) => {
// @ts-ignore
result[key] = object[key];
return result;
}, {});
}, {} as T);
}
interface ProjectInfoFile {
node_type: 'file';
id: number;
name: string;
created: string;
last_updated: string;
last_accessed: string;
last_revision: string;
}
interface ProjectInfoDirectory {
node_type: 'directory';
id: number;
name: string;
files: Array<ProjectInfoFile | ProjectInfoDirectory>;
}
interface ProjectInfoResponse {
details: {
source_language: {
name: string;
code: string;
};
name: string;
identifier: string;
created: string;
description: string;
join_policy: string;
last_build: string | null;
last_activity: string;
participants_count: string; // it's number, but string in the response
logo_url: string | null;
total_strings_count: string; // it's number, but string in the response
total_words_count: string; // it's number, but string in the response
duplicate_strings_count: number;
duplicate_words_count: number;
invite_url: {
translator: string;
proofreader: string;
};
};
languages: Array<{
name: string; // English language name
code: string;
can_translate: 0 | 1;
can_approve: 0 | 1;
}>;
files: Array<ProjectInfoFile | ProjectInfoDirectory>;
}
async function pullLocales() {
const { languages }: ProjectInfoResponse = await crowdin.projectInfo(
PROJECT_ID,
);
async function pullLocales(): Promise<ProjectInfoResponse['languages']> {
const { languages } = await crowdin.projectInfo();
return languages;
}
interface LanguageStatusNode {
node_type: 'directory' | 'file';
id: number;
name: string;
phrases: number;
translated: number;
approved: number;
words: number;
words_translated: number;
words_approved: number;
files: Array<LanguageStatusNode>;
}
function findFile(
root: Array<LanguageStatusNode>,
root: LanguageStatusResponse['files'],
path: string,
): LanguageStatusNode | null {
const [nodeToSearch, ...rest] = path.split('/');
@@ -229,14 +156,18 @@ async function pull() {
);
let downloadingTotal = 0;
let downloadingReady = 0;
interface Result {
locale: ValuesType<typeof locales>,
progress: number,
translatesFilePath: string,
}
const results = await Promise.all(
locales.map(async locale => {
const {
files,
}: { files: Array<LanguageStatusNode> } = await crowdin.languageStatus(
PROJECT_ID,
locale.code,
);
// TODO: there is should be some way to reimplement this
// with reduce to avoid null values
locales.map(async (locale): Promise<Result | null> => {
const { files } = await crowdin.languageStatus(locale.code);
checkingProgressBar.tick();
const fileInfo = findFile(files, CROWDIN_FILE_PATH);
@@ -261,7 +192,6 @@ async function pull() {
});
const translatesFilePath = await crowdin.exportFile(
PROJECT_ID,
CROWDIN_FILE_PATH,
locale.code,
);
@@ -291,47 +221,40 @@ async function pull() {
},
};
await Promise.all(
results.map(
result =>
new Promise((resolve, reject) => {
if (result === null) {
resolve();
results
.filter((result): result is Result => result !== null)
.map(result => new Promise((resolve, reject) => {
const {
locale: { code, name },
progress,
translatesFilePath,
} = result;
const ourCode = toInternalLocale(code);
return;
}
indexFileEntries[ourCode] = {
code: ourCode,
name: NATIVE_NAMES_MAP[ourCode] || iso639.getNativeName(ourCode),
englishName: ENGLISH_NAMES_MAP[ourCode] || name,
progress: parseFloat(progress.toFixed(1)),
isReleased: RELEASED_LOCALES.includes(ourCode),
};
const {
locale: { code, name },
progress,
translatesFilePath,
} = result;
const ourCode = toInternalLocale(code);
indexFileEntries[ourCode] = {
code: ourCode,
name: NATIVE_NAMES_MAP[ourCode] || iso639.getNativeName(ourCode),
englishName: ENGLISH_NAMES_MAP[ourCode] || name,
progress: parseFloat(progress.toFixed(1)),
isReleased: RELEASED_LOCALES.includes(ourCode),
};
fs.copyFile(
translatesFilePath,
path.join(LANG_DIR, `${ourCode}.json`),
0,
err => {
err ? reject(err) : resolve();
},
);
}),
),
fs.copyFile(
translatesFilePath,
path.join(LANG_DIR, `${ourCode}.json`),
0,
err => {
err ? reject(err) : resolve();
},
);
})),
);
console.log('Writing an index file.');
fs.writeFileSync(
path.join(LANG_DIR, INDEX_FILE_NAME),
serializeToFormattedJson(indexFileEntries, { asModule: true }),
serializeToModule(indexFileEntries),
);
console.log(ch.green('The index file was successfully written'));
@@ -362,12 +285,10 @@ function push() {
console.log(`Publishing ${ch.bold(SOURCE_LANG)} translates file...`);
await crowdin.updateFile(
PROJECT_ID,
{
[CROWDIN_FILE_PATH]: path.join(LANG_DIR, `${SOURCE_LANG}.json`),
},
{
// eslint-disable-next-line @typescript-eslint/camelcase
update_option: disapprove
? 'update_as_unapproved'
: 'update_without_changes',

View File

@@ -11,12 +11,15 @@
"license": "ISC",
"dependencies": {
"@babel/node": "^7.8.3",
"@types/mkdirp": "^0.5.2",
"@types/progress": "^2.0.3",
"chalk": "^3.0.0",
"crowdin-api": "erickskrauch/crowdin-api#add_missed_methods_and_fix_files_uploading",
"crowdin-api": "^4.0.0",
"glob": "^7.1.6",
"iso-639-1": "^2.1.0",
"mkdirp": "^0.5.1",
"multi-progress": "^2.0.0",
"prompt": "^1.0.0"
"prompt": "^1.0.0",
"utility-types": "^3.10.0"
}
}