From 757a05900f646db71bf903f040a9060cc71aee1b Mon Sep 17 00:00:00 2001 From: Patryk Osmaczko Date: Mon, 9 May 2022 08:39:39 +0200 Subject: [PATCH] chore(translations): update i18n docs issue: #4544 --- I18N.md | 32 +++++ scripts/translationScripts/README.md | 59 -------- scripts/translationScripts/package-lock.json | 128 ------------------ scripts/translationScripts/package.json | 17 --- scripts/translationScripts/qstrConverter.js | 61 --------- .../status-react-translations/.gitkeep | 0 scripts/translationScripts/utils.js | 20 --- scripts/translationScripts/xmlTranslator.js | 72 ---------- 8 files changed, 32 insertions(+), 357 deletions(-) create mode 100644 I18N.md delete mode 100644 scripts/translationScripts/README.md delete mode 100644 scripts/translationScripts/package-lock.json delete mode 100644 scripts/translationScripts/package.json delete mode 100644 scripts/translationScripts/qstrConverter.js delete mode 100644 scripts/translationScripts/status-react-translations/.gitkeep delete mode 100644 scripts/translationScripts/utils.js delete mode 100644 scripts/translationScripts/xmlTranslator.js diff --git a/I18N.md b/I18N.md new file mode 100644 index 0000000000..e4430ace72 --- /dev/null +++ b/I18N.md @@ -0,0 +1,32 @@ +# Internationalization + +App is translated to many different languages through lokalise.com platform. + +## TLDR + +#### Update base translations file +1. Update qml_en.ts file: `cd scripts/translationScripts && python update-en-ts.py` +2. Ensure updated qml_en.ts file lands on master + +#### Update translation binaries +1. Download translated *.ts files from lokalise.com +2. Override ui/i18n/*.ts files with downloaded ones +2. Create translation binaries `lrelease ui/i18n/*.ts` +3. Commit updated *.ts and *.qm files + +## Lokalise workflow + +Lokalise is a continuous localization and translation management platform. It integrates into development workflow and automates localization process. + +Lokalise workflow: +1. Upload english strings (qml_en.ts) to Lokalise project. This is done automatically, Lokalise auto-pull changes done to master's qml_en.ts file. +2. Translate strings to target languages. Target languages are driven by Lokalise configuration. Translations are done by community. +3. Export *.ts files with translations (e.g. qml_de.ts, qml_en.ts) + +## Updating translation files + +Updating the QML translation files is very easy, as it comes with QT directly. It will scan all files in the projects (those listed in the `SOURCE` section of the `.pro` file) and then add or modify them in the XML-like `.ts` files. + +## Generating binary translation files + +To have the final translation files that will be used by the app, just run `lrelease i18n/*.ts` in the `ui/` directory diff --git a/scripts/translationScripts/README.md b/scripts/translationScripts/README.md deleted file mode 100644 index 86d9334807..0000000000 --- a/scripts/translationScripts/README.md +++ /dev/null @@ -1,59 +0,0 @@ -# Translation scripts - -These scripts are used to translate the app automatically by reusing the existing translation found in the Status-React repo: https://github.com/status-im/status-react/tree/develop/translations - -## TLDR - -1. Copy the translation files from https://github.com/status-im/status-react/tree/develop/translations to `/nim-status-client/scripts/translationScripts/status-react-translations` -2. `cd scripts/translationScripts` -3. Run `npm install` -4. Run `node qstrConverter.js` -5. Open another terminal and `cd ui` -6. In that second terminal, run `lupdate nim-status-client.pro` -7. Back in the first terminal, run `node xmlTranslator.js` -7. [Optional] Manually translate the remaining strings in QT Linguist -9. In the second terminal, run `lrelease -idbased i18n/*.ts` in the `ui/` directory - -:tada: You're files are converted to use `qsTrId` and the translation files are updated. - -## Changing strings to IDs - -One major step is to change the literal strings we use in the code base to the IDs that are used in the translation JSON files. - -For example, in our QML files, we would use `qsTr("Public chat")`, but in Status-React, that string in only represented as `public-chat`. - -Thankfully, QML supports using string IDs instead of literral strings. The trick is to use `qsTrId` instead of `qsTr` and then use a comment to show the context/original string. - -The script to do the change from `qsTr` to `qsTrId` is `qstrConverter.js`. - -First, copy the translation files from https://github.com/status-im/status-react/tree/develop/translations to `/nim-status-client/scripts/translationScripts/status-react-translations`. Those are gitignored to show that we do not maintain those ourselves. - -Then, run `node qstrConverter.js` in the `translationScripts/` directory. - -## Updating translation files - -Updating the QML translation files is then very easy, as it comes with QT directly. It will scan all files in the projects (those listed in the `SOURCE` section of the `.pro` file) and then add or modify them in the XML-like `.ts` files. - -Just run `lupdate nim-status-client.pro` in the `ui/` directory. - -## Run XML translator script - -Most translations are already done in Status-React. To add those translations to the right `.ts` file, run `node xmlTranslator.js` in the `translationScripts/` directory. - -It will check all the TS files and get the good translation from the JSON file and set the translation as done. - -Some translations will not be done, check the next section to know how to translate. - -## Manually translate remaining strings - -Since not all strings used in the desktop app are also used in Status-React, the remaining will need to be translated manually. - -If the strings are not translated, it is not the end of the world, the English strings will be shown instead. - -To do so, you can use QT Linguist to help with the process. Check here to see the Linguist docs: https://doc.qt.io/qt-5/linguist-translators.html - -To open a TS file in QT Linguist, either open the software and use the `Open` feature it has, or go in the `ui/i18n` directory and run `linguist nameOfFile.ts` - -## Generating binary translation files - -To have the final translation files that will be used by the app, just run `lrelease -idbased i18n/*.ts` in the `ui/` directory diff --git a/scripts/translationScripts/package-lock.json b/scripts/translationScripts/package-lock.json deleted file mode 100644 index 4e8eba9ab9..0000000000 --- a/scripts/translationScripts/package-lock.json +++ /dev/null @@ -1,128 +0,0 @@ -{ - "name": "translationscripts", - "version": "1.0.0", - "lockfileVersion": 2, - "requires": true, - "packages": { - "": { - "name": "translationscripts", - "version": "1.0.0", - "license": "ISC", - "dependencies": { - "fs-extra": "^9.0.1", - "xml-js": "^1.6.11" - }, - "devDependencies": {} - }, - "node_modules/at-least-node": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", - "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/fs-extra": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.1.tgz", - "integrity": "sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ==", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^1.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", - "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" - }, - "node_modules/jsonfile": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.0.1.tgz", - "integrity": "sha512-jR2b5v7d2vIOust+w3wtFKZIfpC2pnRmFAhAC/BuweZFQR8qZzxH1OyrQ10HmdVYiXWkYUqPVsz91cG7EL2FBg==", - "dependencies": { - "graceful-fs": "^4.1.6", - "universalify": "^1.0.0" - } - }, - "node_modules/sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" - }, - "node_modules/universalify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz", - "integrity": "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/xml-js": { - "version": "1.6.11", - "resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz", - "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==", - "dependencies": { - "sax": "^1.2.4" - }, - "bin": { - "xml-js": "bin/cli.js" - } - } - }, - "dependencies": { - "at-least-node": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", - "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==" - }, - "fs-extra": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.1.tgz", - "integrity": "sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ==", - "requires": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^1.0.0" - } - }, - "graceful-fs": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", - "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" - }, - "jsonfile": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.0.1.tgz", - "integrity": "sha512-jR2b5v7d2vIOust+w3wtFKZIfpC2pnRmFAhAC/BuweZFQR8qZzxH1OyrQ10HmdVYiXWkYUqPVsz91cG7EL2FBg==", - "requires": { - "graceful-fs": "^4.1.6", - "universalify": "^1.0.0" - } - }, - "sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" - }, - "universalify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz", - "integrity": "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==" - }, - "xml-js": { - "version": "1.6.11", - "resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz", - "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==", - "requires": { - "sax": "^1.2.4" - } - } - } -} diff --git a/scripts/translationScripts/package.json b/scripts/translationScripts/package.json deleted file mode 100644 index 66a6929c77..0000000000 --- a/scripts/translationScripts/package.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "translationscripts", - "private": true, - "version": "1.0.0", - "description": "These scripts are used to translate the app automatically by reusing the existing translation found in the Status-React repo: https://github.com/status-im/status-react/tree/develop/translations", - "main": "qstrConverter.js", - "dependencies": { - "fs-extra": "^9.0.1", - "xml-js": "^1.6.11" - }, - "devDependencies": {}, - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "author": "jrainville", - "license": "ISC" -} diff --git a/scripts/translationScripts/qstrConverter.js b/scripts/translationScripts/qstrConverter.js deleted file mode 100644 index 2bc1e5360e..0000000000 --- a/scripts/translationScripts/qstrConverter.js +++ /dev/null @@ -1,61 +0,0 @@ -const fs = require('fs'); -const {getAllFiles} = require('./utils'); - -const enTranslations = require('./status-react-translations/en.json'); - -console.log('Scanning QML files...') -const qmlFiles = getAllFiles('../../ui', 'qml'); - -const translationKeys = Object.keys(enTranslations) -const translationValues = Object.values(enTranslations) - -// Match all qsTr("...") functions and get the string inside -const qstrRegex = /qsTr\(["'](.*?)["']\)/g -const tabsRegex = /\n([\s]+)/ -let numberOfFilesDone = 0 - -console.log(`Modifying ${qmlFiles.length} files...`) -qmlFiles.forEach(file => { - let fileContent = fs.readFileSync(file).toString(); - - let match, replaceableText, enTranslationIndex, lastSpace, tabSubstring, spaces, replacementId, quote; - let modified = false; - - while ((match = qstrRegex.exec(fileContent)) !== null) { - modified = true; - replaceableText = match[1]; - - enTranslationIndex = translationValues.indexOf(replaceableText) - if (enTranslationIndex > -1) { - replacementId = translationKeys[enTranslationIndex] - } else { - // We need to replace all qsTr because we can't mix qsTrId and qsTr - replacementId = replaceableText.replace(/[^a-zA-Z\d]/g, '-').toLowerCase(); - } - - quote = match[0][5]; - // Replace the qsTr by a qsTrId and a comment - fileContent = fileContent.replace(`qsTr(${quote}${replaceableText}${quote})`, `qsTrId("${replacementId}")`) - - // Find the place where to put the comment - lastSpace = fileContent.lastIndexOf('\n ', match.index); - tabSubstring = fileContent.substring(lastSpace, match.index); - - spaces = tabsRegex.exec(tabSubstring); - fileContent = fileContent.substring(0, lastSpace + 1) + - spaces[1] + `//% "${replaceableText}"` + - fileContent.substring(lastSpace); - - // Increase the last index of the regex as we increased the size of the file and so if the next qstr is on the same line, - // the chances are high that the next match willl be the same word, creating an infinite loop - qstrRegex.lastIndex += spaces[1].length + 6 + replaceableText.length - } - - - fs.writeFileSync(file, fileContent); - numberOfFilesDone++; - if (numberOfFilesDone % 10 === 0 || numberOfFilesDone === qmlFiles.length) { - console.log(`\t${numberOfFilesDone}/${qmlFiles.length} completed...`) - } -}); -console.log('All done!') diff --git a/scripts/translationScripts/status-react-translations/.gitkeep b/scripts/translationScripts/status-react-translations/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/scripts/translationScripts/utils.js b/scripts/translationScripts/utils.js deleted file mode 100644 index 7b19890287..0000000000 --- a/scripts/translationScripts/utils.js +++ /dev/null @@ -1,20 +0,0 @@ -const fs = require('fs'); -const path = require("path") - -const getAllFiles = function(dirPath, ext, arrayOfFiles = []) { - const files = fs.readdirSync(dirPath) - - files.forEach(file => { - if (fs.statSync(path.join(dirPath, file)).isDirectory()) { - arrayOfFiles = getAllFiles(path.join(dirPath, file), ext, arrayOfFiles) - } else if (!ext || file.endsWith(ext)) { - arrayOfFiles.push(path.join(__dirname, dirPath, file)) - } - }) - - return arrayOfFiles -} - -module.exports = { - getAllFiles -}; diff --git a/scripts/translationScripts/xmlTranslator.js b/scripts/translationScripts/xmlTranslator.js deleted file mode 100644 index 992843d8c8..0000000000 --- a/scripts/translationScripts/xmlTranslator.js +++ /dev/null @@ -1,72 +0,0 @@ -const convert = require('xml-js'); -const fs = require('fs'); -const {getAllFiles} = require('./utils'); - -console.log('Scanning TS files...'); -const tsFiles = getAllFiles('../../ui/i18n', 'ts'); - -const options = {compact: true, spaces: 4}; - -tsFiles.forEach(file => { - if (file.endsWith('base.ts')) { - // We skip the base file - return; - } - const fileContent = fs.readFileSync(file).toString(); - const json = convert.xml2js(fileContent, options); - - const doctype = json["_doctype"]; - let language = json[doctype]._attributes.language; - const isEn = language === 'en_US' - - let translations; - try { - translations = require(`./status-react-translations/${language}.json`) - } catch (e) { - // No translation file for the exact match, let's use the file name instead - const match = /qml_([a-zA-Z0-9_]+)\.ts/.exec(file) - language = language || match[1]; - try { - translations = require(`./status-react-translations/${match[1]}.json`) - } catch (e) { - console.error(`No translation file found for ${language}`); - return; - } - } - - let messages = [] - if (json[doctype].context.length) { - messages = json[doctype].context.flatMap(c => c.message) - } else { - messages = json[doctype].context.message; - } - - console.log(`Modying ${language}...`) - messages.forEach(message => { - if (!message._attributes || !message._attributes.id) { - return; - } - if (isEn) { - // We just put the source string in the tranlsation - message.translation = { - "_text": message.source._text - } - return; - } - const messageId = message._attributes.id; - if (!translations[messageId]) { - // Skip this message, as we have no translation - return; - } - - message.translation = { - "_text": translations[messageId] - } - }); - - const xml = convert.js2xml(json, options); - - fs.writeFileSync(file, xml); -}); - -console.log('All done!')