chore(translations): update i18n docs

issue: #4544
This commit is contained in:
Patryk Osmaczko 2022-05-09 08:39:39 +02:00 committed by osmaczko
parent 356f9c53db
commit 757a05900f
8 changed files with 32 additions and 357 deletions

32
I18N.md Normal file
View File

@ -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

View File

@ -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

View File

@ -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"
}
}
}
}

View File

@ -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"
}

View File

@ -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!')

View File

@ -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
};

View File

@ -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!')