parent
356f9c53db
commit
757a05900f
|
@ -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
|
|
@ -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
|
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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"
|
|
||||||
}
|
|
|
@ -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!')
|
|
|
@ -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
|
|
||||||
};
|
|
|
@ -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!')
|
|
Loading…
Reference in New Issue