i18n support

This commit is contained in:
Gauthier Roebroeck 2025-05-28 15:41:01 +08:00
parent 7e16a710d0
commit 382a851b7d
15 changed files with 730 additions and 10 deletions

View file

@ -6,6 +6,7 @@
import pluginVue from 'eslint-plugin-vue'
import {defineConfigWithVueTs, vueTsConfigs} from '@vue/eslint-config-typescript'
import formatjs from 'eslint-plugin-formatjs'
export default defineConfigWithVueTs(
{
@ -32,5 +33,20 @@ export default defineConfigWithVueTs(
],
'vue/multi-word-component-names': 'off',
}
}
},
{
plugins: {
formatjs,
},
rules: {
'formatjs/no-offset': 'error',
'formatjs/enforce-id': [
'error',
{
idInterpolationPattern: '[sha512:contenthash:base64:6]',
},
],
},
},
)

View file

@ -16,10 +16,12 @@
"openapi-fetch": "^0.14.0",
"pinia-plugin-persistedstate": "^4.3.0",
"vue": "^3.5.14",
"vue-intl": "^6.5.25",
"vuetify": "^3.8.5"
},
"devDependencies": {
"@eslint/js": "^9.27.0",
"@formatjs/cli": "^6.7.1",
"@mdi/font": "7.4.47",
"@tsconfig/node22": "^22.0.2",
"@types/node": "^22.15.21",
@ -27,6 +29,7 @@
"@vue/eslint-config-typescript": "^14.1.3",
"@vue/tsconfig": "^0.7.0",
"eslint": "^9.27.0",
"eslint-plugin-formatjs": "^5.3.1",
"eslint-plugin-vue": "^10.1.0",
"npm-run-all2": "^8.0.3",
"openapi-typescript": "^7.8.0",
@ -689,6 +692,151 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@formatjs/cli": {
"version": "6.7.1",
"resolved": "https://registry.npmjs.org/@formatjs/cli/-/cli-6.7.1.tgz",
"integrity": "sha512-ULiXbLkbuTyd8f0qaByu1Nuc+jbAOLH1qRAtHZ7waIABQGPBB93OQ2FFtQPgoYoupKOKyNr+PZXR6pOT45E4EQ==",
"dev": true,
"license": "MIT",
"bin": {
"formatjs": "bin/formatjs"
},
"engines": {
"node": ">= 16"
},
"peerDependencies": {
"@glimmer/env": "^0.1.7",
"@glimmer/reference": "^0.94.0",
"@glimmer/syntax": "^0.94.9",
"@glimmer/validator": "^0.94.0",
"@vue/compiler-core": "^3.5.12",
"content-tag": "^3.0.0",
"ember-template-recast": "^6.1.5",
"vue": "^3.5.12"
},
"peerDependenciesMeta": {
"@glimmer/env": {
"optional": true
},
"@glimmer/reference": {
"optional": true
},
"@glimmer/syntax": {
"optional": true
},
"@glimmer/validator": {
"optional": true
},
"@vue/compiler-core": {
"optional": true
},
"content-tag": {
"optional": true
},
"ember-template-recast": {
"optional": true
},
"vue": {
"optional": true
}
}
},
"node_modules/@formatjs/ecma402-abstract": {
"version": "2.3.4",
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.4.tgz",
"integrity": "sha512-qrycXDeaORzIqNhBOx0btnhpD1c+/qFIHAN9znofuMJX6QBwtbrmlpWfD4oiUUD2vJUOIYFA/gYtg2KAMGG7sA==",
"license": "MIT",
"dependencies": {
"@formatjs/fast-memoize": "2.2.7",
"@formatjs/intl-localematcher": "0.6.1",
"decimal.js": "^10.4.3",
"tslib": "^2.8.0"
}
},
"node_modules/@formatjs/fast-memoize": {
"version": "2.2.7",
"resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.7.tgz",
"integrity": "sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==",
"license": "MIT",
"dependencies": {
"tslib": "^2.8.0"
}
},
"node_modules/@formatjs/icu-messageformat-parser": {
"version": "2.11.2",
"resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.2.tgz",
"integrity": "sha512-AfiMi5NOSo2TQImsYAg8UYddsNJ/vUEv/HaNqiFjnI3ZFfWihUtD5QtuX6kHl8+H+d3qvnE/3HZrfzgdWpsLNA==",
"license": "MIT",
"dependencies": {
"@formatjs/ecma402-abstract": "2.3.4",
"@formatjs/icu-skeleton-parser": "1.8.14",
"tslib": "^2.8.0"
}
},
"node_modules/@formatjs/icu-skeleton-parser": {
"version": "1.8.14",
"resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.14.tgz",
"integrity": "sha512-i4q4V4qslThK4Ig8SxyD76cp3+QJ3sAqr7f6q9VVfeGtxG9OhiAk3y9XF6Q41OymsKzsGQ6OQQoJNY4/lI8TcQ==",
"license": "MIT",
"dependencies": {
"@formatjs/ecma402-abstract": "2.3.4",
"tslib": "^2.8.0"
}
},
"node_modules/@formatjs/intl": {
"version": "3.1.6",
"resolved": "https://registry.npmjs.org/@formatjs/intl/-/intl-3.1.6.tgz",
"integrity": "sha512-tDkXnA4qpIFcDWac8CyVJq6oW8DR7W44QDUBsfXWIIJD/FYYen0QoH46W7XsVMFfPOVKkvbufjboZrrWbEfmww==",
"license": "MIT",
"dependencies": {
"@formatjs/ecma402-abstract": "2.3.4",
"@formatjs/fast-memoize": "2.2.7",
"@formatjs/icu-messageformat-parser": "2.11.2",
"intl-messageformat": "10.7.16",
"tslib": "^2.8.0"
},
"peerDependencies": {
"typescript": "^5.6.0"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/@formatjs/intl-localematcher": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.6.1.tgz",
"integrity": "sha512-ePEgLgVCqi2BBFnTMWPfIghu6FkbZnnBVhO2sSxvLfrdFw7wCHAHiDoM2h4NRgjbaY7+B7HgOLZGkK187pZTZg==",
"license": "MIT",
"dependencies": {
"tslib": "^2.8.0"
}
},
"node_modules/@formatjs/ts-transformer": {
"version": "3.13.34",
"resolved": "https://registry.npmjs.org/@formatjs/ts-transformer/-/ts-transformer-3.13.34.tgz",
"integrity": "sha512-N1n7dA+6dfHn/LDQXrUPOC90Z+7dsDB5cQIJeIysVwGhk8PRyYo2eaotDszZMjQp8+hvs98kJyekn1X7mK1yHQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@formatjs/icu-messageformat-parser": "2.11.2",
"@types/json-stable-stringify": "^1.1.0",
"@types/node": "^22.0.0",
"chalk": "^4.1.2",
"json-stable-stringify": "^1.1.1",
"tslib": "^2.8.0",
"typescript": "^5.6.0"
},
"peerDependencies": {
"ts-jest": "^29"
},
"peerDependenciesMeta": {
"ts-jest": {
"optional": true
}
}
},
"node_modules/@humanfs/core": {
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
@ -1568,6 +1716,17 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/eslint": {
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz",
"integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "*",
"@types/json-schema": "*"
}
},
"node_modules/@types/estree": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
@ -1581,6 +1740,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/json-stable-stringify": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@types/json-stable-stringify/-/json-stable-stringify-1.1.0.tgz",
"integrity": "sha512-ESTsHWB72QQq+pjUFIbEz9uSCZppD31YrVkbt2rnUciTYEvcwN6uZIhX5JZeBHqRlFJ41x/7MewCs7E2Qux6Cg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/node": {
"version": "22.15.21",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz",
@ -1591,6 +1757,13 @@
"undici-types": "~6.21.0"
}
},
"node_modules/@types/picomatch": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/picomatch/-/picomatch-3.0.2.tgz",
"integrity": "sha512-n0i8TD3UDB7paoMMxA3Y65vUncFJXjcUf7lQY7YyKGl6031FNjfsLs6pdLFCy2GNFxItPJG8GvvpbZc2skH7WA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/web-bluetooth": {
"version": "0.0.21",
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
@ -2406,6 +2579,56 @@
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/call-bind": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
"integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.0",
"es-define-property": "^1.0.0",
"get-intrinsic": "^1.2.4",
"set-function-length": "^1.2.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/call-bound": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"get-intrinsic": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@ -2626,6 +2849,12 @@
}
}
},
"node_modules/decimal.js": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.5.0.tgz",
"integrity": "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==",
"license": "MIT"
},
"node_modules/deep-is": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@ -2638,6 +2867,24 @@
"resolved": "https://registry.npmjs.org/deep-pick-omit/-/deep-pick-omit-1.2.1.tgz",
"integrity": "sha512-2J6Kc/m3irCeqVG42T+SaUMesaK7oGWaedGnQQK/+O0gYc+2SP5bKh/KKTE7d7SJ+GCA9UUE1GRzh6oDe0EnGw=="
},
"node_modules/define-data-property": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-define-property": "^1.0.0",
"es-errors": "^1.3.0",
"gopd": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/defu": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
@ -2672,6 +2919,31 @@
"url": "https://dotenvx.com"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/emoji-regex-xs": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-2.0.1.tgz",
"integrity": "sha512-1QFuh8l7LqUcKe24LsPUNzjrzJQ7pgRwp1QMcZ5MX6mFplk2zQ08NVCM84++1cveaUUYtcCYHmeFEuNg16sU4g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
@ -2689,6 +2961,39 @@
"resolved": "https://registry.npmjs.org/errx/-/errx-0.1.0.tgz",
"integrity": "sha512-fZmsRiDNv07K6s2KkKFTiD2aIvECa7++PKyD5NC32tpRw46qZA3sOz+aM+/V9V0GDHxVTKLziveV4JhzBHDp9Q=="
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/esbuild": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz",
@ -2804,6 +3109,27 @@
}
}
},
"node_modules/eslint-plugin-formatjs": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-formatjs/-/eslint-plugin-formatjs-5.3.1.tgz",
"integrity": "sha512-RYXxDyCIcjzXXiclHhtXTJhRnR+HGyk4fyAke4j7Jtw8kCdVG/0rFeLqsIoJojxEB91+tkt0enX2LmDaXNhfdg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@formatjs/icu-messageformat-parser": "2.11.2",
"@formatjs/ts-transformer": "3.13.34",
"@types/eslint": "^9.6.1",
"@types/picomatch": "^3",
"@typescript-eslint/utils": "^8.27.0",
"magic-string": "^0.30.0",
"picomatch": "2 || 3 || 4",
"tslib": "^2.8.0",
"unicode-emoji-utils": "^1.2.0"
},
"peerDependencies": {
"eslint": "^9.23.0"
}
},
"node_modules/eslint-plugin-vue": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-10.1.0.tgz",
@ -3121,6 +3447,55 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"dev": true,
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/giget": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz",
@ -3163,6 +3538,19 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/graphemer": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
@ -3180,6 +3568,45 @@
"node": ">=8"
}
},
"node_modules/has-property-descriptors": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-define-property": "^1.0.0"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/he": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
@ -3267,6 +3694,18 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/intl-messageformat": {
"version": "10.7.16",
"resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.16.tgz",
"integrity": "sha512-UmdmHUmp5CIKKjSoE10la5yfU+AYJAaiYLsodbjL4lji83JNvgOQUjGaGhGrpFCb0Uh7sl7qfP1IyILa8Z40ug==",
"license": "BSD-3-Clause",
"dependencies": {
"@formatjs/ecma402-abstract": "2.3.4",
"@formatjs/fast-memoize": "2.2.7",
"@formatjs/icu-messageformat-parser": "2.11.2",
"tslib": "^2.8.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",
@ -3325,6 +3764,13 @@
"url": "https://github.com/sponsors/mesqueeb"
}
},
"node_modules/isarray": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
"integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
"dev": true,
"license": "MIT"
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@ -3394,6 +3840,26 @@
"dev": true,
"license": "MIT"
},
"node_modules/json-stable-stringify": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.3.0.tgz",
"integrity": "sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind": "^1.0.8",
"call-bound": "^1.0.4",
"isarray": "^2.0.5",
"jsonify": "^0.0.1",
"object-keys": "^1.1.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/json-stable-stringify-without-jsonify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
@ -3414,6 +3880,16 @@
"node": ">=6"
}
},
"node_modules/jsonify": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz",
"integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==",
"dev": true,
"license": "Public Domain",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@ -3552,6 +4028,16 @@
"node": ">= 18"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/memorystream": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz",
@ -3818,6 +4304,16 @@
"pathe": "^2.0.3"
}
},
"node_modules/object-keys": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/ohash": {
"version": "2.0.11",
"resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz",
@ -4786,6 +5282,24 @@
"node": ">=10"
}
},
"node_modules/set-function-length": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
"integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
"dev": true,
"license": "MIT",
"dependencies": {
"define-data-property": "^1.1.4",
"es-errors": "^1.3.0",
"function-bind": "^1.1.2",
"get-intrinsic": "^1.2.4",
"gopd": "^1.0.1",
"has-property-descriptors": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@ -5001,7 +5515,6 @@
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"devOptional": true,
"license": "0BSD"
},
"node_modules/type-check": {
@ -5099,6 +5612,16 @@
"devOptional": true,
"license": "MIT"
},
"node_modules/unicode-emoji-utils": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/unicode-emoji-utils/-/unicode-emoji-utils-1.3.1.tgz",
"integrity": "sha512-6PiQxmnlsOsqzZCZz0sykSyMy/r1HiJiOWWXV98+BDva583DU4CtBeyDNsi4wMYUIbjUtMs4RgAuyft0EKLoVw==",
"dev": true,
"license": "MIT",
"dependencies": {
"emoji-regex-xs": "^2.0.0"
}
},
"node_modules/unimport": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/unimport/-/unimport-4.2.0.tgz",
@ -5674,6 +6197,21 @@
"url": "https://opencollective.com/eslint"
}
},
"node_modules/vue-intl": {
"version": "6.5.25",
"resolved": "https://registry.npmjs.org/vue-intl/-/vue-intl-6.5.25.tgz",
"integrity": "sha512-r7mcl9O/sNF+k+pqiHW+Zu2dbxF7XkME4+cQz5JiZTGJAPRMWX1l5WIELE63/CAxV6EU3CfcryE+r98rYidJlA==",
"license": "ISC",
"dependencies": {
"@babel/types": "^7.26.10",
"@formatjs/icu-messageformat-parser": "2.11.2",
"@formatjs/intl": "3.1.6",
"tslib": "^2.8.0"
},
"peerDependencies": {
"vue": "^3.5.12"
}
},
"node_modules/vue-router": {
"version": "4.5.1",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.1.tgz",

View file

@ -10,7 +10,8 @@
"build-only": "vite build",
"type-check": "vue-tsc --build --force",
"lint": "eslint . --fix",
"openapi-generate": "npx tsx ./openapi-generator.mts"
"openapi-generate": "npx tsx ./openapi-generator.mts",
"i18n-extract": "formatjs extract \"src/**/*.{ts,tsx,vue}\" --ignore=\"**/*.d.ts\" --out-file src/i18n/en.json"
},
"dependencies": {
"@pinia/colada": "^0.16.1",
@ -21,10 +22,12 @@
"openapi-fetch": "^0.14.0",
"pinia-plugin-persistedstate": "^4.3.0",
"vue": "^3.5.14",
"vue-intl": "^6.5.25",
"vuetify": "^3.8.5"
},
"devDependencies": {
"@eslint/js": "^9.27.0",
"@formatjs/cli": "^6.7.1",
"@mdi/font": "7.4.47",
"@tsconfig/node22": "^22.0.2",
"@types/node": "^22.15.21",
@ -32,6 +35,7 @@
"@vue/eslint-config-typescript": "^14.1.3",
"@vue/tsconfig": "^0.7.0",
"eslint": "^9.27.0",
"eslint-plugin-formatjs": "^5.3.1",
"eslint-plugin-vue": "^10.1.0",
"npm-run-all2": "^8.0.3",
"openapi-typescript": "^7.8.0",

View file

@ -25,11 +25,11 @@ declare module 'vue' {
DialogConfirmEdit: typeof import('./components/dialogs/DialogConfirmEdit.vue')['default']
FormUserChangePassword: typeof import('./components/forms/user/FormUserChangePassword.vue')['default']
FormUserEdit: typeof import('./components/forms/user/FormUserEdit.vue')['default']
FormUserRoles: typeof import('./components/forms/user/FormUserRoles.vue')['default']
HelloWorld: typeof import('./components/HelloWorld.vue')['default']
LocaleSelector: typeof import('./components/LocaleSelector.vue')['default']
LoginForm: typeof import('./components/LoginForm.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
ThemeSelector: typeof import('./components/app/bar/ThemeSelector.vue')['default']
ThemeSelector: typeof import('./components/ThemeSelector.vue')['default']
}
}

View file

@ -0,0 +1,53 @@
<template>
<v-menu>
<template #activator="{ props }">
<v-btn
v-bind="props"
icon="mdi-translate"
/>
</template>
<v-list
:selected="[currentLocale]"
color="primary"
>
<v-list-subheader
title="Translations"
class="text-high-emphasis text-uppercase font-weight-black"
/>
<v-list-item
v-for="locale in locales"
:key="locale.value"
:title="locale.title"
:value="locale.value"
@click="setLocale(locale.value)"
/>
<v-list-item
href="https://hosted.weblate.org/projects/komga/webui/"
target="_blank"
>
<v-list-item-title class="font-weight-bold">
Help translate
</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</template>
<script setup lang="ts">
import {availableLocales, currentLocale, setLocale} from '@/utils/locale-helper.ts'
const locales = Object.entries(availableLocales).map(([k, v]) => ({
title: v,
value: k,
}))
</script>
<script lang="ts">
</script>
<style scoped>
</style>

View file

@ -7,12 +7,16 @@
/>
</template>
<v-list>
<v-list
:selected="[appStore.theme]"
color="primary"
>
<v-list-item
v-for="theme in themes"
:key="theme.value"
:prepend-icon="theme.icon"
:title="theme.title"
:value="theme.value"
@click="appStore.theme = theme.value"
/>
</v-list>

View file

@ -11,6 +11,7 @@
</RouterLink>
Komga
</v-app-bar-title>
<LocaleSelector />
<ThemeSelector />
</v-app-bar>
</template>

View file

@ -28,8 +28,17 @@
/>
<v-list-item
to="/server/settings"
title="Settings"
/>
>
<v-list-item-title>
{{
$formatMessage({
description: 'Drawer menu for Server Settings',
defaultMessage: 'Settings',
id: '9yKJ2S'
})
}}
</v-list-item-title>
</v-list-item>
<v-list-item
to="/server/ui"
title="User Interface"

View file

@ -12,7 +12,7 @@
<v-text-field
v-model="confirmPassword"
class="mt-2"
:rules="[rules.sameAs(newPassword)]"
:rules="[rules.sameAs(newPassword, 'Passwords must be identical')]"
label="Confirm password"
autocomplete="off"
:type="showPassword ? 'text' : 'password'"

10
next-ui/src/i18n/en.json Normal file
View file

@ -0,0 +1,10 @@
{
"9yKJ2S": {
"defaultMessage": "Settings",
"description": "Drawer menu for Server Settings"
},
"localename": {
"defaultMessage": "English",
"description": "The name of the locale, shown in the language selection menu. Must be translated to the language's name"
}
}

10
next-ui/src/i18n/fr.json Normal file
View file

@ -0,0 +1,10 @@
{
"9yKJ2S": {
"defaultMessage": "Réglages",
"description": "Drawer menu for Server Settings"
},
"localename": {
"defaultMessage": "Français",
"description": "The name of the locale, shown in the language selection menu. Must be translated to the language's name"
}
}

View file

@ -10,6 +10,7 @@ import pinia from '../stores'
import router from '../router'
import {PiniaColada} from '@pinia/colada'
import { PiniaColadaAutoRefetch } from '@pinia/colada-plugin-auto-refetch'
import {vueIntl} from '@/plugins/vue-intl.ts'
// Types
import type {App} from 'vue'
@ -21,6 +22,7 @@ export function registerPlugins(app: App) {
app
.use(vuetify)
.use(vuetifyRulesPlugin)
.use(vueIntl)
// .use(DataLoaderPlugin, {router})
.use(router)
.use(pinia)

View file

@ -0,0 +1,10 @@
import {createIntl} from 'vue-intl'
import {currentLocale, defaultLocale, loadLocale} from '@/utils/locale-helper.ts'
const messages = await loadLocale(currentLocale)
export const vueIntl = createIntl({
locale: currentLocale,
defaultLocale: defaultLocale,
messages
})

View file

@ -48,7 +48,7 @@ export const vuetify = createVuetify({
export const vuetifyRulesPlugin = createRulesPlugin({
aliases: {
sameAs: (other?: string, err?: string) => {
return (v: unknown) => other === v || err || 'Field must be same'
return (v: unknown) => other === v || err || 'Field must have the same value'
},
},
}, vuetify.locale)

View file

@ -0,0 +1,63 @@
import {defineMessage} from 'vue-intl'
export const defaultLocale = 'en'
const localeName = defineMessage({
description: 'The name of the locale, shown in the language selection menu. Must be translated to the language\'s name',
defaultMessage: 'English',
id: 'localename'
})
/**
* Loads messages from a translation file by its locale code.
* If the translation file does not exist, loads the `defaultLocale` instead.
* @param locale the locale code, e.g. 'fr'
*/
export async function loadLocale(locale: string) {
const localeToLoad = locale in availableLocales ? locale : defaultLocale
const { default: messages } = await import(`@/i18n/${localeToLoad}.json`);
return messages;
}
async function loadAvailableLocales(): Promise<Record<string, string>> {
const localeFiles = import.meta.glob('@/i18n/*.json')
const locales: Record<string, string> = {}
for (const path in localeFiles) {
const matched = path.match(/([A-Za-z0-9-_]+)\./i)
if (matched && matched.length > 1) {
const locale = matched[1]
const messages = await localeFiles[path]!() as Record<string, {defaultMessage: string}>
locales[locale!] = messages[localeName.id]!.defaultMessage
}
}
return locales
}
/**
* Available locales loaded from translation files.
* Key is the locale code (e.g. 'fr')
* Value is the locale name in its own locale (e.g. 'Français')
*/
export const availableLocales = await loadAvailableLocales()
/**
* Gets the saved locale from localStorage.
* If the locale is not valid, defaults to 'en'.
*/
function getLocale(): string {
const storageLocale = localStorage.getItem('userLocale') ?? defaultLocale
return storageLocale in availableLocales ? storageLocale : defaultLocale
}
export const currentLocale = getLocale()
/**
* Save the locale to localStorage and reloads the window if it has changed.
* @param locale the new locale
*/
export function setLocale(locale: string) {
if(locale !== currentLocale) {
localStorage.setItem('userLocale', locale)
window.location.reload()
}
}