Commit 9544a582 authored by Eliot Berriot's avatar Eliot Berriot
Browse files

Merge branch 'po-extract' into 'develop'

Added script to extract translations into PO files

See merge request funkwhale/funkwhale!130
parents 7d71f926 35dda16a
...@@ -87,3 +87,5 @@ docs/_build ...@@ -87,3 +87,5 @@ docs/_build
data/ data/
.env .env
po/*.po
...@@ -68,6 +68,8 @@ build_front: ...@@ -68,6 +68,8 @@ build_front:
script: script:
- yarn install - yarn install
- yarn run i18n-extract
- yarn run i18n-compile
- yarn run build - yarn run build
cache: cache:
key: "$CI_PROJECT_ID__front_dependencies" key: "$CI_PROJECT_ID__front_dependencies"
......
...@@ -208,6 +208,17 @@ Typical workflow for a merge request ...@@ -208,6 +208,17 @@ Typical workflow for a merge request
8. Take a step back and enjoy, we're really grateful you did all of this and took the time to contribute! 8. Take a step back and enjoy, we're really grateful you did all of this and took the time to contribute!
Internationalization
--------------------
When working on the front-end, any end-user string should be translated
using either ``<i18next path="yourstring">`` or the ``$t('yourstring')``
function.
Extraction is done by calling ``yarn run i18n-extract``, which
will pull all the strings from source files and put them in a PO file.
Working with federation locally Working with federation locally
------------------------------- -------------------------------
...@@ -245,7 +256,7 @@ Run a reverse proxy for your instances ...@@ -245,7 +256,7 @@ Run a reverse proxy for your instances
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Crete docker network Create docker network
^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^
Create the federation network:: Create the federation network::
...@@ -265,7 +276,7 @@ need:: ...@@ -265,7 +276,7 @@ need::
export COMPOSE_PROJECT_NAME=node2 export COMPOSE_PROJECT_NAME=node2
docker-compose -f dev.yml run --rm api python manage.py migrate docker-compose -f dev.yml run --rm api python manage.py migrate
docker-compose -f dev.yml run --rm api python manage.py createsuperuser docker-compose -f dev.yml run --rm api python manage.py createsuperuser
docker-compose -f dev.yml up nginx api front docker-compose -f dev.yml up nginx api front nginx api celeryworker
Note that by default, if you don't export the COMPOSE_PROJECT_NAME, Note that by default, if you don't export the COMPOSE_PROJECT_NAME,
we will default to node1 as the name of your instance. we will default to node1 as the name of your instance.
......
Added a i18n-extract yarn script to extract strings to PO files (#162)
...@@ -29,6 +29,17 @@ fs.readdir(poDir, (err, files) => { ...@@ -29,6 +29,17 @@ fs.readdir(poDir, (err, files) => {
console.log(err) console.log(err)
} else { } else {
console.log(`Wrote translation file: ${output}`) console.log(`Wrote translation file: ${output}`)
if (lang === 'en') {
// for english, we need to specify that json values are equal to the keys.
// otherwise we end up with empty strings on the front end for english
var contents = fs.readFileSync(output)
var jsonContent = JSON.parse(contents)
var finalContent = {}
Object.keys(jsonContent).forEach(function(key) {
finalContent[key] = key
})
fs.writeFile(output, JSON.stringify(finalContent))
}
} }
}) })
}) })
...@@ -36,4 +47,3 @@ fs.readdir(poDir, (err, files) => { ...@@ -36,4 +47,3 @@ fs.readdir(poDir, (err, files) => {
} }
} }
}) })
...@@ -8,6 +8,8 @@ ...@@ -8,6 +8,8 @@
"dev": "node build/dev-server.js", "dev": "node build/dev-server.js",
"start": "node build/dev-server.js", "start": "node build/dev-server.js",
"build": "node build/build.js", "build": "node build/build.js",
"i18n-extract": "find src/ -name '*.vue' | xargs vendor/vue-i18n-xgettext/index.js > ../po/en.po",
"i18n-compile": "node build/i18n.js",
"unit": "cross-env BABEL_ENV=test karma start test/unit/karma.conf.js --single-run", "unit": "cross-env BABEL_ENV=test karma start test/unit/karma.conf.js --single-run",
"unit-watch": "cross-env BABEL_ENV=test karma start test/unit/karma.conf.js", "unit-watch": "cross-env BABEL_ENV=test karma start test/unit/karma.conf.js",
"e2e": "node test/e2e/runner.js", "e2e": "node test/e2e/runner.js",
...@@ -102,6 +104,7 @@ ...@@ -102,6 +104,7 @@
"sinon-chai": "^2.8.0", "sinon-chai": "^2.8.0",
"sinon-stub-promise": "^4.0.0", "sinon-stub-promise": "^4.0.0",
"url-loader": "^0.5.8", "url-loader": "^0.5.8",
"vue-i18n-xgettext": "^0.0.4",
"vue-loader": "^12.1.0", "vue-loader": "^12.1.0",
"vue-style-loader": "^3.0.1", "vue-style-loader": "^3.0.1",
"vue-template-compiler": "^2.3.3", "vue-template-compiler": "^2.3.3",
......
...@@ -80,16 +80,16 @@ export default { ...@@ -80,16 +80,16 @@ export default {
'privacy_level': { 'privacy_level': {
type: 'dropdown', type: 'dropdown',
initial: this.$store.state.auth.profile.privacy_level, initial: this.$store.state.auth.profile.privacy_level,
label: this.$t('Activity visibility'), label: 'Activity visibility',
help: this.$t('Determine the visibility level of your activity'), help: 'Determine the visibility level of your activity',
choices: [ choices: [
{ {
value: 'me', value: 'me',
label: this.$t('Nobody except me') label: 'Nobody except me'
}, },
{ {
value: 'instance', value: 'instance',
label: this.$t('Everyone on this instance') label: 'Everyone on this instance'
} }
] ]
} }
......
...@@ -85,9 +85,9 @@ export default { ...@@ -85,9 +85,9 @@ export default {
orderingDirection: defaultOrdering.direction, orderingDirection: defaultOrdering.direction,
ordering: defaultOrdering.field, ordering: defaultOrdering.field,
orderingOptions: [ orderingOptions: [
['title', this.$t('Track name')], ['title', 'Track name'],
['album__title', this.$t('Album name')], ['album__title', 'Album name'],
['artist__name', this.$t('Artist name')] ['artist__name', 'Artist name']
] ]
} }
}, },
......
...@@ -53,8 +53,14 @@ export default Vue.extend({ ...@@ -53,8 +53,14 @@ export default Vue.extend({
releaseImportData: [], releaseImportData: [],
releaseGroupsData: {}, releaseGroupsData: {},
releases: [], releases: [],
releaseTypes: [this.$t('Album')], releaseTypes: ['Album'],
availableReleaseTypes: [this.$t('Album'), this.$t('Live'), this.$t('Compilation'), this.$t('EP'), this.$t('Single'), this.$t('Other')] availableReleaseTypes: [
'Album',
'Live',
'Compilation',
'EP',
'Single',
'Other']
} }
}, },
created () { created () {
......
...@@ -38,13 +38,13 @@ ...@@ -38,13 +38,13 @@
<div class="ui hidden divider"></div> <div class="ui hidden divider"></div>
<div class="ui attached segment"> <div class="ui attached segment">
<template v-if="currentStep === 0"> <template v-if="currentStep === 0">
<i18next tag="p" path="First, choose where you want to import the music from:"/> <i18next tag="p" path="First, choose where you want to import the music from"/>
<form class="ui form"> <form class="ui form">
<div class="field"> <div class="field">
<div class="ui radio checkbox"> <div class="ui radio checkbox">
<input type="radio" id="external" value="external" v-model="currentSource"> <input type="radio" id="external" value="external" v-model="currentSource">
<label for="external"> <label for="external">
<i18next path="External source. Supported backends:"/> <i18next path="External source. Supported backends"/>
<div v-for="backend in backends" class="ui basic label"> <div v-for="backend in backends" class="ui basic label">
<i v-if="backend.icon" :class="[backend.icon, 'icon']"></i> <i v-if="backend.icon" :class="[backend.icon, 'icon']"></i>
{{ backend.label }} {{ backend.label }}
......
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
var _cheerio = require('cheerio');
var _cheerio2 = _interopRequireDefault(_cheerio);
var _pofile = require('pofile');
var _pofile2 = _interopRequireDefault(_pofile);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var tRegexp = new RegExp('.*\\$t\\([\'\"\`](.*)[\'\"\`]\\).*', 'g');
var Translation = function () {
function Translation(filename, lineNumber, msg) {
_classCallCheck(this, Translation);
this.filename = filename;
this.lineNumber = lineNumber;
this.msg = msg;
}
_createClass(Translation, [{
key: 'toPofileItem',
value: function toPofileItem() {
var item = new _pofile2.default.Item();
item.msgid = this.msg;
item.msgctxt = null;
item.references = [this.filename + ':' + this.lineNumber];
item.msgid_plural = null;
item.msgstr = [];
item.extractedComments = [];
return item;
}
}]);
return Translation;
}();
var Extractor = function () {
function Extractor(options) {
_classCallCheck(this, Extractor);
this.options = _extends({
startDelim: '{{',
endDelim: '}}',
attributes: ['path']
}, options);
this.translations = [];
}
_createClass(Extractor, [{
key: 'parse',
value: function parse(filename, content) {
var $ = _cheerio2.default.load(content, {
decodeEntities: false,
withStartIndices: true
});
var translations = $('template *').map(function (i, el) {
var node = $(el);
var msg = null;
if (node['0'].name === 'i18next') {
// here, we extract the translations from <i18next path="string">
msg = this.extractTranslationMessageFromI18Next(node);
}
if (msg) {
var truncatedText = content.substr(0, el.startIndex);
var lineNumber = truncatedText.split(/\r\n|\r|\n/).length;
return new Translation(filename, lineNumber, msg);
}
}.bind(this)).get();
var scriptTranslations = $('script,template').map(function (i, el) {
// here, we extract the translations from $t('string')
// within scripts and templates
var script = $(el).text();
var lines = script.split('\n');
var _translations = [];
lines.forEach(function (line) {
var truncatedText = content.substr(0, el.startIndex);
var matches;
while ((matches = tRegexp.exec(line)) !== null) {
var lineNumber = truncatedText.split(/\r\n|\r|\n/).length;
_translations.push(new Translation(filename, lineNumber, matches[1]));
}
})
return _translations
}.bind(this)).get();
this.translations = this.translations.concat(translations);
this.translations = this.translations.concat(scriptTranslations);
}
}, {
key: 'extractTranslationMessageFromI18Next',
value: function extractTranslationMessageFromI18Next(node) {
// extract from attributes
var _iteratorNormalCompletion = true;
var _didIteratorError = false;
var _iteratorError = undefined;
try {
for (var _iterator = this.options.attributes[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
var attr = _step.value;
if (node.attr('path')) {
return node.attr('path');
}
}
} catch (err) {
_didIteratorError = true;
_iteratorError = err;
} finally {
try {
if (!_iteratorNormalCompletion && _iterator.return) {
_iterator.return();
}
} finally {
if (_didIteratorError) {
throw _iteratorError;
}
}
}
}
}, {
key: 'toPofile',
value: function toPofile() {
var pofile = new _pofile2.default();
pofile.headers = {
'Last-Translator': 'vue-i18n-xgettext',
'Content-Type': 'text/plain; charset=UTF-8',
'Content-Transfer-Encoding': '8bit',
'MIME-Version': '1.1'
};
var itemMapping = {};
var _iteratorNormalCompletion2 = true;
var _didIteratorError2 = false;
var _iteratorError2 = undefined;
try {
for (var _iterator2 = this.translations[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
var translation = _step2.value;
var _item = translation.toPofileItem();
if (!itemMapping[_item.msgid]) {
itemMapping[_item.msgid] = _item;
} else {
var oldItem = itemMapping[_item.msgid];
// TODO: deal with plurals/context
if (_item.references.length && oldItem.references.indexOf(_item.references[0]) === -1) {
oldItem.references.push(_item.references[0]);
}
if (_item.extractedComments.length && soldItem.extractedComments.indexOf(_item.extractedComments[0]) === -1) {
oldItem.extractedComments.push(_item.extractedComments[0]);
}
}
}
} catch (err) {
_didIteratorError2 = true;
_iteratorError2 = err;
} finally {
try {
if (!_iteratorNormalCompletion2 && _iterator2.return) {
_iterator2.return();
}
} finally {
if (_didIteratorError2) {
throw _iteratorError2;
}
}
}
for (var msgid in itemMapping) {
var item = itemMapping[msgid];
pofile.items.push(item);
}
pofile.items.sort(function (a, b) {
return a.msgid.localeCompare(b.msgid);
});
return pofile;
}
}, {
key: 'toString',
value: function toString() {
return this.toPofile().toString();
}
}]);
return Extractor;
}();
exports.default = Extractor;
//# sourceMappingURL=extractor.js.map
#!/usr/bin/env node
'use strict';
var _fs = require('fs');
var _fs2 = _interopRequireDefault(_fs);
var _minimist = require('minimist');
var _minimist2 = _interopRequireDefault(_minimist);
var _extractor = require('./extractor.js');
var _extractor2 = _interopRequireDefault(_extractor);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
var argv = (0, _minimist2.default)(process.argv.slice(2));
var files = argv._.sort() || [];
var attributes = argv.attribute || [];
var outputFile = argv.output || null;
if (!files || files.length === 0) {
console.log('Usage: vue-i18n-xgettext [--attribute ATTRIBUTE] [--output OUTPUT_FILE] FILES');
process.exit(1);
}
var defaultAttributes = ['v-text'];
var finalAttributes = defaultAttributes;
if (typeof attributes === 'string') {
finalAttributes.push(attributes);
} else {
finalAttributes = finalAttributes.concat(attributes);
}
var extractor = new _extractor2.default({
attributes: finalAttributes
});
files.forEach(function (filename) {
var extension = filename.split('.').pop();
if (extension !== 'vue') {
console.log('file ' + filename + ' with extension ' + extension + ' will not be processed (skipped)');
return;
}
var data = _fs2.default.readFileSync(filename, { encoding: 'utf-8' }).toString();
try {
extractor.parse(filename, data);
} catch (e) {
console.trace(e);
process.exit(1);
}
});
var output = extractor.toString();
if (outputFile) {
_fs2.default.writeFileSync(outputFile, output);
} else {
console.log(output);
}
//# sourceMappingURL=index.js.map
...@@ -1311,6 +1311,27 @@ check-types@^7.3.0: ...@@ -1311,6 +1311,27 @@ check-types@^7.3.0:
version "7.3.0" version "7.3.0"
resolved "https://registry.yarnpkg.com/check-types/-/check-types-7.3.0.tgz#468f571a4435c24248f5fd0cb0e8d87c3c341e7d" resolved "https://registry.yarnpkg.com/check-types/-/check-types-7.3.0.tgz#468f571a4435c24248f5fd0cb0e8d87c3c341e7d"
cheerio@^0.22.0:
version "0.22.0"
resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-0.22.0.tgz#a9baa860a3f9b595a6b81b1a86873121ed3a269e"
dependencies:
css-select "~1.2.0"
dom-serializer "~0.1.0"
entities "~1.1.1"
htmlparser2 "^3.9.1"
lodash.assignin "^4.0.9"
lodash.bind "^4.1.4"
lodash.defaults "^4.0.1"
lodash.filter "^4.4.0"
lodash.flatten "^4.2.0"
lodash.foreach "^4.3.0"
lodash.map "^4.4.0"
lodash.merge "^4.4.0"
lodash.pick "^4.2.1"
lodash.reduce "^4.4.0"
lodash.reject "^4.4.0"
lodash.some "^4.4.0"
chokidar@^1.4.1: chokidar@^1.4.1:
version "1.7.0" version "1.7.0"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468"
...@@ -1783,7 +1804,7 @@ css-loader@^0.28.0: ...@@ -1783,7 +1804,7 @@ css-loader@^0.28.0:
postcss-value-parser "^3.3.0" postcss-value-parser "^3.3.0"
source-list-map "^2.0.0" source-list-map "^2.0.0"
css-select@^1.1.0: css-select@^1.1.0, css-select@~1.2.0:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858" resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858"
dependencies: dependencies:
...@@ -2110,7 +2131,7 @@ dom-serialize@^2.2.0: ...@@ -2110,7 +2131,7 @@ dom-serialize@^2.2.0:
extend "^3.0.0" extend "^3.0.0"
void-elements "^2.0.0" void-elements "^2.0.0"
dom-serializer@0: dom-serializer@0, dom-serializer@~0.1.0:
version "0.1.0" version "0.1.0"
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82"
dependencies: dependencies:
...@@ -2972,6 +2993,10 @@ fs.realpath@^1.0.0: ...@@ -2972,6 +2993,10 @@ fs.realpath@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
fs@0.0.1-security:
version "0.0.1-security"
resolved "https://registry.yarnpkg.com/fs/-/fs-0.0.1-security.tgz#8a7bd37186b6dddf3813f23858b57ecaaf5e41d4"
fsevents@^1.0.0, fsevents@^1.1.2: fsevents@^1.0.0, fsevents@^1.1.2:
version "1.1.3" version "1.1.3"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.1.3.tgz#11f82318f5fe7bb2cd22965a108e9306208216d8" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.1.3.tgz#11f82318f5fe7bb2cd22965a108e9306208216d8"
...@@ -3433,7 +3458,7 @@ html-webpack-plugin@^2.28.0: ...@@ -3433,7 +3458,7 @@ html-webpack-plugin@^2.28.0:
pretty-error "^2.0.2" pretty-error "^2.0.2"
toposort "^1.0.0" toposort "^1.0.0"
htmlparser2@^3.8.2: htmlparser2@^3.8.2, htmlparser2@^3.9.1:
version "3.9.2" version "3.9.2"
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.9.2.tgz#1bdf87acca0f3f9e53fa4fcceb0f4b4cbb00b338" resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.9.2.tgz#1bdf87acca0f3f9e53fa4fcceb0f4b4cbb00b338"
dependencies: dependencies:
...@@ -3523,10 +3548,6 @@ https-proxy-agent@1: ...@@ -3523,10 +3548,6 @@ https-proxy-agent@1:
debug "2" debug "2"
extend "3" extend "3"
i18next-browser-languagedetector@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/i18next-browser-languagedetector/-/i18next-browser-languagedetector-2.2.0.tgz#5f41abe61964a56dce70102ab31c3ed5d5866edc"
i18next-conv@^6.0.0: i18next-conv@^6.0.0:
version "6.0.0" version "6.0.0"
resolved "https://registry.yarnpkg.com/i18next-conv/-/i18next-conv-6.0.0.tgz#875a27bfb069db894f7b0a1484e0052100bc9383" resolved "https://registry.yarnpkg.com/i18next-conv/-/i18next-conv-6.0.0.tgz#875a27bfb069db894f7b0a1484e0052100bc9383"
...@@ -4343,6 +4364,14 @@ lodash.assign@^4.2.0: ...@@ -4343,6 +4364,14 @@ lodash.assign@^4.2.0:
version "4.2.0" version "4.2.0"
resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7" resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7"
lodash.assignin@^4.0.9: