Skip to content

Commit 2827331

Browse files
author
Piotr Siatka
committed
Add auto localization keys translation.
Add initialized translations.
1 parent a686dd0 commit 2827331

29 files changed

+8339
-8320
lines changed

config/supported.localization.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"langs": ["bg-bg", "ca-es", "da-dk", "de-de", "el-gr", "es-es", "et-ee", "fi-fi", "fr-fr", "it-it", "ja-jp", "lt-lt", "lv-lv", "nb-no", "nl-nl", "pl-pl", "pt-pt", "ro-ro", "ru-ru", "sk-sk", "sr-latn-rs", "sv-se", "tr-tr", "vi-vn", "zh-cn", "zh-tw"]
3+
}

scripts/execute-translation.js

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
/**
2+
* Script executes the translation of the localization keys for the provided languages (config/supported.localization.json) with the usage of the Azure Cognitive services.
3+
* In case the language localization file doesn't exist, it is going to be created.
4+
* Only languages supported by Azure Cognitive services are supported.
5+
*
6+
* English is used as a main language. The translation is executed for the keys which are the same as in English or are missing.
7+
*/
8+
9+
10+
const path = require('path');
11+
const fs = require('fs');
12+
const _ = require('lodash');
13+
// Cognitive services
14+
const request = require('request-promise');
15+
const uuidv4 = require('uuid/v4');
16+
17+
// Replace with process.env.subscriptionKey to get an access to Azure Cognitive services
18+
const subscriptionKey = process.env.subscriptionKey;
19+
const endpoint = "https://api.cognitive.microsofttranslator.com";
20+
21+
const locHelper = require('./export-localization');
22+
// Load configuration for supported languages
23+
const languagesConfiguration = require('../config/supported.localization.json');
24+
25+
/**
26+
* Function executes the translation using cognitive services.
27+
*/
28+
async function executeTranslation(lang, inputObj) {
29+
try {
30+
let options = {
31+
method: 'POST',
32+
baseUrl: endpoint,
33+
url: 'translate',
34+
qs: {
35+
'api-version': '3.0',
36+
'to': [`${lang}`]
37+
},
38+
headers: {
39+
'Ocp-Apim-Subscription-Key': subscriptionKey,
40+
'Content-type': 'application/json',
41+
'X-ClientTraceId': uuidv4().toString()
42+
},
43+
body: inputObj,
44+
json: true,
45+
};
46+
47+
const response = await request(options);
48+
if (!response || response.length < 0) {
49+
throw new Error("Somethig went wrong when obtaining translated text!");
50+
}
51+
52+
// Go through all the results
53+
const translations = response.map((item) => {
54+
// Every translation is in a separate 1-element array -> make it flat
55+
return item.translations[0];
56+
})
57+
58+
return translations;
59+
} catch (err) {
60+
console.error(`[Exception]: Cannot execute translation for lang - ${lang}. Err=${err}`)
61+
return null;
62+
}
63+
}
64+
65+
function compareTranslationKeys(srcObj, dstObj) {
66+
// Extract all the keys and set them in alpabetyical order
67+
let dstKeys = Object.keys(dstObj);
68+
69+
// Array<string>
70+
let toTranslate = [];
71+
dstKeys.forEach((locKey) => {
72+
if (typeof srcObj[locKey] !== "string") {
73+
// In case we have nested translation objects
74+
toTranslate = toTranslate.concat(compareTranslationKeys(srcObj[locKey], dstObj[locKey]));
75+
} else if (srcObj[locKey] === dstObj[locKey]) {
76+
// In case the english value is the same as localized one, add it to translate
77+
toTranslate.push(srcObj[locKey]);
78+
}
79+
});
80+
81+
return toTranslate;
82+
}
83+
84+
let currentTranslationIndex = 0;
85+
function injectTranslatedKeys(srcObj, dstObj, translatedValues) {
86+
const srcKeys = Object.keys(srcObj);
87+
srcKeys.forEach((locKey) => {
88+
if (typeof srcObj[locKey] !== "string") {
89+
dstObj[locKey] = injectTranslatedKeys(srcObj[locKey], dstObj[locKey], translatedValues);
90+
} else if (srcObj[locKey] === dstObj[locKey]) {
91+
const translatedKey = translatedValues[currentTranslationIndex++];
92+
dstObj[locKey] = translatedKey ? translatedKey : dstObj[locKey];
93+
}
94+
});
95+
96+
return dstObj;
97+
}
98+
99+
function prepareTranslationRequestMsg(wordsToTranslate) {
100+
// Execute translation for every 50 words in batch
101+
const result = [];
102+
const chunk = 50;
103+
for (let i = 0; i < wordsToTranslate.length; i += chunk) {
104+
const slicedWords = wordsToTranslate.slice(i, i + chunk);
105+
const slicedMessages = [];
106+
// do whatever
107+
slicedWords.forEach((word) => {
108+
slicedMessages.push({
109+
//'text': encodeURI(word)
110+
'text': word
111+
});
112+
});
113+
result.push(slicedMessages);
114+
}
115+
116+
return result;
117+
}
118+
119+
function extranctTranslatedKeys(translatedMsgs) {
120+
// Flatten the result to retrieve original structure of the keys array
121+
const translatedKeys = [];
122+
translatedMsgs.forEach((keys) => {
123+
keys.forEach((translationMsg) => {
124+
// There is often a replacement of '||' to ' | | ' during the translation
125+
// Replace ' | | ' to '||'
126+
const translationResult = translationMsg.text ? translationMsg.text.replace(" | | ", "||") : translationMsg.text;
127+
translatedKeys.push(translationResult);
128+
})
129+
});
130+
131+
return translatedKeys;
132+
}
133+
134+
async function executeLocalizationTranslation(srcObj, langObj, lang) {
135+
try {
136+
// Initialize result object with all english keys and localized keys
137+
const dstLoc = Object.assign({}, srcObj, langObj);
138+
139+
// Prepare keys to translate
140+
const keysToTranslate = compareTranslationKeys(srcObj, dstLoc);
141+
142+
if (keysToTranslate && keysToTranslate.length <= 0) {
143+
console.log(`There are no keys to translate`);
144+
dstLoc;
145+
}
146+
console.log(`There are ${keysToTranslate.length} keys to translate.`)
147+
148+
// Split the array to separate calls in case max limit of carachters (5000) is reached and execute translation
149+
const requestMessges = prepareTranslationRequestMsg(keysToTranslate);
150+
const promises = [];
151+
requestMessges.forEach((msgBody) => {
152+
promises.push(executeTranslation(lang, msgBody));
153+
});
154+
155+
const translatedMsgs = await Promise.all(promises);
156+
const translatedKeys = extranctTranslatedKeys(translatedMsgs);
157+
158+
// Inject translated keys into dstLoc object
159+
// Reset the global rec counter
160+
currentTranslationIndex = 0;
161+
const result = injectTranslatedKeys(srcObj, dstLoc, translatedKeys);
162+
163+
return result;
164+
} catch (err) {
165+
console.log(`[Exception]: executeLocalizationTranslation : ${err.message}`);
166+
return null;
167+
}
168+
}
169+
170+
const run = async () => {
171+
// Load files in the localization directory
172+
const locDirPath = path.join(__dirname, '../src/loc');
173+
let locFiles = fs.readdirSync(locDirPath);
174+
locFiles = locFiles.filter(f => f !== "mystrings.d.ts" && f != "en-us.ts");
175+
176+
// Load main localization file
177+
const mainLoc = locHelper.getSPLocalizationFileAsJSON('en-us');
178+
179+
// Iterate over all supported languages and prepare translation request
180+
for (const lang of languagesConfiguration.langs) {
181+
console.log(`Processing ${lang}.`);
182+
183+
// If current loc file doesn't exist - copy the original one
184+
let currentLoc = locHelper.getSPLocalizationFileAsJSON(lang);
185+
if (!currentLoc) {
186+
currentLoc = _.cloneDeep(mainLoc);
187+
}
188+
189+
const translatedObj = await executeLocalizationTranslation(mainLoc, currentLoc, lang);
190+
// Replace translated part in .ts file
191+
if (translatedObj) {
192+
locHelper.replaceTranslatedKeysInJSON(translatedObj, lang)
193+
}
194+
195+
// Set delay to wait for Azure to execute the transation
196+
console.log(`Finished processing ${lang}`);
197+
console.log();
198+
}
199+
};
200+
run();
201+

scripts/export-localization.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/**
2+
* Scripts exports english localization keys to the .csv file
3+
*/
4+
5+
const path = require('path');
6+
const fs = require('fs');
7+
const ts = require("typescript");
8+
require('amd-loader');
9+
10+
const jsPlaceholder =
11+
`declare var define: any;
12+
13+
define([], () => {
14+
return {0};
15+
});`;
16+
17+
const getSPLocalizationFileAsJSON = (locale) => {
18+
const locFilePath = path.join(__dirname, `../src/loc/${locale}.ts`);
19+
const tmpLocJSFilePath = path.join(__dirname, `${locale}-tmp.js`);
20+
21+
// Localization file doesn't exist
22+
if (!fs.existsSync(locFilePath)) {
23+
return null;
24+
}
25+
26+
// Read en-us localization file and transpile if to JS
27+
// Add named module to avoid amdefine excpetion
28+
const enLocFileContent = fs.readFileSync(locFilePath, 'utf-8');
29+
let result = ts.transpileModule(enLocFileContent, { compilerOptions: { module: ts.ModuleKind.CommonJS }});
30+
31+
// Create temp JS file
32+
fs.writeFileSync(tmpLocJSFilePath, result.outputText);
33+
34+
var locResources = require(`./${locale}-tmp`);
35+
36+
// Remove tmp file
37+
fs.unlinkSync(tmpLocJSFilePath);
38+
return locResources;
39+
}
40+
41+
const replaceTranslatedKeysInJSON = (translatedObj, locale) => {
42+
const locFilePath = path.join(__dirname, `../src/loc/${locale}.ts`);
43+
const fileContent = jsPlaceholder.replace('{0}', JSON.stringify(translatedObj, null, 2));
44+
45+
// Save file content
46+
fs.writeFileSync(locFilePath, fileContent);
47+
}
48+
49+
module.exports = { getSPLocalizationFileAsJSON , replaceTranslatedKeysInJSON }

0 commit comments

Comments
 (0)