Skip to content

Commit 9a8c7b9

Browse files
committed
replace webpack-i18n-loader
1 parent 10753ec commit 9a8c7b9

File tree

8 files changed

+177
-101
lines changed

8 files changed

+177
-101
lines changed

.circleci/config.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ version: 2.1
33
executors:
44
docker_cypress:
55
docker:
6-
- image: cypress/base:14.17.0
6+
- image: cypress/base:14.18.1
77

88
jobs:
99
# Circle's "Auto-cancel redundant builds" feature doesn't work on the default branch. When merging PRs on data, this

.nvmrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
14.17.0
1+
14.18.1

netlify.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
[build]
22
publish = "public"
33
command = "./deploy.sh"
4-
environment = {NODE_VERSION = "14.17.0", HUGO_VERSION = "0.140.0", YARN_VERSION = "1.22.4"}
4+
environment = { NODE_VERSION = "14.18.1", HUGO_VERSION = "0.140.0", YARN_VERSION = "1.22.4" }

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@
3434
],
3535
"license": "MIT",
3636
"scripts": {
37-
"build": "cross-env BABEL_ENV=production; webpack --mode=production --config webpack.prod.js",
38-
"dev": "webpack --watch --mode=development --config webpack.dev.js",
37+
"build": "yarn tsm scripts/build-i18n.ts",
38+
"dev": "yarn tsm scripts/build-i18n.ts --watch",
3939
"merge-conflict-artifact-check": "./scripts/merge-conflict-artifact-check.sh",
4040
"lint": "yarn tsc && yarn eslint src --ext .json --ext .js --ext .jsx --ext .ts --ext .tsx && yarn stylelint assets/styles && git diff --check",
4141
"fix": "yarn eslint src --ext .json --ext .js --fix && yarn stylelint assets/styles --fix",
@@ -84,6 +84,7 @@
8484
"@cypress/skip-test": "^2.6.1",
8585
"@this-dot/cypress-indexeddb": "^2.0.0",
8686
"@types/fs-extra": "^9.0.13",
87+
"@types/glob": "^8.1.0",
8788
"@types/js-cookie": "^3.0.1",
8889
"@types/preact-i18n": "^2.3.0-preactx",
8990
"@types/qrcode": "^1.4.2",
@@ -93,6 +94,7 @@
9394
"@typescript-eslint/parser": "^5.15.0",
9495
"babel-loader": "^8.0.6",
9596
"carbone": "^3.2.3",
97+
"chokidar": "4.0.1",
9698
"cross-env": "^7.0.2",
9799
"cssnano": "^5.0.12",
98100
"cypress": "^12.8.1",

scripts/build-i18n.ts

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
/**
2+
* @file This script builds our translation files for Hugo and JS because Hugo cannot do that itself. It replaces our
3+
* old custom Webpack i18n loader.
4+
*
5+
* It supports building for production (without any flags) or watch mode for development (with `--watch`). It is run by
6+
* `yarn dev` and `yarn build`.
7+
*/
8+
import { readFileSync, writeFileSync, mkdirSync } from 'fs';
9+
import { join, basename } from 'path';
10+
import glob from 'glob';
11+
import { getDirname } from 'cross-dirname';
12+
import deepmerge from 'deepmerge';
13+
import { watch as chokidar } from 'chokidar';
14+
15+
type TranslationScope = string;
16+
type TranslationKey = string;
17+
type TranslationFile = Record<TranslationScope, Record<TranslationKey, string>>;
18+
19+
const watch = process.argv.includes('--watch');
20+
const quiet = process.argv.includes('--quiet');
21+
22+
const dirname = getDirname();
23+
24+
const inputDir = join(dirname, '..', 'src', 'i18n');
25+
const hugoOutputDir = join(dirname, '..', 'i18n');
26+
const jsOutputDir = join(dirname, '..', 'static', 'js');
27+
28+
const allTranslations = Object.fromEntries(
29+
glob
30+
.sync('*.json', { cwd: inputDir, absolute: true })
31+
.map((p) => [basename(p, '.json'), readFileSync(p, 'utf8')] as const)
32+
.map((r) => [r[0], JSON.parse(r[1]) as TranslationFile] as const)
33+
);
34+
35+
// eslint-disable-next-line no-console
36+
const log = (...messages: unknown[]) => !quiet && console.log('[i18n]', ...messages, `(${new Date().toISOString()})`);
37+
38+
/**
39+
* Emit the Hugo and JS translations for the given input translations.
40+
*/
41+
const emitTranslations = (translations: Record<string, TranslationFile>) => {
42+
const en = allTranslations['en'];
43+
44+
// Ensure output directories exist.
45+
mkdirSync(hugoOutputDir, { recursive: true });
46+
mkdirSync(jsOutputDir, { recursive: true });
47+
48+
// eslint-disable-next-line prefer-const
49+
for (let [language, data] of Object.entries(translations)) {
50+
// Since we support languages where we cannot guarantee that all strings are always translated, we need to
51+
// fallback to English for untranslated strings.
52+
data = deepmerge(en, data);
53+
54+
// Macros allow us set set common strings (like the site name) once in the translations, and then use that value
55+
// in many places. Macros are used by inserting `${macro_name}` somewhere in a translation, where `macro_name`
56+
// is the key of the translation under the `macros` context that sets the macro's value.
57+
let json = JSON.stringify(data);
58+
for (const [name, value] of Object.entries(data.macros)) {
59+
json = json.replace(new RegExp(`\\\${${name}}`, 'g'), value);
60+
}
61+
data = JSON.parse(json);
62+
63+
// Emit the translation files for Hugo.
64+
if (data.hugo) {
65+
const hugoData = data.hugo;
66+
67+
// To avoid translating the same strings multiple times, we can import other translations into Hugo here.
68+
const importKeys = {
69+
generator: [
70+
'access-request-statement',
71+
'erasure-request-statement',
72+
'rectification-request-statement',
73+
'objection-request-statement',
74+
],
75+
} as const;
76+
77+
for (const scope of Object.keys(importKeys) as (keyof typeof importKeys)[]) {
78+
const keys = importKeys[scope].filter((key) => data[scope][key] !== undefined);
79+
80+
for (const key of keys) hugoData[`imported--${scope}-${key}`] = data[scope][key];
81+
}
82+
83+
writeFileSync(join(hugoOutputDir, `${language}.json`), JSON.stringify(hugoData, null, 4));
84+
}
85+
86+
// The JS translation files don't need to include the translations only used by Hugo (#620).
87+
delete data.hugo;
88+
89+
// Emit the translation files to be included in the HTML.
90+
writeFileSync(
91+
join(jsOutputDir, `translations-${language}.gen.js`),
92+
`window.I18N_DEFINITION = ${JSON.stringify(data)}`
93+
);
94+
log('Emitted translations for:', language);
95+
}
96+
};
97+
98+
/**
99+
* Emit the special requests translations file to be included in the HTML.
100+
*/
101+
const emitRequestsTranslations = () => {
102+
const requestsTranslations = Object.entries(allTranslations).reduce(
103+
(acc, [language, translations]) => ({ ...acc, [language]: translations.requests || {} }),
104+
{}
105+
);
106+
107+
writeFileSync(
108+
join(jsOutputDir, 'translations-requests.gen.js'),
109+
`window.I18N_DEFINITION_REQUESTS = ${JSON.stringify(requestsTranslations)}`
110+
);
111+
log('Emitted requests translations.');
112+
};
113+
114+
if (watch) {
115+
log('Watching translation files for changes …');
116+
117+
const watcher = chokidar(inputDir, {
118+
ignored: (path, stats) => !!stats?.isFile() && !path.endsWith('.json'),
119+
ignoreInitial: false,
120+
depth: 0,
121+
});
122+
123+
const handler = (path: string) => {
124+
try {
125+
const translations = {
126+
[basename(path, '.json')]: JSON.parse(readFileSync(path, 'utf8')) as TranslationFile,
127+
};
128+
emitTranslations(translations);
129+
emitRequestsTranslations();
130+
} catch (err) {
131+
log('Error while rebuilding translations:', err);
132+
}
133+
};
134+
watcher.on('add', handler);
135+
watcher.on('change', handler);
136+
137+
process.on('SIGINT', () => {
138+
watcher.close();
139+
log('Stopped watching translation files.');
140+
process.exit();
141+
});
142+
} else {
143+
emitTranslations(allTranslations);
144+
emitRequestsTranslations();
145+
}

scripts/webpack-i18n-loader.js

Lines changed: 0 additions & 85 deletions
This file was deleted.

webpack.common.js

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -58,17 +58,6 @@ module.exports = {
5858
loader: 'babel-loader',
5959
},
6060
},
61-
// This loader has three purposes:
62-
// * Hugo doesn't support nested translations but we want to have both the Hugo and Preact translations in a
63-
// single file. The loader simply extracts the Hugo translations, prepares them for Hugo and outputs them
64-
// to a separate file.
65-
// * It further generates the JS files to be included in the HTML for the individual languages.
66-
// * Finally, it combines the requests translations for all languages into one JS files to be included in
67-
// the HTML of all language versions.
68-
{
69-
test: /src[/\\]i18n[/\\][a-z]{2}\.json/,
70-
loader: path.resolve('scripts/webpack-i18n-loader.js'),
71-
},
7261
],
7362
},
7463
plugins: [

yarn.lock

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1770,6 +1770,14 @@
17701770
dependencies:
17711771
"@types/node" "*"
17721772

1773+
"@types/glob@^8.1.0":
1774+
version "8.1.0"
1775+
resolved "https://registry.yarnpkg.com/@types/glob/-/glob-8.1.0.tgz#b63e70155391b0584dce44e7ea25190bbc38f2fc"
1776+
integrity sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==
1777+
dependencies:
1778+
"@types/minimatch" "^5.1.2"
1779+
"@types/node" "*"
1780+
17731781
"@types/js-cookie@^3.0.1":
17741782
version "3.0.1"
17751783
resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-3.0.1.tgz#04aa743e2e0a85a22ee9aa61f6591a8bc19b5d68"
@@ -1795,6 +1803,11 @@
17951803
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
17961804
integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4=
17971805

1806+
"@types/minimatch@^5.1.2":
1807+
version "5.1.2"
1808+
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-5.1.2.tgz#07508b45797cb81ec3f273011b054cd0755eddca"
1809+
integrity sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==
1810+
17981811
"@types/minimist@^1.2.0":
17991812
version "1.2.0"
18001813
resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.0.tgz#69a23a3ad29caf0097f06eda59b361ee2f0639f6"
@@ -3415,6 +3428,13 @@ check-more-types@^2.24.0:
34153428
resolved "https://registry.yarnpkg.com/check-more-types/-/check-more-types-2.24.0.tgz#1420ffb10fd444dcfc79b43891bbfffd32a84600"
34163429
integrity sha1-FCD/sQ/URNz8ebQ4kbv//TKoRgA=
34173430

3431+
chokidar@4.0.1:
3432+
version "4.0.1"
3433+
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-4.0.1.tgz#4a6dff66798fb0f72a94f616abbd7e1a19f31d41"
3434+
integrity sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==
3435+
dependencies:
3436+
readdirp "^4.0.1"
3437+
34183438
chokidar@^2.1.8:
34193439
version "2.1.8"
34203440
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917"
@@ -10986,6 +11006,11 @@ readdirp@^2.2.1:
1098611006
micromatch "^3.1.10"
1098711007
readable-stream "^2.0.2"
1098811008

11009+
readdirp@^4.0.1:
11010+
version "4.1.2"
11011+
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.1.2.tgz#eb85801435fbf2a7ee58f19e0921b068fc69948d"
11012+
integrity sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==
11013+
1098911014
readdirp@~3.4.0:
1099011015
version "3.4.0"
1099111016
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.4.0.tgz#9fdccdf9e9155805449221ac645e8303ab5b9ada"

0 commit comments

Comments
 (0)