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