mirror of
https://github.com/elyby/accounts-frontend.git
synced 2025-05-31 14:11:58 +05:30
Replace OneSkyApp with Crowdin API. Remove unused strings from password component
This commit is contained in:
334
scripts/i18n-crowdin.js
Normal file
334
scripts/i18n-crowdin.js
Normal file
@@ -0,0 +1,334 @@
|
||||
// @flow
|
||||
/* eslint-env node */
|
||||
/* eslint-disable no-console */
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import CrowdinApi from 'crowdin-api';
|
||||
import MultiProgress from 'multi-progress';
|
||||
import ch from 'chalk';
|
||||
import iso639 from 'iso-639-1';
|
||||
import prompt from 'prompt';
|
||||
|
||||
const CONFIG_PATH = path.resolve(`${__dirname}/../config/env.js`);
|
||||
if (!fs.existsSync(CONFIG_PATH)) {
|
||||
console.error('To use this scripts please create config/env.js file first');
|
||||
process.exit(126);
|
||||
}
|
||||
|
||||
const PROJECT_ID = 'elyby';
|
||||
const PROJECT_KEY = require('./../config/env.js').crowdinApiKey;
|
||||
const CROWDIN_FILE_PATH = 'accounts/site.json';
|
||||
const SOURCE_LANG = 'en';
|
||||
const LANG_DIR = path.resolve(`${__dirname}/../src/i18n`);
|
||||
const INDEX_FILE_NAME = 'index.json';
|
||||
const MIN_RELEASE_PROGRESS = 80; // Minimal ready percent before translation can be published
|
||||
|
||||
const crowdin = new CrowdinApi({ apiKey: PROJECT_KEY });
|
||||
const progressBar = new MultiProgress();
|
||||
|
||||
/**
|
||||
* Locales that has been verified by core team members
|
||||
*/
|
||||
const RELEASED_LOCALES = ['be', 'fr', 'id', 'pt', 'ru', 'uk', 'vi', 'zh'];
|
||||
|
||||
/**
|
||||
* Array of Crowdin locales to our internal locales representation
|
||||
*/
|
||||
const LOCALES_MAP = {
|
||||
'pt-BR': 'pt',
|
||||
'zh-CN': 'zh',
|
||||
};
|
||||
|
||||
/**
|
||||
* This array allows us to customise native languages names, because ISO-639-1 sometimes is strange
|
||||
*/
|
||||
const NATIVE_NAMES_MAP = {
|
||||
be: 'Беларуская',
|
||||
id: 'Bahasa Indonesia',
|
||||
lt: 'Lietuvių',
|
||||
pl: 'Polski',
|
||||
pt: 'Português do Brasil',
|
||||
sr: 'Српски',
|
||||
ro: 'Română',
|
||||
zh: '简体中文',
|
||||
};
|
||||
|
||||
/**
|
||||
* This arrays allows us to override Crowdin English languages names
|
||||
*/
|
||||
const ENGLISH_NAMES_MAP = {
|
||||
pt: 'Portuguese, Brazilian',
|
||||
sr: 'Serbian',
|
||||
zh: 'Simplified Chinese',
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts Crowdin's language code to our internal value
|
||||
*
|
||||
* @param {string} code
|
||||
* @return {string}
|
||||
*/
|
||||
function toInternalLocale(code: string): string {
|
||||
return LOCALES_MAP[code] || code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Форматирует входящий объект с переводами в итоговую строку в том формате, в каком они
|
||||
* хранятся в самом приложении
|
||||
*
|
||||
* @param {object} translates
|
||||
* @return {string}
|
||||
*/
|
||||
function serializeToFormattedJson(translates: Object): string {
|
||||
return JSON.stringify(sortByKeys(translates), null, 4) + '\n'; // eslint-disable-line prefer-template
|
||||
}
|
||||
|
||||
/**
|
||||
* http://stackoverflow.com/a/29622653/5184751
|
||||
*
|
||||
* @param {object} object
|
||||
* @return {object}
|
||||
*/
|
||||
function sortByKeys(object: Object): Object {
|
||||
return Object.keys(object).sort().reduce((result, key) => {
|
||||
result[key] = object[key];
|
||||
return result;
|
||||
}, {});
|
||||
}
|
||||
|
||||
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);
|
||||
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>, path: string): LanguageStatusNode | null {
|
||||
const [nodeToSearch, ...rest] = path.split('/');
|
||||
for (const node of root) {
|
||||
if (node.name !== nodeToSearch) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (rest.length === 0) {
|
||||
return node;
|
||||
}
|
||||
|
||||
return findFile(node.files, rest.join('/'));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
interface IndexFileEntry {
|
||||
code: string;
|
||||
name: string;
|
||||
englishName: string;
|
||||
progress: number;
|
||||
isReleased: bool;
|
||||
}
|
||||
|
||||
async function pull() {
|
||||
console.log('Pulling locales list...');
|
||||
const locales = await pullLocales();
|
||||
const checkingProgressBar = progressBar.newBar('| Pulling locales info :bar :percent | :current/:total', {
|
||||
total: locales.length,
|
||||
incomplete: '\u2591',
|
||||
complete: '\u2588',
|
||||
width: locales.length,
|
||||
});
|
||||
// Add prefix 'c' to current and total to prevent filling thees placeholders with real values
|
||||
const downloadingProgressBar = progressBar.newBar('| Downloading translates :bar :percent | :cCurrent/:cTotal', {
|
||||
total: 100,
|
||||
incomplete: '\u2591',
|
||||
complete: '\u2588',
|
||||
width: locales.length,
|
||||
});
|
||||
let downloadingTotal = 0;
|
||||
let downloadingReady = 0;
|
||||
const results = await Promise.all(locales.map(async (locale) => {
|
||||
const { files }: { files: Array<LanguageStatusNode> } = await crowdin.languageStatus(PROJECT_ID, locale.code);
|
||||
checkingProgressBar.tick();
|
||||
const fileInfo = findFile(files, CROWDIN_FILE_PATH);
|
||||
if (fileInfo === null) {
|
||||
throw new Error('Unable to find translation file. Please check the CROWDIN_FILE_PATH param.');
|
||||
}
|
||||
|
||||
const progress = fileInfo.words_approved / fileInfo.words * 100;
|
||||
if (!RELEASED_LOCALES.includes(toInternalLocale(locale.code)) && progress < MIN_RELEASE_PROGRESS) {
|
||||
return null;
|
||||
}
|
||||
|
||||
downloadingProgressBar.update(downloadingReady / ++downloadingTotal, {
|
||||
cCurrent: downloadingReady,
|
||||
cTotal: downloadingTotal,
|
||||
});
|
||||
|
||||
const translatesFilePath = await crowdin.exportFile(PROJECT_ID, CROWDIN_FILE_PATH, locale.code);
|
||||
|
||||
downloadingProgressBar.update(++downloadingReady / downloadingTotal, {
|
||||
cCurrent: downloadingReady,
|
||||
cTotal: downloadingTotal,
|
||||
});
|
||||
|
||||
return {
|
||||
locale,
|
||||
progress,
|
||||
translatesFilePath,
|
||||
};
|
||||
}));
|
||||
|
||||
console.log('Locales are downloaded. Writing them to file system.');
|
||||
|
||||
const indexFileEntries: { [string]: IndexFileEntry } = {
|
||||
en: {
|
||||
code: 'en',
|
||||
name: 'English',
|
||||
englishName: 'English',
|
||||
progress: 100,
|
||||
isReleased: true,
|
||||
},
|
||||
};
|
||||
// $FlowFixMe
|
||||
await Promise.all(results.map((result) => new Promise((resolve, reject) => {
|
||||
if (result === null) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
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();
|
||||
});
|
||||
})));
|
||||
|
||||
console.log('Writing an index file.');
|
||||
|
||||
fs.writeFileSync(path.join(LANG_DIR, INDEX_FILE_NAME), serializeToFormattedJson(indexFileEntries));
|
||||
|
||||
console.log(ch.green('The index file was successfully written'));
|
||||
}
|
||||
|
||||
function push() {
|
||||
return new Promise((resolve, reject) => {
|
||||
prompt.start();
|
||||
prompt.get({
|
||||
properties: {
|
||||
disapprove: {
|
||||
description: 'Disapprove changed lines? [Y/n]',
|
||||
pattern: /^y|n$/i,
|
||||
message: 'Please enter "y" or "n"',
|
||||
default: 'y',
|
||||
before: (value) => value.toLowerCase() === 'y',
|
||||
},
|
||||
},
|
||||
}, async (err, { disapprove }) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
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 camelcase
|
||||
update_option: disapprove ? 'update_as_unapproved' : 'update_without_changes',
|
||||
});
|
||||
|
||||
console.log(ch.green('Success'));
|
||||
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const action = process.argv[2];
|
||||
switch (action) {
|
||||
case 'pull':
|
||||
pull();
|
||||
break;
|
||||
case 'push':
|
||||
push();
|
||||
break;
|
||||
default:
|
||||
console.error(`Unknown action ${action}`);
|
||||
}
|
||||
} catch (exception) {
|
||||
console.error(exception);
|
||||
}
|
||||
Reference in New Issue
Block a user