forked from qmk/qmk_configurator
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add translation feature from CSV (qmk#601)
* Add i18n feature Adding loader move to csv parse translations cleanup Add sync and chk for i18n Doc addition Legacy translation deletion Rebase and sync delete useless ignore replace npm to yarn fix lang label * fix status * merge fix * fix missing trad * missing translation
- Loading branch information
Showing
82 changed files
with
1,825 additions
and
3,093 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
# :globe_with_meridians: Internationalization Contribution Guide | ||
|
||
## How it works? | ||
|
||
All the translations are stored in the `CSV` files in `src/i18n`. Each line describe a key/value pair. Those files are parsed and integrated into the Front-End application during the build process. The source language for all keys is the English, stored in `src/i18n/en.csv`. | ||
|
||
The key maybe be simple or may describe a nested element with the use of namespaces. For example: | ||
```bash | ||
# this describe Loading translation as a global translation | ||
loading,Loading | ||
# output is : TranslationObject.loading | ||
``` | ||
|
||
```bash | ||
# This describe the Reload translation in the Configuration section in the admin panel | ||
configuration:admin:reload,Reload | ||
# output is : TranslationObject.configuration.admin.reload | ||
``` | ||
|
||
Using namespaces give us more readability of where the translation is, mainly for people not familiar with front-end development or javascript. | ||
|
||
If you want to dig into the code: | ||
|
||
Csv to json parsing: [scripts/i18n/csv-json.js](scripts/i18n/csv-json.js) (called with `loader.js`) | ||
|
||
Csv synchronization: [scripts/i18n/csv-sync.js](scripts/i18n/csv-sync.js) (called with `i18n:sync`) | ||
|
||
The CSV synchronization make sure the translations files are synchronized beetween each others and also reorder keys. | ||
|
||
Front-end integration: [src/i18n/index.js](src/i18n/index.js) | ||
|
||
## :suspect: How do i contribute? | ||
|
||
Just fork the project, make a branch for the completion of your translation and create a pull request. | ||
|
||
If you're not familiar with all those aspects GitHub offers a web based csv editor. Here how you proceed from the qmk_configurator repository. | ||
|
||
- go on [https://github.com/qmk/qmk_configurator](https://github.com/qmk/qmk_configurator) | ||
- Click on fork (you'll be redirected to your fork on your account) | ||
- Go on the CSV file you want to edit and click `Edit the file` with a pencil icon | ||
- Make your changes | ||
- Once you're done go to the bottom of the page and add a revelant commit message. | ||
- Click on `create a new branch for this commit and start a pull request` | ||
- go on [https://github.com/qmk/qmk_configurator](https://github.com/qmk/qmk_configurator) | ||
- You'll see `compare & pull request` button appear at the top of the page, Click on it. | ||
- Accept the pull request | ||
- Wait for the review. | ||
|
||
:raised_hands: TADA :raised_hands: | ||
|
||
__Note__: If you want to add a sentence with a comma ',' you'll have to use quotes " around the string to escape the sentence with the comma; otherwise the CSV format will not be respected. | ||
|
||
## :gift: I can provide a new language translation, how do i proceed? | ||
|
||
First you have to create a file in the `src/i18n/` folder with the [ISO 639-1 code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) (2 letters one) of the language you want to provide. For example `zh.csv` for chinese. And insert the headers in it: | ||
``` | ||
Key,Translation | ||
``` | ||
|
||
Once it's done you can use the command: | ||
|
||
``` | ||
yarn i18n:sync | ||
``` | ||
|
||
And fill the document :smiley: |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
function namespaceNesting(o, k, v) { | ||
let namespace = ''; | ||
const splittedKey = k.split(':'); | ||
let key = ''; | ||
if (splittedKey.length > 1) { | ||
namespace = splittedKey[0]; | ||
key = splittedKey[1]; | ||
} else { | ||
key = splittedKey[0]; | ||
} | ||
if (!o[namespace]) { | ||
o[namespace] = {}; | ||
} | ||
if (splittedKey.length > 2) { | ||
splittedKey.shift(); | ||
namespaceNesting(o[namespace], splittedKey.join(':'), v); | ||
} else { | ||
if (v && namespace) { | ||
o[namespace][key] = v; | ||
} else if (v) { | ||
o[key] = v; | ||
} | ||
} | ||
} | ||
|
||
function genTsObj(csvEntries) { | ||
const o = {}; | ||
for (const item of csvEntries) { | ||
namespaceNesting(o, item.Key, item.Translation); | ||
} | ||
return o; | ||
} | ||
|
||
module.exports = { | ||
genTsObj | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
const fs = require('fs'); | ||
const args = process.argv.slice(2); | ||
const { PATH_CSV } = require('./env'); | ||
const referenceFile = 'en.csv'; | ||
const files = fs | ||
.readdirSync(PATH_CSV) | ||
.filter(f => f !== referenceFile && f.endsWith('csv')); | ||
const stringify = require('csv-stringify'); | ||
const parse = require('csv-parse/lib/sync'); | ||
|
||
function aggregateRecords(records) { | ||
const common = records.filter(r => { | ||
return r.Key.split(',')[0].indexOf(':') === -1; | ||
}); | ||
const others = records.filter(r => { | ||
return r.Key.split(',')[0].indexOf(':') !== -1; | ||
}); | ||
return common.sort(sortKeys).concat(others.sort(sortKeys)); | ||
} | ||
|
||
function sortKeys(a, b) { | ||
if (a.Key < b.Key) { | ||
return -1; | ||
} | ||
if (a.Key > b.Key) { | ||
return 1; | ||
} | ||
return 0; | ||
} | ||
|
||
function readReferenceFile() { | ||
return readTranslation(referenceFile); | ||
} | ||
|
||
function readTranslation(filename) { | ||
const source = fs.readFileSync(`${PATH_CSV}${filename}`, 'utf8'); | ||
const records = parse(source, { | ||
columns: true, | ||
skip_empty_lines: true | ||
}); | ||
records.sort(sortKeys); | ||
return records; | ||
} | ||
|
||
function updateMissingTranslation( | ||
referenceTranslation, | ||
dialectTranslationArray | ||
) { | ||
const sK = dialectTranslationArray.find( | ||
s => referenceTranslation.Key === s.Key | ||
); | ||
if (!sK) { | ||
dialectTranslationArray.push({ | ||
Key: referenceTranslation.Key, | ||
Translation: '' | ||
}); | ||
} | ||
} | ||
|
||
function deleteDeprecatedTranslations(dialectArray, referenceArray) { | ||
const referenceKeys = referenceArray.map(x => x.Key); | ||
dialectArray = dialectArray.filter(x => referenceKeys.includes(x.Key)); | ||
return dialectArray; | ||
} | ||
|
||
function syncTranslationFiles(referenceTranslations) { | ||
files.forEach(f => { | ||
let dialect = fs.readFileSync(`${PATH_CSV}${f}`, 'utf8'); | ||
let records = parse(dialect, { | ||
columns: true, | ||
skip_empty_lines: true | ||
}); | ||
referenceTranslations.forEach(t => { | ||
updateMissingTranslation(t, records); | ||
}); | ||
records = deleteDeprecatedTranslations(records, referenceTranslations); | ||
stringify( | ||
[{ Key: 'Key', Translation: 'Translation' }].concat( | ||
aggregateRecords(records) | ||
), | ||
(err, csvOutput) => { | ||
fs.writeFileSync(`${PATH_CSV}${f}`, csvOutput); | ||
} | ||
); | ||
}); | ||
// Reordering the reference Translation | ||
stringify( | ||
[{ Key: 'Key', Translation: 'Translation' }].concat( | ||
aggregateRecords(referenceTranslations) | ||
), | ||
(err, csvOutput) => { | ||
fs.writeFileSync(`${PATH_CSV}en.csv`, csvOutput); | ||
} | ||
); | ||
} | ||
|
||
function checkTranslationFiles(referenceTranslations) { | ||
let errors = {}; | ||
files.forEach(f => { | ||
const dialectTranslations = readTranslation(f); | ||
const dialectKeys = dialectTranslations.map(s => s.Key); | ||
const missingTranslations = referenceTranslations.filter( | ||
t => !dialectKeys.includes(t.Key) | ||
); | ||
if (missingTranslations.length > 0) { | ||
errors[f] = missingTranslations.map(s => s.Key); | ||
} | ||
}); | ||
if (Object.keys(errors).length > 0) { | ||
console.log('Missing translations found'); | ||
console.log(''); | ||
Object.keys(errors).forEach(e => { | ||
console.log(`Missing Keys in ${e}`); | ||
console.log(''); | ||
console.log(errors[e].join('\n')); | ||
console.log(''); | ||
}); | ||
console.log('Translation report:'); | ||
Object.keys(errors).forEach(e => { | ||
console.log(`${e} : ${errors[e].length} entries missing`); | ||
}); | ||
return false; | ||
} else { | ||
console.log('Everything is synchronized'); | ||
return true; | ||
} | ||
} | ||
|
||
function main() { | ||
const reference = readReferenceFile(); | ||
switch (args[0]) { | ||
case 'sync': | ||
syncTranslationFiles(reference); | ||
break; | ||
case 'chk': | ||
if (checkTranslationFiles(reference)) { | ||
process.exit(0); | ||
} else { | ||
process.exit(1); | ||
} | ||
break; | ||
default: | ||
console.log('Invalid argument. sync / ci are only available'); | ||
process.exit(1); | ||
} | ||
} | ||
|
||
main(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
module.exports = { | ||
PATH_CSV: './src/i18n/' | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
const { genTsObj } = require('./csv-json'); | ||
const parse = require('csv-parse/lib/sync'); | ||
|
||
exports.default = function(source) { | ||
const records = parse(source, { | ||
columns: true, | ||
skip_empty_lines: true | ||
}); | ||
const output = genTsObj(records); | ||
return `export default ${JSON.stringify(output)}`; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.