This repository has been archived by the owner on May 2, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 89
/
lost-in-translation.js
169 lines (153 loc) · 6.36 KB
/
lost-in-translation.js
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
const { parse } = require('path');
const { readdirSync } = require('fs');
const { spawnSync } = require('child_process');
const { GoogleSpreadsheet } = require('google-spreadsheet');
/**
* This script finds all the english translation keys across
* all commits (including branches and pull requests) for each
* locale.json in app/locales. Afterwards it looks at each locale
* separately to see if it's missing any one the english keys.
* Usage: node ops/lost-in-translation.js
* Important: You need to run it from the root directory.
*/
/**
* Calls git cli with arguments and returns stdout.
*/
function git(...args) {
const { stdout } = spawnSync('git', args, { encoding: 'utf-8'});
return stdout;
}
/**
* Returns the commit hashes for a file path across all local branches.
*/
function findCommitHashesForFile(filePath) {
const output = git('log', '--pretty=format:"%h"', '--all', '--', filePath);
const hashes = output.split('\n').map(line => line.replace(/"/g, ''));
return hashes;
}
/**
* Returns the JSON representation of the contents of a file at certain commit hash.
*/
function retrieveJSONForFileAtCommitHash(filePath, commitHash) {
const contentsAtCommitHash = git('show', `${commitHash}:${filePath}`);
try {
return JSON.parse(contentsAtCommitHash);
} catch (error) {
// Sometimes the file is not in a proper JSON format. Simply return nothing in that case.
return {};
}
}
/**
* Normalizes translation key like we do in app/server.ts.
*/
function normalizeTranslationKey(translationKey) {
return translationKey.replace(/[\s\n\t]+/g, ' ').trim();
}
/**
* Find all the locales (ie. en-IN, no, se) in the provided directory.
*/
function retrieveAllLocales(directoryPath) {
const filenames = readdirSync(directoryPath);
const locales = filenames.map(filename => parse(filename).name);
return locales;
}
/**
* Add rows to a Google Spreadsheet that are not already added.
*/
async function addUniqueRowsToGoogleSheet(sheet, rowsToAdd) {
const alreadyAddedRows = await sheet.getRows({ limit: 1000 });
const uniqueRowsToAdd = rowsToAdd.filter(rowToAdd =>
!alreadyAddedRows.find(alreadyAddedRow => alreadyAddedRow.key === rowToAdd.key));
await sheet.addRows(uniqueRowsToAdd);
return uniqueRowsToAdd;
}
/**
* Step 1: Find all the (english) translation keys across all branches and PRs.
*
* If a Dutch developer has made a feature in a branch, we expect that him/her added a key
* to one or many locale files (usually the english locale (en.json) or to their own locale
* file (nl.json), or both).
*
* But they probably haven't added translations to all the other locales, because they don't
* speak the other languages. And this is the problem, because now we need find people to help
* translate the added keys to all the other locales.
*
* So what we do here is simply to find *all* the english translation keys across the entire
* project. That means looking at all the english translations keys in all the locale files
* since the start of the project across all branches and PRs. We throw them into a set that
* we use in the next step.
*/
const allLocales = retrieveAllLocales('app/locales/');
const allEnglishTranslationKeys = new Set([]);
for (const locale of allLocales) {
const filePath = `app/locales/${locale}.json`;
for (const commitHash of findCommitHashesForFile(filePath)) {
const translation = retrieveJSONForFileAtCommitHash(filePath, commitHash);
for (const translationKey of Object.keys(translation)) {
allEnglishTranslationKeys.add(normalizeTranslationKey(translationKey));
}
}
}
/**
* Step 2: For each locale file check if there are any missing english translation keys
* across all branches and PRs and all commits/changes.
*
* If there's a missing english translation key, we know that we're most likely missing
* a translation for that locale. So what we need to do is to translate it, or get help
* to translate it.
*
* We throw it into a dictionary where the key is the locale and the value is a set of
* missing translations from english to that locale.
*/
const translationKeysByLocale = {};
for (const locale of allLocales) {
const filePath = `app/locales/${locale}.json`;
for (const commitHash of findCommitHashesForFile(filePath)) {
const translation = retrieveJSONForFileAtCommitHash(filePath, commitHash);
const translationKeys = Object.keys(translation).map(key => normalizeTranslationKey(key));
if (translationKeysByLocale[locale] === undefined) {
translationKeysByLocale[locale] = new Set([]);
}
translationKeysByLocale[locale] = new Set([...translationKeysByLocale[locale], ...translationKeys]);
}
}
/**
* Step 3: Add rows of missing translations to Google Spreadsheet.
*
* https://docs.google.com/spreadsheets/d/1ILFfc1DX4ujMnLnf9UqhwQGM9Ke3s1cAWciy8VqMHZw
*/
(async () => {
const doc = new GoogleSpreadsheet('1ILFfc1DX4ujMnLnf9UqhwQGM9Ke3s1cAWciy8VqMHZw');
await doc.useServiceAccountAuth(require('./coronastatus-translation-486cef09736e-credentials.json'));
await doc.loadInfo();
const allLocales = retrieveAllLocales('app/locales/');
for (let sheetIndex = 0; sheetIndex < doc.sheetCount; sheetIndex++) {
const sheet = doc.sheetsByIndex[sheetIndex];
for (const locale of allLocales) {
if (sheet.title !== locale) {
continue;
}
try {
// Create a sheet if it doesn't already exist.
await doc.addSheet({ title: locale, headerValues: ['key', 'translation'] });
await new Promise(resolve => setTimeout(resolve, 10*1000));
} catch (error) {
// We don't do anything if the sheet for this locale exists.
}
// Add the missing rows by looking at the key column.
const rows = [];
for (const englishTranslationKey of allEnglishTranslationKeys) {
if (!translationKeysByLocale[locale].has(englishTranslationKey)) {
const row = { 'key': englishTranslationKey, translation: '' };
rows.push(row);
}
}
// Print out how many rows we added.
const addedRows = await addUniqueRowsToGoogleSheet(sheet, rows);
console.log(`Added ${addedRows.length} of ${rows.length} missing translations to the ${locale} sheet.`);
// Avoid getting rate limited by Google's API (max writes per 100 seconds).
console.log('Waiting before processing the next sheet.');
await new Promise(resolve => setTimeout(resolve, 20*1000));
}
}
})();