diff --git a/package.json b/package.json index de7847c34b..19cc5e6af1 100644 --- a/package.json +++ b/package.json @@ -125,6 +125,7 @@ "karma-spec-reporter": "0.0.31", "karma-summary-reporter": "^1.5.1", "karma-webpack": "4.0.0-beta.0", + "loader-utils": "^1.2.3", "matrix-mock-request": "^1.2.2", "matrix-react-test-utils": "^0.2.0", "minimist": "^1.2.0", diff --git a/scripts/copy-res.js b/scripts/copy-res.js index cde96bc419..4a2c6707d0 100755 --- a/scripts/copy-res.js +++ b/scripts/copy-res.js @@ -1,5 +1,7 @@ #!/usr/bin/env node +const loaderUtils = require("loader-utils"); + // copies the resources into the webapp directory. // @@ -61,12 +63,6 @@ const COPY_LIST = [ ["./config.json", "webapp", { directwatch: 1 }], ]; -INCLUDE_LANGS.forEach(function(l) { - COPY_LIST.push([ - l.value, "webapp/i18n/", { lang: 1 }, - ]); -}); - const parseArgs = require('minimist'); const Cpx = require('cpx'); const chokidar = require('chokidar'); @@ -77,8 +73,8 @@ const argv = parseArgs( process.argv.slice(2), {} ); -var watch = argv.w; -var verbose = argv.v; +const watch = argv.w; +const verbose = argv.v; function errCheck(err) { if (err) { @@ -136,39 +132,11 @@ function next(i, err) { .on('change', copy) .on('ready', cb) .on('error', errCheck); - } else if (opts.lang) { - const reactSdkFile = 'node_modules/matrix-react-sdk/src/i18n/strings/' + source + '.json'; - const riotWebFile = 'src/i18n/strings/' + source + '.json'; - - // XXX: Use a debounce because for some reason if we read the language - // file immediately after the FS event is received, the file contents - // appears empty. Possibly https://github.com/nodejs/node/issues/6112 - let makeLangDebouncer; - const makeLang = () => { - if (makeLangDebouncer) { - clearTimeout(makeLangDebouncer); - } - makeLangDebouncer = setTimeout(() => { - genLangFile(source, dest); - }, 500); - }; - - [reactSdkFile, riotWebFile].forEach(function(f) { - chokidar.watch(f) - .on('add', makeLang) - .on('change', makeLang) - //.on('ready', cb) We'd have to do this when both files are ready - .on('error', errCheck); - }); - next(i + 1, err); } else { cpx.on('watch-ready', cb); cpx.on("watch-error", cb); cpx.watch(); } - } else if (opts.lang) { - genLangFile(source, dest); - next(i + 1, err); } else { cpx.copy(cb); } @@ -195,21 +163,28 @@ function genLangFile(lang, dest) { translations = weblateToCounterpart(translations); - fs.writeFileSync(dest + lang + '.json', JSON.stringify(translations, null, 4)); + const json = JSON.stringify(translations, null, 4); + const jsonBuffer = Buffer.from(json); + const digest = loaderUtils.getHashDigest(jsonBuffer, null, null, 7); + const filename = `${lang}.${digest}.json`; + + fs.writeFileSync(dest + filename, json); if (verbose) { - console.log("Generated language file: " + lang); + console.log("Generated language file: " + filename); } + + return filename; } -function genLangList() { +function genLangList(langFileMap) { const languages = {}; INCLUDE_LANGS.forEach(function(lang) { const normalizedLanguage = lang.value.toLowerCase().replace("_", "-"); const languageParts = normalizedLanguage.split('-'); if (languageParts.length == 2 && languageParts[0] == languageParts[1]) { - languages[languageParts[0]] = {'fileName': lang.value + '.json', 'label': lang.label}; + languages[languageParts[0]] = {'fileName': langFileMap[lang.value], 'label': lang.label}; } else { - languages[normalizedLanguage] = {'fileName': lang.value + '.json', 'label': lang.label}; + languages[normalizedLanguage] = {'fileName': langFileMap[lang.value], 'label': lang.label}; } }); fs.writeFile('webapp/i18n/languages.json', JSON.stringify(languages, null, 4), function(err) { @@ -257,5 +232,50 @@ function weblateToCounterpart(inTrs) { return outTrs; } -genLangList(); +/** +watch the input files for a given language, +regenerate the file, adding its content-hashed filename to langFileMap +and regenerating languages.json with the new filename +*/ +function watchLanguage(lang, dest, langFileMap) { + const reactSdkFile = 'node_modules/matrix-react-sdk/src/i18n/strings/' + lang + '.json'; + const riotWebFile = 'src/i18n/strings/' + lang + '.json'; + + // XXX: Use a debounce because for some reason if we read the language + // file immediately after the FS event is received, the file contents + // appears empty. Possibly https://github.com/nodejs/node/issues/6112 + let makeLangDebouncer; + const makeLang = () => { + if (makeLangDebouncer) { + clearTimeout(makeLangDebouncer); + } + makeLangDebouncer = setTimeout(() => { + const filename = genLangFile(lang, dest); + langFileMap[lang]=filename; + genLangList(langFileMap); + }, 500); + }; + + [reactSdkFile, riotWebFile].forEach(function(f) { + chokidar.watch(f) + .on('add', makeLang) + .on('change', makeLang) + .on('error', errCheck); + }); +} + +// language resources +const I18N_DEST = "webapp/i18n/"; +const I18N_FILENAME_MAP = INCLUDE_LANGS.reduce((m, l) => { + const filename = genLangFile(l.value, I18N_DEST); + m[l.value] = filename; + return m; +}, {}); +genLangList(I18N_FILENAME_MAP); + +if (watch) { + INCLUDE_LANGS.forEach(l => watchLanguage(l.value, I18N_DEST, I18N_FILENAME_MAP)); +} + +// non-language resources next(0); diff --git a/src/vector/index.html b/src/vector/index.html index 5451ec5232..f24007c964 100644 --- a/src/vector/index.html +++ b/src/vector/index.html @@ -3,22 +3,22 @@ Riot - - - - - - - - - + + + + + + + + + - + - - + + <% for (var i=0; i < htmlWebpackPlugin.files.css.length; i++) { diff --git a/webpack.config.js b/webpack.config.js index aba990541b..70ef0574a7 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -6,6 +6,12 @@ const HtmlWebpackPlugin = require('html-webpack-plugin'); let og_image_url = process.env.RIOT_OG_IMAGE_URL; if (!og_image_url) og_image_url = 'https://riot.im/app/themes/riot/img/logos/riot-im-logo-black-text.png'; +// relative to languageHandler.js in matrix-react-sdk +let RIOT_LANGUAGES_FILE = process.env.RIOT_LANGUAGES_FILE; +if (!RIOT_LANGUAGES_FILE) { + RIOT_LANGUAGES_FILE = "../../riot-web/webapp/i18n/languages.json"; +} + module.exports = { entry: { // Load babel-polyfill first to avoid issues where some imports (namely react) @@ -61,7 +67,17 @@ module.exports = { }), }, { - test: /\.(gif|png|svg|ttf)$/, + // cache-bust languages.json file placed in + // riot-web/webapp/i18n during build by copy-res.js + test: /\.*languages.json$/, + type: "javascript/auto", + loader: 'file-loader', + options: { + name: 'i18n/[name].[hash:7].[ext]', + }, + }, + { + test: /\.(gif|png|svg|ttf|xml|ico)$/, // Use a content-based hash in the name so that we can set a long cache // lifetime for assets while still delivering changes quickly. oneOf: [ @@ -148,8 +164,8 @@ module.exports = { 'process.env': { NODE_ENV: JSON.stringify(process.env.NODE_ENV), }, + 'LANGUAGES_FILE': JSON.stringify(RIOT_LANGUAGES_FILE), }), - new ExtractTextPlugin("bundles/[hash]/[name].css", { allChunks: true, }),