From eeec686cf42b5beb4985d8891bd953c339945658 Mon Sep 17 00:00:00 2001 From: Brieuc Dubois <git@bhasher.com> Date: Sun, 17 Mar 2024 00:20:46 +0100 Subject: [PATCH] frontend: internationalization --- frontend/package-lock.json | 323 ++++++++++++++++-- frontend/package.json | 2 + frontend/rollup.config.js | 5 + frontend/src/lib/components/header.svelte | 9 +- .../components/header/localSelector.svelte | 20 ++ frontend/src/lib/services/i18n.ts | 82 +++++ frontend/src/routes/+layout.svelte | 15 +- frontend/static/lang/en.json | 6 + frontend/static/lang/fr.json | 6 + 9 files changed, 430 insertions(+), 38 deletions(-) create mode 100644 frontend/rollup.config.js create mode 100644 frontend/src/lib/components/header/localSelector.svelte create mode 100644 frontend/src/lib/services/i18n.ts create mode 100644 frontend/static/lang/en.json create mode 100644 frontend/static/lang/fr.json diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 1d111cde..f0ca6d69 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11,10 +11,12 @@ "axios": "^1.6.7", "axios-jwt": "^4.0.2", "jwt-decode": "^4.0.0", + "svelte-i18n": "^4.0.0", "svelte-material-icons": "^3.0.5", "svelte-select": "^5.8.3" }, "devDependencies": { + "@rollup/plugin-json": "^6.1.0", "@sveltejs/adapter-auto": "^3.0.0", "@sveltejs/adapter-static": "^3.0.1", "@sveltejs/kit": "^2.0.0", @@ -81,7 +83,6 @@ "cpu": [ "ppc64" ], - "dev": true, "optional": true, "os": [ "aix" @@ -97,7 +98,6 @@ "cpu": [ "arm" ], - "dev": true, "optional": true, "os": [ "android" @@ -113,7 +113,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "android" @@ -129,7 +128,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "android" @@ -145,7 +143,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "darwin" @@ -161,7 +158,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "darwin" @@ -177,7 +173,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "freebsd" @@ -193,7 +188,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "freebsd" @@ -209,7 +203,6 @@ "cpu": [ "arm" ], - "dev": true, "optional": true, "os": [ "linux" @@ -225,7 +218,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -241,7 +233,6 @@ "cpu": [ "ia32" ], - "dev": true, "optional": true, "os": [ "linux" @@ -257,7 +248,6 @@ "cpu": [ "loong64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -273,7 +263,6 @@ "cpu": [ "mips64el" ], - "dev": true, "optional": true, "os": [ "linux" @@ -289,7 +278,6 @@ "cpu": [ "ppc64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -305,7 +293,6 @@ "cpu": [ "riscv64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -321,7 +308,6 @@ "cpu": [ "s390x" ], - "dev": true, "optional": true, "os": [ "linux" @@ -337,7 +323,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -353,7 +338,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "netbsd" @@ -369,7 +353,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "openbsd" @@ -385,7 +368,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "sunos" @@ -401,7 +383,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "win32" @@ -417,7 +398,6 @@ "cpu": [ "ia32" ], - "dev": true, "optional": true, "os": [ "win32" @@ -433,7 +413,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "win32" @@ -542,6 +521,50 @@ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz", "integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==" }, + "node_modules/@formatjs/ecma402-abstract": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.18.2.tgz", + "integrity": "sha512-+QoPW4csYALsQIl8GbN14igZzDbuwzcpWrku9nyMXlaqAlwRBgl5V+p0vWMGFqHOw37czNXaP/lEk4wbLgcmtA==", + "dependencies": { + "@formatjs/intl-localematcher": "0.5.4", + "tslib": "^2.4.0" + } + }, + "node_modules/@formatjs/fast-memoize": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.0.tgz", + "integrity": "sha512-hnk/nY8FyrL5YxwP9e4r9dqeM6cAbo8PeU9UjyXojZMNvVad2Z06FAVHyR3Ecw6fza+0GH7vdJgiKIVXTMbSBA==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@formatjs/icu-messageformat-parser": { + "version": "2.7.6", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.7.6.tgz", + "integrity": "sha512-etVau26po9+eewJKYoiBKP6743I1br0/Ie00Pb/S/PtmYfmjTcOn2YCh2yNkSZI12h6Rg+BOgQYborXk46BvkA==", + "dependencies": { + "@formatjs/ecma402-abstract": "1.18.2", + "@formatjs/icu-skeleton-parser": "1.8.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@formatjs/icu-skeleton-parser": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.0.tgz", + "integrity": "sha512-QWLAYvM0n8hv7Nq5BEs4LKIjevpVpbGLAJgOaYzg9wABEoX1j0JO1q2/jVkO6CVlq0dbsxZCngS5aXbysYueqA==", + "dependencies": { + "@formatjs/ecma402-abstract": "1.18.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@formatjs/intl-localematcher": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.4.tgz", + "integrity": "sha512-zTwEpWOzZ2CiKcB93BLngUX59hQkuZjT2+SAQEscSm52peDW/getsawMcWF1rGRpMCX6D7nSJA3CzJ8gn13N/g==", + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -735,6 +758,54 @@ "integrity": "sha512-2LuNTFBIO0m7kKIQvvPHN6UE63VjpmL9rnEEaOOaiSPbZK+zUOYIzBAWcED+3XYzhYsd/0mD57VdxAEqqV52CQ==", "dev": true }, + "node_modules/@rollup/plugin-json": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz", + "integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^5.1.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz", + "integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.12.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.12.0.tgz", @@ -1618,6 +1689,21 @@ "node": ">= 6" } }, + "node_modules/cli-color": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/cli-color/-/cli-color-2.0.4.tgz", + "integrity": "sha512-zlnpg0jNcibNrO7GG9IeHH7maWFeCz+Ja1wx/7tZNU5ASSSSZ+/qZciM0/LHCYxSdqv5h2sdbQ/PXYdOuetXvA==", + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.64", + "es6-iterator": "^2.0.3", + "memoizee": "^0.4.15", + "timers-ext": "^0.1.7" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/code-red": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/code-red/-/code-red-1.0.4.tgz", @@ -1733,6 +1819,18 @@ "node": ">=4" } }, + "node_modules/d": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz", + "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==", + "dependencies": { + "es5-ext": "^0.10.64", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.12" + } + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -1760,7 +1858,6 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -1863,17 +1960,64 @@ "errno": "cli.js" } }, + "node_modules/es5-ext": { + "version": "0.10.64", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", + "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", + "hasInstallScript": true, + "dependencies": { + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.3", + "esniff": "^2.0.1", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, "node_modules/es6-promise": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", "integrity": "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==", "dev": true }, + "node_modules/es6-symbol": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz", + "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==", + "dependencies": { + "d": "^1.0.2", + "ext": "^1.7.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/es6-weak-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz", + "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.46", + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.1" + } + }, "node_modules/esbuild": { "version": "0.19.12", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", - "dev": true, "hasInstallScript": true, "bin": { "esbuild": "bin/esbuild" @@ -2098,6 +2242,20 @@ "integrity": "sha512-Cf6VksWPsTuW01vU9Mk/3vRue91Zevka5SjyNf3nEpokFRuqt/KjUQoGAwq9qMmhpLTHmXzSIrFRw8zxWzmFBA==", "dev": true }, + "node_modules/esniff": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", + "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.62", + "event-emitter": "^0.3.5", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", @@ -2165,6 +2323,23 @@ "node": ">=0.10.0" } }, + "node_modules/event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", + "dependencies": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, + "node_modules/ext": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", + "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", + "dependencies": { + "type": "^2.7.2" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2442,8 +2617,7 @@ "node_modules/globalyzer": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/globalyzer/-/globalyzer-0.1.0.tgz", - "integrity": "sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==", - "dev": true + "integrity": "sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==" }, "node_modules/globby": { "version": "11.1.0", @@ -2468,8 +2642,7 @@ "node_modules/globrex": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", - "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", - "dev": true + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==" }, "node_modules/graceful-fs": { "version": "4.2.11", @@ -2590,6 +2763,17 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, + "node_modules/intl-messageformat": { + "version": "10.5.11", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.5.11.tgz", + "integrity": "sha512-eYq5fkFBVxc7GIFDzpFQkDOZgNayNTQn4Oufe8jw6YY6OHVw70/4pA3FyCsQ0Gb2DnvEJEMmN2tOaXUGByM+kg==", + "dependencies": { + "@formatjs/ecma402-abstract": "1.18.2", + "@formatjs/fast-memoize": "2.2.0", + "@formatjs/icu-messageformat-parser": "2.7.6", + "tslib": "^2.4.0" + } + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -2662,6 +2846,11 @@ "node": ">=8" } }, + "node_modules/is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==" + }, "node_modules/is-reference": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz", @@ -2863,6 +3052,14 @@ "node": ">=10" } }, + "node_modules/lru-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", + "integrity": "sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==", + "dependencies": { + "es5-ext": "~0.10.2" + } + }, "node_modules/magic-string": { "version": "0.30.8", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz", @@ -2903,6 +3100,21 @@ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==" }, + "node_modules/memoizee": { + "version": "0.4.15", + "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.15.tgz", + "integrity": "sha512-UBWmJpLZd5STPm7PMUlOw/TSy972M+z8gcyQ5veOnSDRREz/0bmpyTfKt3/51DhEBqCZQn1udM/5flcSPYhkdQ==", + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.53", + "es6-weak-map": "^2.0.3", + "event-emitter": "^0.3.5", + "is-promise": "^2.2.2", + "lru-queue": "^0.1.0", + "next-tick": "^1.1.0", + "timers-ext": "^0.1.7" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -3015,7 +3227,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", - "dev": true, "engines": { "node": ">=4" } @@ -3087,6 +3298,11 @@ "node": ">= 4.4.x" } }, + "node_modules/next-tick": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", + "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==" + }, "node_modules/node-releases": { "version": "2.0.14", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", @@ -3708,7 +3924,6 @@ "version": "1.8.1", "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", - "dev": true, "dependencies": { "mri": "^1.1.0" }, @@ -4155,6 +4370,34 @@ "svelte": "^3.19.0 || ^4.0.0" } }, + "node_modules/svelte-i18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/svelte-i18n/-/svelte-i18n-4.0.0.tgz", + "integrity": "sha512-4vivjKZADUMRIhTs38JuBNy3unbnh9AFRxWFLxq62P4NHic+/BaIZZlAsvqsCdnp7IdJf5EoSiH6TNdItcjA6g==", + "dependencies": { + "cli-color": "^2.0.3", + "deepmerge": "^4.2.2", + "esbuild": "^0.19.2", + "estree-walker": "^2", + "intl-messageformat": "^10.5.3", + "sade": "^1.8.1", + "tiny-glob": "^0.2.9" + }, + "bin": { + "svelte-i18n": "dist/cli.js" + }, + "engines": { + "node": ">= 16" + }, + "peerDependencies": { + "svelte": "^3 || ^4" + } + }, + "node_modules/svelte-i18n/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" + }, "node_modules/svelte-material-icons": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/svelte-material-icons/-/svelte-material-icons-3.0.5.tgz", @@ -4357,11 +4600,19 @@ "node": ">=0.8" } }, + "node_modules/timers-ext": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.7.tgz", + "integrity": "sha512-b85NUNzTSdodShTIbky6ZF02e8STtVVfD+fu4aXXShEELpozH+bCpJLYMPZbsABN2wDH7fJpqIoXxJpzbf0NqQ==", + "dependencies": { + "es5-ext": "~0.10.46", + "next-tick": "1" + } + }, "node_modules/tiny-glob": { "version": "0.2.9", "resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz", "integrity": "sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==", - "dev": true, "dependencies": { "globalyzer": "0.1.0", "globrex": "^0.1.2" @@ -4409,8 +4660,12 @@ "node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", - "dev": true + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "node_modules/type": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.2.tgz", + "integrity": "sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==" }, "node_modules/type-check": { "version": "0.4.0", diff --git a/frontend/package.json b/frontend/package.json index 6f0f517d..e9ff5b31 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,6 +12,7 @@ "format": "prettier --write ." }, "devDependencies": { + "@rollup/plugin-json": "^6.1.0", "@sveltejs/adapter-auto": "^3.0.0", "@sveltejs/adapter-static": "^3.0.1", "@sveltejs/kit": "^2.0.0", @@ -42,6 +43,7 @@ "axios": "^1.6.7", "axios-jwt": "^4.0.2", "jwt-decode": "^4.0.0", + "svelte-i18n": "^4.0.0", "svelte-material-icons": "^3.0.5", "svelte-select": "^5.8.3" } diff --git a/frontend/rollup.config.js b/frontend/rollup.config.js new file mode 100644 index 00000000..983b1302 --- /dev/null +++ b/frontend/rollup.config.js @@ -0,0 +1,5 @@ +import json from '@rollup/plugin-json'; + +export default { + plugins: [json(), svelte({})] +}; diff --git a/frontend/src/lib/components/header.svelte b/frontend/src/lib/components/header.svelte index c191440b..dd7e6c23 100644 --- a/frontend/src/lib/components/header.svelte +++ b/frontend/src/lib/components/header.svelte @@ -1,15 +1,17 @@ -<script> +<script lang="ts"> import session from '$lib/stores/JWTSession'; import { get } from 'svelte/store'; import Logout from 'svelte-material-icons/Logout.svelte'; import Login from 'svelte-material-icons/Login.svelte'; + import LocalSelector from './header/localSelector.svelte'; + import { _ } from '$lib/services/i18n'; </script> <header class="bg-secondary text-white flex align-middle justify-between px-4 py-2"> - <h1 class="font-bold text-2xl"><a href="/">LanguageLab</a></h1> + <h1 class="font-bold text-2xl"><a href="/">{$_('header.appName')}</a></h1> <div class="flex align-middle"> {#if session.isLoggedIn()} - <span class="pr-2">Connecté en tant que <strong>{get(session.username)}</strong></span> + <span class="pr-2">{$_('header.connectedAs')} <strong>{get(session.username)}</strong></span> <a href="/logout"><Logout class="h-4/5" /></a> {:else} <a @@ -18,5 +20,6 @@ )}"><Login /></a > {/if} + <LocalSelector /> </div> </header> diff --git a/frontend/src/lib/components/header/localSelector.svelte b/frontend/src/lib/components/header/localSelector.svelte new file mode 100644 index 00000000..08926bd5 --- /dev/null +++ b/frontend/src/lib/components/header/localSelector.svelte @@ -0,0 +1,20 @@ +<script lang="ts"> + import { _, _activeLocale, locales, setupI18n } from '../../services/i18n'; + + let value: string = _activeLocale; + + function onChange(event: Event) { + const target = event.target as HTMLSelectElement; + setupI18n({ + withLocale: target.value + }); + } +</script> + +<div class="ml-2"> + <select {value} on:change={onChange} class="bg-transparent"> + {#each Object.entries(locales) as [locale, name] (locale)} + <option value={locale}>{name}</option> + {/each} + </select> +</div> diff --git a/frontend/src/lib/services/i18n.ts b/frontend/src/lib/services/i18n.ts new file mode 100644 index 00000000..a410c02e --- /dev/null +++ b/frontend/src/lib/services/i18n.ts @@ -0,0 +1,82 @@ +/** + * Inspired by https://github.com/PhraseApp-Blog/svelte-i18n-2020-06/tree/master + */ + +import { get, derived, writable } from 'svelte/store'; +import { + _, + date, + init, + locale, + number, + dictionary, + addMessages, + getLocaleFromNavigator +} from 'svelte-i18n'; + +export const locales = { + en: 'En', + fr: 'Fr' +}; + +const fallbackLocale = 'fr'; + +const MESSAGE_FILE_URL_TEMPLATE = '/lang/{locale}.json'; + +export let _activeLocale: string; + +const isDownloading = writable(false); + +function setupI18n(options = { withLocale: fallbackLocale }) { + const locale_ = supported(options.withLocale || language(getLocaleFromNavigator() ?? '')); + + init({ initialLocale: locale_, fallbackLocale: fallbackLocale }); + + if (!hasLoadedLocale(locale_)) { + isDownloading.set(true); + + const messagesFileUrl = MESSAGE_FILE_URL_TEMPLATE.replace('{locale}', locale_); + + return loadJson(messagesFileUrl).then((messages) => { + _activeLocale = locale_; + + addMessages(locale_, messages); + + locale.set(locale_); + + isDownloading.set(false); + }); + } +} + +const isLocaleLoaded = derived( + [isDownloading, dictionary], + ([$isDownloading, $dictionary]) => + !$isDownloading && + $dictionary[_activeLocale] && + Object.keys($dictionary[_activeLocale]).length > 0 +); + +const dir = derived(locale, ($locale) => ($locale === 'ar' ? 'rtl' : 'ltr')); + +function loadJson(url: string) { + return fetch(url).then((response) => response.json()); +} + +function hasLoadedLocale(locale: string) { + return get(dictionary)[locale]; +} + +function language(locale: string): string { + return locale.replace('_', '-').split('-')[0]; +} + +function supported(locale: string): string { + if (Object.keys(locales).includes(locale)) { + return locale; + } else { + return fallbackLocale; + } +} + +export { _, setupI18n, isLocaleLoaded, locale, dir, date, number }; diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index 83ce6cef..880468e1 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -1,8 +1,21 @@ <script> import { SvelteToast } from '@zerodevx/svelte-toast'; import '../app.css'; + import { setupI18n, isLocaleLoaded, dir, _ } from '$lib/services/i18n'; + + setupI18n(); + + $: if (document.dir !== $dir) { + document.dir = $dir; + } + + $: if ($isLocaleLoaded) { + document.title = $_('header.appName'); + } </script> -<slot /> +{#if $isLocaleLoaded} + <slot /> +{/if} <SvelteToast /> diff --git a/frontend/static/lang/en.json b/frontend/static/lang/en.json new file mode 100644 index 00000000..0cc241b2 --- /dev/null +++ b/frontend/static/lang/en.json @@ -0,0 +1,6 @@ +{ + "header": { + "appName": "LanguageLab", + "connectedAs": "Connected as" + } +} diff --git a/frontend/static/lang/fr.json b/frontend/static/lang/fr.json new file mode 100644 index 00000000..76b694ac --- /dev/null +++ b/frontend/static/lang/fr.json @@ -0,0 +1,6 @@ +{ + "header": { + "appName": "LanguageLab", + "connectedAs": "Connecté en tant que" + } +} -- GitLab