diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..1fd3f983e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,10 @@ +root = true + +[*] +charset=utf-8 +end_of_line = lf + +[*.{ts,tsx}] +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/crates/spa/src/vite.rs b/crates/spa/src/vite.rs index 8a56777c7..092357053 100644 --- a/crates/spa/src/vite.rs +++ b/crates/spa/src/vite.rs @@ -54,6 +54,7 @@ enum FileType { Stylesheet, Woff, Woff2, + Json, } impl FileType { @@ -63,6 +64,7 @@ impl FileType { Some("js") => Some(Self::Script), Some("woff") => Some(Self::Woff), Some("woff2") => Some(Self::Woff2), + Some("json") => Some(Self::Json), _ => None, } } @@ -122,6 +124,9 @@ impl<'a> Asset<'a> { FileType::Woff | FileType::Woff2 => { format!(r#""#,) } + FileType::Json => { + format!(r#""#,) + } } } @@ -140,9 +145,33 @@ impl<'a> Asset<'a> { FileType::Script => Some(format!( r#""# )), - FileType::Woff | FileType::Woff2 => None, + FileType::Woff | FileType::Woff2 | FileType::Json => None, } } + + /// Returns `true` if the asset type is a script + #[must_use] + pub fn is_script(&self) -> bool { + self.file_type == FileType::Script + } + + /// Returns `true` if the asset type is a stylesheet + #[must_use] + pub fn is_stylesheet(&self) -> bool { + self.file_type == FileType::Stylesheet + } + + /// Returns `true` if the asset type is JSON + #[must_use] + pub fn is_json(&self) -> bool { + self.file_type == FileType::Json + } + + /// Returns `true` if the asset type is a font + #[must_use] + pub fn is_font(&self) -> bool { + self.file_type == FileType::Woff || self.file_type == FileType::Woff2 + } } impl Manifest { diff --git a/crates/templates/src/functions.rs b/crates/templates/src/functions.rs index 6a5285c7f..cb81f75dd 100644 --- a/crates/templates/src/functions.rs +++ b/crates/templates/src/functions.rs @@ -362,15 +362,17 @@ impl Object for IncludeAsset { BTreeSet::new() }; - let tags: Vec = preloads + let preloads = preloads .iter() - .map(|asset| asset.preload_tag(self.url_builder.assets_base().into())) - .chain( - assets - .iter() - .filter_map(|asset| asset.include_tag(self.url_builder.assets_base().into())), - ) - .collect(); + // Only preload scripts and stylesheets for now + .filter(|asset| asset.is_script() || asset.is_stylesheet()) + .map(|asset| asset.preload_tag(self.url_builder.assets_base().into())); + + let assets = assets + .iter() + .filter_map(|asset| asset.include_tag(self.url_builder.assets_base().into())); + + let tags: Vec = preloads.chain(assets).collect(); Ok(Value::from_safe_string(tags.join("\n"))) } diff --git a/frontend/i18next-parser.config.ts b/frontend/i18next-parser.config.ts new file mode 100644 index 000000000..6c2bf253f --- /dev/null +++ b/frontend/i18next-parser.config.ts @@ -0,0 +1,36 @@ +// Copyright 2023 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import type { UserConfig } from "i18next-parser"; + +const config: UserConfig = { + keySeparator: ".", + pluralSeparator: ":", + defaultNamespace: "frontend", + lexers: { + ts: [ + { + lexer: "JavascriptLexer", + functions: ["t", "translatedError"], + namespaceFunctions: ["useTranslation", "withTranslation"], + }, + ], + }, + locales: ["en"], + output: "locales/$LOCALE.json", + input: ["src/**/*.{ts,tsx}"], + sort: true, +}; + +export default config; diff --git a/frontend/locales/en.json b/frontend/locales/en.json new file mode 100644 index 000000000..80c1e0d1b --- /dev/null +++ b/frontend/locales/en.json @@ -0,0 +1,140 @@ +{ + "action": { + "cancel": "Cancel", + "continue": "Continue", + "save": "Save" + }, + "common": { + "add": "Add", + "error": "Error", + "loading": "Loading…", + "next": "Next", + "previous": "Previous" + }, + "frontend": { + "add_email_form": { + "email_denied_alert": { + "text": "The entered email is not allowed by the server policy.", + "title": "Email denied by policy" + }, + "email_exists_alert": { + "text": "The entered email is already added to this account", + "title": "Email already exists" + }, + "email_field_label": "Add email", + "email_invalid_alert": { + "text": "The entered email is invalid", + "title": "Invalid email" + } + }, + "app_sessions_list": { + "error": "Failed to load app sessions", + "heading": "Apps" + }, + "browser_session_details": { + "current_badge": "Current", + "session_details_title": "Session" + }, + "browser_sessions_overview": { + "body:one": "{{count}} active session", + "body:other": "{{count}} active sessions", + "heading": "Browsers", + "view_all_button": "View all" + }, + "compat_session_detail": { + "client_details_title": "Client", + "name": "Name", + "session_details_title": "Session" + }, + "device_type_icon_label": { + "desktop": "Desktop", + "mobile": "Mobile", + "unknown": "Unknown device type", + "web": "Web" + }, + "end_session_button": { + "confirmation_modal_title": "Are you sure you want to end this session?", + "text": "End session" + }, + "error_boundary_title": "Something went wrong", + "last_active": { + "active_date": "Active {{relativeDate}}", + "active_now": "Active now", + "inactive_90_days": "Inactive for 90+ days" + }, + "nav": { + "profile": "Profile", + "sessions": "Sessions" + }, + "not_found_alert_title": "Not found.", + "not_logged_in_alert": "You're not logged in.", + "oauth2_client_detail": { + "details_title": "Client", + "id": "Client ID", + "name": "Name", + "policy": "Policy", + "terms": "Terms of service" + }, + "oauth2_session_detail": { + "client_details_name": "Name", + "client_title": "Client", + "session_details_title": "Session" + }, + "pagination_controls": { + "total": "Total: {{totalCount}}" + }, + "selectable_session": { + "label": "Select session" + }, + "session": { + "current_badge": "Current", + "finished_date": "Finished <1/>", + "signed_in_date": "Signed in <1/>" + }, + "session_detail": { + "alert": { + "button": "Go back", + "text": "This session does not exist, or is no longer active.", + "title": "Cannot find session: {{deviceId}}" + } + }, + "unknown_route": "Unknown route {{route}}", + "unverified_email_alert": { + "button": "Review and verify", + "text:one": "You have {{count}} unverified email address.", + "text:other": "You have {{count}} unverified email addresses.", + "title": "Unverified email" + }, + "user_email": { + "delete_button_confirmation_modal": { + "body": "Are you sure you want to remove this email?" + }, + "delete_button_title": "Remove email address", + "email": "Email", + "make_primary_button": "Make primary", + "primary_email": "Primary email", + "retry_button": "Retry verification", + "unverified": "Unverified" + }, + "user_email_list": { + "heading": "Emails", + "no_primary_email_alert": "No primary email address" + }, + "user_greeting": { + "error": "Failed to load user" + }, + "user_name": { + "display_name_field_label": "Display Name" + }, + "user_sessions_overview": { + "heading": "Where you're signed in" + }, + "verify_email": { + "code_field_label": "6-digit code", + "enter_code_prompt": "Enter the 6-digit code sent to <1>{{email}", + "heading": "Verify your email", + "invalid_code_alert": "Invalid code", + "unknown_email": "Unknown email" + } + } +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a3a0ed568..5aa9c29c3 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -22,12 +22,16 @@ "classnames": "^2.3.2", "date-fns": "^2.30.0", "graphql": "^16.8.1", + "i18next": "^23.5.1", + "i18next-browser-languagedetector": "^7.1.0", + "i18next-http-backend": "^2.2.2", "jotai": "^2.4.3", "jotai-devtools": "^0.6.3", "jotai-location": "^0.5.1", "jotai-urql": "^0.7.1", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-i18next": "^13.3.0", "ua-parser-js": "^1.0.36" }, "devDependencies": { @@ -54,6 +58,7 @@ "eslint-plugin-matrix-org": "^1.2.1", "eslint-plugin-prettier": "^5.0.0", "happy-dom": "^12.9.1", + "i18next-parser": "^8.9.0", "postcss": "^8.4.31", "prettier": "3.0.3", "react-test-renderer": "^18.2.0", @@ -4842,6 +4847,18 @@ "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, + "node_modules/@gulpjs/to-absolute-glob": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@gulpjs/to-absolute-glob/-/to-absolute-glob-4.0.0.tgz", + "integrity": "sha512-kjotm7XJrJ6v+7knhPaRgaT6q8F8K2jiafwYdNHLzmV0uGLuZY43FK6smNSHUPrhq5kX2slCUy+RGG/xGqmIKA==", + "dev": true, + "dependencies": { + "is-negated-glob": "^1.0.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.11", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", @@ -8833,6 +8850,12 @@ "@types/node": "*" } }, + "node_modules/@types/symlink-or-copy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@types/symlink-or-copy/-/symlink-or-copy-1.2.0.tgz", + "integrity": "sha512-Lja2xYuuf2B3knEsga8ShbOdsfNOtzT73GyJmZyY7eGl2+ajOqrs8yM5ze0fsSoYwvA6bw7/Qr7OZ7PEEmYwWg==", + "dev": true + }, "node_modules/@types/ua-parser-js": { "version": "0.7.38", "resolved": "https://registry.npmjs.org/@types/ua-parser-js/-/ua-parser-js-0.7.38.tgz", @@ -10606,6 +10629,100 @@ "node": ">=8" } }, + "node_modules/broccoli-node-api": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/broccoli-node-api/-/broccoli-node-api-1.7.0.tgz", + "integrity": "sha512-QIqLSVJWJUVOhclmkmypJJH9u9s/aWH4+FH6Q6Ju5l+Io4dtwqdPUNmDfw40o6sxhbZHhqGujDJuHTML1wG8Yw==", + "dev": true + }, + "node_modules/broccoli-node-info": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/broccoli-node-info/-/broccoli-node-info-2.2.0.tgz", + "integrity": "sha512-VabSGRpKIzpmC+r+tJueCE5h8k6vON7EIMMWu6d/FyPdtijwLQ7QvzShEw+m3mHoDzUaj/kiZsDYrS8X2adsBg==", + "dev": true, + "engines": { + "node": "8.* || >= 10.*" + } + }, + "node_modules/broccoli-output-wrapper": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/broccoli-output-wrapper/-/broccoli-output-wrapper-3.2.5.tgz", + "integrity": "sha512-bQAtwjSrF4Nu0CK0JOy5OZqw9t5U0zzv2555EA/cF8/a8SLDTIetk9UgrtMVw7qKLKdSpOZ2liZNeZZDaKgayw==", + "dev": true, + "dependencies": { + "fs-extra": "^8.1.0", + "heimdalljs-logger": "^0.1.10", + "symlink-or-copy": "^1.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + } + }, + "node_modules/broccoli-output-wrapper/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/broccoli-output-wrapper/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/broccoli-output-wrapper/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/broccoli-plugin": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/broccoli-plugin/-/broccoli-plugin-4.0.7.tgz", + "integrity": "sha512-a4zUsWtA1uns1K7p9rExYVYG99rdKeGRymW0qOCNkvDPHQxVi3yVyJHhQbM3EZwdt2E0mnhr5e0c/bPpJ7p3Wg==", + "dev": true, + "dependencies": { + "broccoli-node-api": "^1.7.0", + "broccoli-output-wrapper": "^3.2.5", + "fs-merger": "^3.2.1", + "promise-map-series": "^0.3.0", + "quick-temp": "^0.1.8", + "rimraf": "^3.0.2", + "symlink-or-copy": "^1.3.1" + }, + "engines": { + "node": "10.* || >= 12.*" + } + }, + "node_modules/broccoli-plugin/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/browser-assert": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/browser-assert/-/browser-assert-1.2.1.tgz", @@ -10934,6 +11051,146 @@ "node": "*" } }, + "node_modules/cheerio": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", + "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", + "dev": true, + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "htmlparser2": "^8.0.1", + "parse5": "^7.0.0", + "parse5-htmlparser2-tree-adapter": "^7.0.0" + }, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cheerio-select/node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cheerio-select/node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/cheerio-select/node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/cheerio-select/node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dev": true, + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/cheerio/node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/cheerio/node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/cheerio/node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dev": true, + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -11179,6 +11436,12 @@ "node": ">=0.10.0" } }, + "node_modules/clone-stats": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", + "integrity": "sha512-au6ydSpg6nsrigcZ4m8Bc9hxjeW+GJ8xh5G3BJCMt4WXe1H10UNaVOamqQTmrx1kjVuxAHIQSNU6hY4Nsn9/ag==", + "dev": true + }, "node_modules/clsx": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz", @@ -11253,6 +11516,15 @@ "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", "dev": true }, + "node_modules/colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "dev": true, + "engines": { + "node": ">=0.1.90" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -11649,6 +11921,12 @@ "url": "https://opencollective.com/date-fns" } }, + "node_modules/de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true + }, "node_modules/debounce": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", @@ -12326,6 +12604,12 @@ "node": ">=10.13.0" } }, + "node_modules/ensure-posix-path": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ensure-posix-path/-/ensure-posix-path-1.1.1.tgz", + "integrity": "sha512-VWU0/zXzVbeJNXvME/5EmLuEj2TauvoaTz6aFYK1Z92JCBlDlZ3Gu0tuGR42kpW1754ywTs+QB0g5TP0oj9Zaw==", + "dev": true + }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -12350,6 +12634,12 @@ "node": ">=4" } }, + "node_modules/eol": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/eol/-/eol-0.9.1.tgz", + "integrity": "sha512-Ds/TEoZjwggRoz/Q2O7SE3i4Jm66mqTDfmdHdq/7DKVk3bro9Q8h6WdXKdPqFLMoqxrDK5SVRzHVPOS6uuGtrg==", + "dev": true + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -14138,6 +14428,12 @@ "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", "dev": true }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true + }, "node_modules/fast-glob": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", @@ -14619,6 +14915,51 @@ "node": ">=14.14" } }, + "node_modules/fs-merger": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/fs-merger/-/fs-merger-3.2.1.tgz", + "integrity": "sha512-AN6sX12liy0JE7C2evclwoo0aCG3PFulLjrTLsJpWh/2mM+DinhpSGqYLbHBBbIW1PLRNcFhJG8Axtz8mQW3ug==", + "dev": true, + "dependencies": { + "broccoli-node-api": "^1.7.0", + "broccoli-node-info": "^2.1.0", + "fs-extra": "^8.0.1", + "fs-tree-diff": "^2.0.1", + "walk-sync": "^2.2.0" + } + }, + "node_modules/fs-merger/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fs-merger/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/fs-merger/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/fs-minipass": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", @@ -14649,6 +14990,35 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, + "node_modules/fs-mkdirp-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-2.0.1.tgz", + "integrity": "sha512-UTOY+59K6IA94tec8Wjqm0FSh5OVudGNB0NL/P6fB3HiE3bYOY3VYBGijsnOHNkQSwC1FKkU77pmq7xp9CskLw==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.8", + "streamx": "^2.12.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/fs-tree-diff": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fs-tree-diff/-/fs-tree-diff-2.0.1.tgz", + "integrity": "sha512-x+CfAZ/lJHQqwlD64pYM5QxWjzWhSjroaVsr8PW831zOApL55qPibed0c+xebaLWVr2BnHFoHdrwOv8pzt8R5A==", + "dev": true, + "dependencies": { + "@types/symlink-or-copy": "^1.2.0", + "heimdalljs-logger": "^0.1.7", + "object-assign": "^4.1.0", + "path-posix": "^1.0.0", + "symlink-or-copy": "^1.1.8" + }, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -14897,6 +15267,25 @@ "glob": "^7.1.6" } }, + "node_modules/glob-stream": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-8.0.0.tgz", + "integrity": "sha512-CdIUuwOkYNv9ZadR3jJvap8CMooKziQZ/QCSPhEb7zqfsEI5YnPmvca7IvbaVE3z58ZdUYD2JsU6AUWjL8WZJA==", + "dev": true, + "dependencies": { + "@gulpjs/to-absolute-glob": "^4.0.0", + "anymatch": "^3.1.3", + "fastq": "^1.13.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "is-negated-glob": "^1.0.0", + "normalize-path": "^3.0.0", + "streamx": "^2.12.5" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/glob-to-regexp": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", @@ -15076,6 +15465,15 @@ "graphql": ">=0.11 <=16" } }, + "node_modules/gulp-sort": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/gulp-sort/-/gulp-sort-2.0.0.tgz", + "integrity": "sha512-MyTel3FXOdh1qhw1yKhpimQrAmur9q1X0ZigLmCOxouQD+BD3za9/89O+HfbgBQvvh4igEbp0/PUWO+VqGYG1g==", + "dev": true, + "dependencies": { + "through2": "^2.0.1" + } + }, "node_modules/gunzip-maybe": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/gunzip-maybe/-/gunzip-maybe-1.4.2.tgz", @@ -15225,6 +15623,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, "node_modules/header-case": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/header-case/-/header-case-2.0.4.tgz", @@ -15235,6 +15642,46 @@ "tslib": "^2.0.3" } }, + "node_modules/heimdalljs": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/heimdalljs/-/heimdalljs-0.2.6.tgz", + "integrity": "sha512-o9bd30+5vLBvBtzCPwwGqpry2+n0Hi6H1+qwt6y+0kwRHGGF8TFIhJPmnuM0xO97zaKrDZMwO/V56fAnn8m/tA==", + "dev": true, + "dependencies": { + "rsvp": "~3.2.1" + } + }, + "node_modules/heimdalljs-logger": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/heimdalljs-logger/-/heimdalljs-logger-0.1.10.tgz", + "integrity": "sha512-pO++cJbhIufVI/fmB/u2Yty3KJD0TqNPecehFae0/eps0hkZ3b4Zc/PezUMOpYuHFQbA7FxHZxa305EhmjLj4g==", + "dev": true, + "dependencies": { + "debug": "^2.2.0", + "heimdalljs": "^0.2.6" + } + }, + "node_modules/heimdalljs-logger/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/heimdalljs-logger/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/heimdalljs/node_modules/rsvp": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-3.2.1.tgz", + "integrity": "sha512-Rf4YVNYpKjZ6ASAmibcwTNciQ5Co5Ztq6iZPEykHpkoflnD/K5ryE/rHehFsTm4NJj8nKDhbi3eKBWGogmNnkg==", + "dev": true + }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", @@ -15255,6 +15702,14 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "dependencies": { + "void-elements": "3.1.0" + } + }, "node_modules/html-tags": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz", @@ -15267,6 +15722,68 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "node_modules/htmlparser2/node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/htmlparser2/node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/htmlparser2/node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dev": true, + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -15318,6 +15835,483 @@ "node": ">=10.17.0" } }, + "node_modules/i18next": { + "version": "23.5.1", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.5.1.tgz", + "integrity": "sha512-JelYzcaCoFDaa+Ysbfz2JsGAKkrHiMG6S61+HLBUEIPaF40WMwW9hCPymlQGrP+wWawKxKPuSuD71WZscCsWHg==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "dependencies": { + "@babel/runtime": "^7.22.5" + } + }, + "node_modules/i18next-browser-languagedetector": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-7.1.0.tgz", + "integrity": "sha512-cr2k7u1XJJ4HTOjM9GyOMtbOA47RtUoWRAtt52z43r3AoMs2StYKyjS3URPhzHaf+mn10hY9dZWamga5WPQjhA==", + "dependencies": { + "@babel/runtime": "^7.19.4" + } + }, + "node_modules/i18next-http-backend": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-2.2.2.tgz", + "integrity": "sha512-mJu4ZqzDtBiU3O4GV9AbK5ekEqoDMdMnCl3pkdXmb5b8yoIH//u8FsmIe6C5qXb3teZu+j6VMi20tjUgzeABiw==", + "dependencies": { + "cross-fetch": "3.1.6" + } + }, + "node_modules/i18next-http-backend/node_modules/cross-fetch": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.6.tgz", + "integrity": "sha512-riRvo06crlE8HiqOwIpQhxwdOk4fOeR7FVM/wXoxchFEqMNUjvbs3bfo4OTgMEMHzppd4DxFBDbyySj8Cv781g==", + "dependencies": { + "node-fetch": "^2.6.11" + } + }, + "node_modules/i18next-parser": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/i18next-parser/-/i18next-parser-8.9.0.tgz", + "integrity": "sha512-8B+XTeBscPeswJbpBlGYxzUNXybDRltwTupJD+lLTGzBwfSfviAsSl2MEFpHc/fVlwjjd4Muw19nMpK2O+CB8A==", + "dev": true, + "dependencies": { + "broccoli-plugin": "^4.0.7", + "cheerio": "^1.0.0-rc.2", + "colors": "1.4.0", + "commander": "~11.0.0", + "eol": "^0.9.1", + "esbuild": "^0.19.0", + "fs-extra": "^11.1.0", + "gulp-sort": "^2.0.0", + "i18next": "^23.5.1", + "js-yaml": "4.1.0", + "lilconfig": "^2.0.6", + "rsvp": "^4.8.2", + "sort-keys": "^5.0.0", + "typescript": "^5.0.4", + "vinyl": "~3.0.0", + "vinyl-fs": "^4.0.0", + "vue-template-compiler": "^2.6.11" + }, + "bin": { + "i18next": "bin/cli.js" + }, + "engines": { + "node": ">=16.0.0 || >=18.0.0 || >=20.0.0", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/i18next-parser/node_modules/@esbuild/android-arm": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.5.tgz", + "integrity": "sha512-bhvbzWFF3CwMs5tbjf3ObfGqbl/17ict2/uwOSfr3wmxDE6VdS2GqY/FuzIPe0q0bdhj65zQsvqfArI9MY6+AA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/i18next-parser/node_modules/@esbuild/android-arm64": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.5.tgz", + "integrity": "sha512-5d1OkoJxnYQfmC+Zd8NBFjkhyCNYwM4n9ODrycTFY6Jk1IGiZ+tjVJDDSwDt77nK+tfpGP4T50iMtVi4dEGzhQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/i18next-parser/node_modules/@esbuild/android-x64": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.5.tgz", + "integrity": "sha512-9t+28jHGL7uBdkBjL90QFxe7DVA+KGqWlHCF8ChTKyaKO//VLuoBricQCgwhOjA1/qOczsw843Fy4cbs4H3DVA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/i18next-parser/node_modules/@esbuild/darwin-arm64": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.5.tgz", + "integrity": "sha512-mvXGcKqqIqyKoxq26qEDPHJuBYUA5KizJncKOAf9eJQez+L9O+KfvNFu6nl7SCZ/gFb2QPaRqqmG0doSWlgkqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/i18next-parser/node_modules/@esbuild/darwin-x64": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.5.tgz", + "integrity": "sha512-Ly8cn6fGLNet19s0X4unjcniX24I0RqjPv+kurpXabZYSXGM4Pwpmf85WHJN3lAgB8GSth7s5A0r856S+4DyiA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/i18next-parser/node_modules/@esbuild/freebsd-arm64": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.5.tgz", + "integrity": "sha512-GGDNnPWTmWE+DMchq1W8Sd0mUkL+APvJg3b11klSGUDvRXh70JqLAO56tubmq1s2cgpVCSKYywEiKBfju8JztQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/i18next-parser/node_modules/@esbuild/freebsd-x64": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.5.tgz", + "integrity": "sha512-1CCwDHnSSoA0HNwdfoNY0jLfJpd7ygaLAp5EHFos3VWJCRX9DMwWODf96s9TSse39Br7oOTLryRVmBoFwXbuuQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/i18next-parser/node_modules/@esbuild/linux-arm": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.5.tgz", + "integrity": "sha512-lrWXLY/vJBzCPC51QN0HM71uWgIEpGSjSZZADQhq7DKhPcI6NH1IdzjfHkDQws2oNpJKpR13kv7/pFHBbDQDwQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/i18next-parser/node_modules/@esbuild/linux-arm64": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.5.tgz", + "integrity": "sha512-o3vYippBmSrjjQUCEEiTZ2l+4yC0pVJD/Dl57WfPwwlvFkrxoSO7rmBZFii6kQB3Wrn/6GwJUPLU5t52eq2meA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/i18next-parser/node_modules/@esbuild/linux-ia32": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.5.tgz", + "integrity": "sha512-MkjHXS03AXAkNp1KKkhSKPOCYztRtK+KXDNkBa6P78F8Bw0ynknCSClO/ztGszILZtyO/lVKpa7MolbBZ6oJtQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/i18next-parser/node_modules/@esbuild/linux-loong64": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.5.tgz", + "integrity": "sha512-42GwZMm5oYOD/JHqHska3Jg0r+XFb/fdZRX+WjADm3nLWLcIsN27YKtqxzQmGNJgu0AyXg4HtcSK9HuOk3v1Dw==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/i18next-parser/node_modules/@esbuild/linux-mips64el": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.5.tgz", + "integrity": "sha512-kcjndCSMitUuPJobWCnwQ9lLjiLZUR3QLQmlgaBfMX23UEa7ZOrtufnRds+6WZtIS9HdTXqND4yH8NLoVVIkcg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/i18next-parser/node_modules/@esbuild/linux-ppc64": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.5.tgz", + "integrity": "sha512-yJAxJfHVm0ZbsiljbtFFP1BQKLc8kUF6+17tjQ78QjqjAQDnhULWiTA6u0FCDmYT1oOKS9PzZ2z0aBI+Mcyj7Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/i18next-parser/node_modules/@esbuild/linux-riscv64": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.5.tgz", + "integrity": "sha512-5u8cIR/t3gaD6ad3wNt1MNRstAZO+aNyBxu2We8X31bA8XUNyamTVQwLDA1SLoPCUehNCymhBhK3Qim1433Zag==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/i18next-parser/node_modules/@esbuild/linux-s390x": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.5.tgz", + "integrity": "sha512-Z6JrMyEw/EmZBD/OFEFpb+gao9xJ59ATsoTNlj39jVBbXqoZm4Xntu6wVmGPB/OATi1uk/DB+yeDPv2E8PqZGw==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/i18next-parser/node_modules/@esbuild/linux-x64": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.5.tgz", + "integrity": "sha512-psagl+2RlK1z8zWZOmVdImisMtrUxvwereIdyJTmtmHahJTKb64pAcqoPlx6CewPdvGvUKe2Jw+0Z/0qhSbG1A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/i18next-parser/node_modules/@esbuild/netbsd-x64": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.5.tgz", + "integrity": "sha512-kL2l+xScnAy/E/3119OggX8SrWyBEcqAh8aOY1gr4gPvw76la2GlD4Ymf832UCVbmuWeTf2adkZDK+h0Z/fB4g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/i18next-parser/node_modules/@esbuild/openbsd-x64": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.5.tgz", + "integrity": "sha512-sPOfhtzFufQfTBgRnE1DIJjzsXukKSvZxloZbkJDG383q0awVAq600pc1nfqBcl0ice/WN9p4qLc39WhBShRTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/i18next-parser/node_modules/@esbuild/sunos-x64": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.5.tgz", + "integrity": "sha512-dGZkBXaafuKLpDSjKcB0ax0FL36YXCvJNnztjKV+6CO82tTYVDSH2lifitJ29jxRMoUhgkg9a+VA/B03WK5lcg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/i18next-parser/node_modules/@esbuild/win32-arm64": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.5.tgz", + "integrity": "sha512-dWVjD9y03ilhdRQ6Xig1NWNgfLtf2o/STKTS+eZuF90fI2BhbwD6WlaiCGKptlqXlURVB5AUOxUj09LuwKGDTg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/i18next-parser/node_modules/@esbuild/win32-ia32": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.5.tgz", + "integrity": "sha512-4liggWIA4oDgUxqpZwrDhmEfAH4d0iljanDOK7AnVU89T6CzHon/ony8C5LeOdfgx60x5cnQJFZwEydVlYx4iw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/i18next-parser/node_modules/@esbuild/win32-x64": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.5.tgz", + "integrity": "sha512-czTrygUsB/jlM8qEW5MD8bgYU2Xg14lo6kBDXW6HdxKjh8M5PzETGiSHaz9MtbXBYDloHNUAUW2tMiKW4KM9Mw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/i18next-parser/node_modules/commander": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.0.0.tgz", + "integrity": "sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ==", + "dev": true, + "engines": { + "node": ">=16" + } + }, + "node_modules/i18next-parser/node_modules/esbuild": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.5.tgz", + "integrity": "sha512-bUxalY7b1g8vNhQKdB24QDmHeY4V4tw/s6Ak5z+jJX9laP5MoQseTOMemAr0gxssjNcH0MCViG8ONI2kksvfFQ==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.19.5", + "@esbuild/android-arm64": "0.19.5", + "@esbuild/android-x64": "0.19.5", + "@esbuild/darwin-arm64": "0.19.5", + "@esbuild/darwin-x64": "0.19.5", + "@esbuild/freebsd-arm64": "0.19.5", + "@esbuild/freebsd-x64": "0.19.5", + "@esbuild/linux-arm": "0.19.5", + "@esbuild/linux-arm64": "0.19.5", + "@esbuild/linux-ia32": "0.19.5", + "@esbuild/linux-loong64": "0.19.5", + "@esbuild/linux-mips64el": "0.19.5", + "@esbuild/linux-ppc64": "0.19.5", + "@esbuild/linux-riscv64": "0.19.5", + "@esbuild/linux-s390x": "0.19.5", + "@esbuild/linux-x64": "0.19.5", + "@esbuild/netbsd-x64": "0.19.5", + "@esbuild/openbsd-x64": "0.19.5", + "@esbuild/sunos-x64": "0.19.5", + "@esbuild/win32-arm64": "0.19.5", + "@esbuild/win32-ia32": "0.19.5", + "@esbuild/win32-x64": "0.19.5" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -15881,6 +16875,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-negated-glob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz", + "integrity": "sha512-czXVVn/QEmgvej1f50BZ648vUI+em0xqMq2Sn+QncCLN4zj1UAxlT+kw/6ggQTOaZPd1HqKQGEqbpQVtJucWug==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-negative-zero": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", @@ -15935,6 +16938,18 @@ "node": ">=8" } }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-plain-object": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", @@ -16088,6 +17103,15 @@ "tslib": "^2.0.3" } }, + "node_modules/is-valid-glob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-1.0.0.tgz", + "integrity": "sha512-AhiROmoEFDSsjx8hW+5sGwgKVIORcXnrlAx/R0ZSeaPw70Vw0CqkGBBhHGL58Uox2eXnU1AnvXJl1XlyedO5bA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-weakmap": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", @@ -17010,6 +18034,15 @@ "node": ">=14.0.0" } }, + "node_modules/lead": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/lead/-/lead-4.0.0.tgz", + "integrity": "sha512-DpMa59o5uGUWWjruMp71e6knmwKU3jRBBn1kjuLWN9EeIOxNeSAwvHf03WIl8g/ZMR2oSQC9ej3yeLBwdDc/pg==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -17404,6 +18437,25 @@ "react": ">= 0.14.0" } }, + "node_modules/matcher-collection": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/matcher-collection/-/matcher-collection-2.0.1.tgz", + "integrity": "sha512-daE62nS2ZQsDg9raM0IlZzLmI2u+7ZapXBwdoeBUKAYERPDDIc0qNqA8E0Rp2D+gspKR7BgIFP52GeujaGXWeQ==", + "dev": true, + "dependencies": { + "@types/minimatch": "^3.0.3", + "minimatch": "^3.0.2" + }, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/matcher-collection/node_modules/@types/minimatch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", + "dev": true + }, "node_modules/mdast-util-definitions": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-4.0.0.tgz", @@ -17640,6 +18692,15 @@ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", "dev": true }, + "node_modules/mktemp": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/mktemp/-/mktemp-0.4.0.tgz", + "integrity": "sha512-IXnMcJ6ZyTuhRmJSjzvHSRhlVPiN9Jwc6e59V0bEJ0ba6OBeX2L0E+mRN1QseeOF4mM+F1Rit6Nh7o+rl2Yn/A==", + "dev": true, + "engines": { + "node": ">0.9" + } + }, "node_modules/mlly": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.4.2.tgz", @@ -17767,7 +18828,6 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dev": true, "dependencies": { "whatwg-url": "^5.0.0" }, @@ -17848,6 +18908,18 @@ "svg-arc-to-cubic-bezier": "^3.0.0" } }, + "node_modules/now-and-later": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-3.0.0.tgz", + "integrity": "sha512-pGO4pzSdaxhWTGkfSfHx3hVzJVslFPwBp2Myq9MYN/ChfJZF87ochMAXnvz6/58RJSf5ik2q9tXprBBrk2cpcg==", + "dev": true, + "dependencies": { + "once": "^1.4.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, "node_modules/npm-run-path": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", @@ -18300,6 +19372,46 @@ "resolved": "https://registry.npmjs.org/parse-svg-path/-/parse-svg-path-0.1.2.tgz", "integrity": "sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ==" }, + "node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dev": true, + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz", + "integrity": "sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==", + "dev": true, + "dependencies": { + "domhandler": "^5.0.2", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter/node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -18361,6 +19473,12 @@ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, + "node_modules/path-posix": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/path-posix/-/path-posix-1.0.0.tgz", + "integrity": "sha512-1gJ0WpNIiYcQydgg3Ed8KzvIqTsDpNwq+cjBCssvBtuTWjEqY1AW+i+OepiEMqDCzyro9B2sLAe4RBPajMYFiA==", + "dev": true + }, "node_modules/path-root": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", @@ -18782,6 +19900,15 @@ "asap": "~2.0.3" } }, + "node_modules/promise-map-series": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/promise-map-series/-/promise-map-series-0.3.0.tgz", + "integrity": "sha512-3npG2NGhTc8BWBolLLf8l/92OxMGaRLbqvIh9wjCHhDXNvk4zsxaTaCpiCunW09qWPrN2zeNSNwRLVBrQQtutA==", + "dev": true, + "engines": { + "node": "10.* || >= 12.*" + } + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -19000,6 +20127,35 @@ } ] }, + "node_modules/queue-tick": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", + "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", + "dev": true + }, + "node_modules/quick-temp": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/quick-temp/-/quick-temp-0.1.8.tgz", + "integrity": "sha512-YsmIFfD9j2zaFwJkzI6eMG7y0lQP7YeWzgtFgNl38pGWZBSXJooZbOWwkcRot7Vt0Fg9L23pX0tqWU3VvLDsiA==", + "dev": true, + "dependencies": { + "mktemp": "~0.4.0", + "rimraf": "^2.5.4", + "underscore.string": "~3.3.4" + } + }, + "node_modules/quick-temp/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, "node_modules/ramda": { "version": "0.29.0", "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.29.0.tgz", @@ -19164,6 +20320,27 @@ "react": ">=16.13.1" } }, + "node_modules/react-i18next": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-13.3.0.tgz", + "integrity": "sha512-FlR9xjYHSPIJfQspEmkN0yOlxgRyNuiJKJ8gCaZH08UJ7SZHG+VrptEPcpEMEchjNoCOZdKcvJ3PnmHEZhkeXg==", + "dependencies": { + "@babel/runtime": "^7.22.5", + "html-parse-stringify": "^3.0.1" + }, + "peerDependencies": { + "i18next": ">= 23.2.3", + "react": ">= 16.8.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/react-inspector": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/react-inspector/-/react-inspector-6.0.2.tgz", @@ -19713,6 +20890,15 @@ "integrity": "sha512-O3vsMYfWighyFbTd8hk8VaSj9UAGENxAtX+//ugIst2RMk5e03h6RoIS+0ylsFxY1gvmPuAY/PO4It+gPEeySA==", "dev": true }, + "node_modules/replace-ext": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-2.0.0.tgz", + "integrity": "sha512-UszKE5KVK6JvyD92nzMn9cDapSk6w/CaFZ96CnmDMUqH9oowfxF/ZjRITD25H4DnOQClLA4/j7jLGXXLVKxAug==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -19753,6 +20939,18 @@ "node": ">=8" } }, + "node_modules/resolve-options": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-options/-/resolve-options-2.0.0.tgz", + "integrity": "sha512-/FopbmmFOQCfsCx77BRFdKOniglTiHumLgwvd6IDPihy1GKkadZbgQJBcTb2lMzSR1pndzd96b1nZrreZ7+9/A==", + "dev": true, + "dependencies": { + "value-or-function": "^4.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", @@ -19871,6 +21069,15 @@ "fsevents": "~2.3.2" } }, + "node_modules/rsvp": { + "version": "4.8.5", + "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", + "integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==", + "dev": true, + "engines": { + "node": "6.* || >= 7.*" + } + }, "node_modules/run-applescript": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-5.0.0.tgz", @@ -20297,6 +21504,21 @@ "tslib": "^2.0.3" } }, + "node_modules/sort-keys": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-5.0.0.tgz", + "integrity": "sha512-Pdz01AvCAottHTPQGzndktFNdbRA75BgOfeT1hH+AMnJFv8lynkPi42rfeEhpx1saTEI3YNMWxfqu0sFD1G8pw==", + "dev": true, + "dependencies": { + "is-plain-obj": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -20452,6 +21674,15 @@ "url": "https://opencollective.com/storybook" } }, + "node_modules/stream-composer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stream-composer/-/stream-composer-1.0.2.tgz", + "integrity": "sha512-bnBselmwfX5K10AH6L4c8+S5lgZMWI7ZYrz2rvYjCPB2DIMC4Ig8OpxGpNJSxRZ58oti7y1IcNvjBAz9vW5m4w==", + "dev": true, + "dependencies": { + "streamx": "^2.13.2" + } + }, "node_modules/stream-shift": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", @@ -20467,6 +21698,16 @@ "node": ">=10.0.0" } }, + "node_modules/streamx": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.15.1.tgz", + "integrity": "sha512-fQMzy2O/Q47rgwErk/eGeLu/roaFWV0jVsogDmrszM9uIw8L5OA+t+V93MgYlufNptfjmYR1tOMWhei/Eh7TQA==", + "dev": true, + "dependencies": { + "fast-fifo": "^1.1.0", + "queue-tick": "^1.0.1" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -20865,6 +22106,12 @@ "tslib": "^2.0.3" } }, + "node_modules/symlink-or-copy": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/symlink-or-copy/-/symlink-or-copy-1.3.1.tgz", + "integrity": "sha512-0K91MEXFpBUaywiwSSkmKjnGcasG/rVBXFLJz5DrgGabpYD6N+3yZrfD6uUIfpuTu65DZLHi7N8CizHc07BPZA==", + "dev": true + }, "node_modules/synchronous-promise": { "version": "2.0.17", "resolved": "https://registry.npmjs.org/synchronous-promise/-/synchronous-promise-2.0.17.tgz", @@ -21007,6 +22254,15 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "dev": true, + "dependencies": { + "streamx": "^2.12.5" + } + }, "node_modules/telejson": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/telejson/-/telejson-7.2.0.tgz", @@ -21262,6 +22518,18 @@ "node": ">=8.0" } }, + "node_modules/to-through": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/to-through/-/to-through-3.0.0.tgz", + "integrity": "sha512-y8MN937s/HVhEoBU1SxfHC+wxCHkV1a9gW8eAdTadYh/bGyesZIVcbjI+mSpFbSVwQici/XjBjuUyri1dnXwBw==", + "dev": true, + "dependencies": { + "streamx": "^2.12.5" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/tocbot": { "version": "4.21.2", "resolved": "https://registry.npmjs.org/tocbot/-/tocbot-4.21.2.tgz", @@ -21280,8 +22548,7 @@ "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "dev": true + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, "node_modules/ts-api-utils": { "version": "1.0.3", @@ -21562,6 +22829,25 @@ "node": ">=0.10.0" } }, + "node_modules/underscore.string": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-3.3.6.tgz", + "integrity": "sha512-VoC83HWXmCrF6rgkyxS9GHv8W9Q5nhMKho+OadDJGzL2oDYbYEppBaCMH6pFlwLeqj2QS+hhkw2kpXkSdD1JxQ==", + "dev": true, + "dependencies": { + "sprintf-js": "^1.1.1", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/underscore.string/node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true + }, "node_modules/undici-types": { "version": "5.25.3", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.25.3.tgz", @@ -21962,6 +23248,15 @@ "spdx-expression-parse": "^3.0.0" } }, + "node_modules/value-or-function": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-4.0.0.tgz", + "integrity": "sha512-aeVK81SIuT6aMJfNo9Vte8Dw0/FZINGBV8BfCraGtqVxIeLAEhJyoWs8SmvRVmXfGss2PmmOwZCuBPbZR+IYWg==", + "dev": true, + "engines": { + "node": ">= 10.13.0" + } + }, "node_modules/value-or-promise": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/value-or-promise/-/value-or-promise-1.0.12.tgz", @@ -21980,6 +23275,133 @@ "node": ">= 0.8" } }, + "node_modules/vinyl": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-3.0.0.tgz", + "integrity": "sha512-rC2VRfAVVCGEgjnxHUnpIVh3AGuk62rP3tqVrn+yab0YH7UULisC085+NYH+mnqf3Wx4SpSi1RQMwudL89N03g==", + "dev": true, + "dependencies": { + "clone": "^2.1.2", + "clone-stats": "^1.0.0", + "remove-trailing-separator": "^1.1.0", + "replace-ext": "^2.0.0", + "teex": "^1.0.1" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/vinyl-contents": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/vinyl-contents/-/vinyl-contents-2.0.0.tgz", + "integrity": "sha512-cHq6NnGyi2pZ7xwdHSW1v4Jfnho4TEGtxZHw01cmnc8+i7jgR6bRnED/LbrKan/Q7CvVLbnvA5OepnhbpjBZ5Q==", + "dev": true, + "dependencies": { + "bl": "^5.0.0", + "vinyl": "^3.0.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/vinyl-contents/node_modules/bl": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", + "integrity": "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==", + "dev": true, + "dependencies": { + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/vinyl-contents/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/vinyl-fs": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-4.0.0.tgz", + "integrity": "sha512-7GbgBnYfaquMk3Qu9g22x000vbYkOex32930rBnc3qByw6HfMEAoELjCjoJv4HuEQxHAurT+nvMHm6MnJllFLw==", + "dev": true, + "dependencies": { + "fs-mkdirp-stream": "^2.0.1", + "glob-stream": "^8.0.0", + "graceful-fs": "^4.2.11", + "iconv-lite": "^0.6.3", + "is-valid-glob": "^1.0.0", + "lead": "^4.0.0", + "normalize-path": "3.0.0", + "resolve-options": "^2.0.0", + "stream-composer": "^1.0.2", + "streamx": "^2.14.0", + "to-through": "^3.0.0", + "value-or-function": "^4.0.0", + "vinyl": "^3.0.0", + "vinyl-sourcemap": "^2.0.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/vinyl-fs/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/vinyl-sourcemap": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/vinyl-sourcemap/-/vinyl-sourcemap-2.0.0.tgz", + "integrity": "sha512-BAEvWxbBUXvlNoFQVFVHpybBbjW1r03WhohJzJDSfgrrK5xVYIDTan6xN14DlyImShgDRv2gl9qhM6irVMsV0Q==", + "dev": true, + "dependencies": { + "convert-source-map": "^2.0.0", + "graceful-fs": "^4.2.10", + "now-and-later": "^3.0.0", + "streamx": "^2.12.5", + "vinyl": "^3.0.0", + "vinyl-contents": "^2.0.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/vinyl/node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, "node_modules/vite": { "version": "4.4.11", "resolved": "https://registry.npmjs.org/vite/-/vite-4.4.11.tgz", @@ -22272,6 +23694,45 @@ "node": ">=0.4.0" } }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/vue-template-compiler": { + "version": "2.7.14", + "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.14.tgz", + "integrity": "sha512-zyA5Y3ArvVG0NacJDkkzJuPQDF8RFeRlzV2vLeSnhSpieO6LK2OVbdLPi5MPPs09Ii+gMO8nY4S3iKQxBxDmWQ==", + "dev": true, + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, + "node_modules/walk-sync": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/walk-sync/-/walk-sync-2.2.0.tgz", + "integrity": "sha512-IC8sL7aB4/ZgFcGI2T1LczZeFWZ06b3zoHH7jBPyHxOtIIz1jppWHjjEXkOFvFojBVAK9pV7g47xOZ4LW3QLfg==", + "dev": true, + "dependencies": { + "@types/minimatch": "^3.0.3", + "ensure-posix-path": "^1.1.0", + "matcher-collection": "^2.0.0", + "minimatch": "^3.0.4" + }, + "engines": { + "node": "8.* || >= 10.*" + } + }, + "node_modules/walk-sync/node_modules/@types/minimatch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", + "dev": true + }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", @@ -22386,7 +23847,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dev": true, "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" @@ -22395,8 +23855,7 @@ "node_modules/whatwg-url/node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "dev": true + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, "node_modules/which": { "version": "2.0.2", diff --git a/frontend/package.json b/frontend/package.json index 3b5244a4d..441aebad3 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,13 +6,14 @@ "scripts": { "dev": "vite", "generate": "graphql-codegen && eslint --fix .", - "lint": "graphql-codegen && eslint . && tsc", + "lint": "graphql-codegen && eslint . && tsc && i18next --fail-on-warnings --fail-on-update", "build": "rimraf ./dist/ && vite build", "preview": "vite preview", "test": "vitest", "coverage": "vitest run --coverage", "storybook": "storybook dev -p 6006", - "build-storybook": "storybook build" + "build-storybook": "storybook build", + "i18n": "i18next" }, "dependencies": { "@emotion/react": "^11.11.1", @@ -29,12 +30,16 @@ "classnames": "^2.3.2", "date-fns": "^2.30.0", "graphql": "^16.8.1", + "i18next": "^23.5.1", + "i18next-browser-languagedetector": "^7.1.0", + "i18next-http-backend": "^2.2.2", "jotai": "^2.4.3", "jotai-devtools": "^0.6.3", "jotai-location": "^0.5.1", "jotai-urql": "^0.7.1", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-i18next": "^13.3.0", "ua-parser-js": "^1.0.36" }, "devDependencies": { @@ -61,6 +66,7 @@ "eslint-plugin-matrix-org": "^1.2.1", "eslint-plugin-prettier": "^5.0.0", "happy-dom": "^12.9.1", + "i18next-parser": "^8.9.0", "postcss": "^8.4.31", "prettier": "3.0.3", "react-test-renderer": "^18.2.0", diff --git a/frontend/src/@types/i18next.d.ts b/frontend/src/@types/i18next.d.ts new file mode 100644 index 000000000..63a64b6b6 --- /dev/null +++ b/frontend/src/@types/i18next.d.ts @@ -0,0 +1,27 @@ +// Copyright 2023 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import "i18next"; +import type frontend from "../../public/locales/en.json"; + +declare module "i18next" { + interface CustomTypeOptions { + keySeparator: "."; + pluralSeparator: ":"; + defaultNS: "frontend"; + resources: { + frontend: typeof frontend; + }; + } +} diff --git a/frontend/src/components/Client/OAuth2ClientDetail.tsx b/frontend/src/components/Client/OAuth2ClientDetail.tsx index 88a8803a8..f25a0e946 100644 --- a/frontend/src/components/Client/OAuth2ClientDetail.tsx +++ b/frontend/src/components/Client/OAuth2ClientDetail.tsx @@ -13,6 +13,7 @@ // limitations under the License. import { H3 } from "@vector-im/compound-web"; +import { useTranslation } from "react-i18next"; import { FragmentType, useFragment } from "../../gql"; import { graphql } from "../../gql/gql"; @@ -52,16 +53,20 @@ const FriendlyExternalLink: React.FC<{ uri?: string }> = ({ uri }) => { const OAuth2ClientDetail: React.FC = ({ client }) => { const data = useFragment(OAUTH2_CLIENT_FRAGMENT, client); + const { t } = useTranslation(); const details = [ - { label: "Name", value: data.clientName }, - { label: "Client ID", value: {data.clientId} }, + { label: t("frontend.oauth2_client_detail.name"), value: data.clientName }, { - label: "Terms of service", + label: t("frontend.oauth2_client_detail.id"), + value: {data.clientId}, + }, + { + label: t("frontend.oauth2_client_detail.terms"), value: data.tosUri && , }, { - label: "Policy", + label: t("frontend.oauth2_client_detail.policy"), value: data.policyUri && , }, ].filter(({ value }) => !!value); @@ -76,7 +81,10 @@ const OAuth2ClientDetail: React.FC = ({ client }) => { />

{data.clientName}

- + ); }; diff --git a/frontend/src/components/ConfirmationModal/ConfirmationModal.tsx b/frontend/src/components/ConfirmationModal/ConfirmationModal.tsx index 4835bf998..8b01a510b 100644 --- a/frontend/src/components/ConfirmationModal/ConfirmationModal.tsx +++ b/frontend/src/components/ConfirmationModal/ConfirmationModal.tsx @@ -26,6 +26,7 @@ import { import { Button } from "@vector-im/compound-web"; import classNames from "classnames"; import { ReactNode } from "react"; +import { Translation } from "react-i18next"; import styles from "./ConfirmationModal.module.css"; @@ -51,40 +52,44 @@ const ConfirmationModal: React.FC> = ({ trigger, title, }) => ( - - {trigger} - - - { - if (onDeny) { - onDeny(); - } else { - // if there is no deny callback, we should prevent the escape key from closing the modal - event.preventDefault(); - } - }} - > - {title} - {children} -
- {onDeny && ( - - - - )} - - - -
-
-
-
+ + {(t): ReactNode => ( + + {trigger} + + + { + if (onDeny) { + onDeny(); + } else { + // if there is no deny callback, we should prevent the escape key from closing the modal + event.preventDefault(); + } + }} + > + {title} + {children} +
+ {onDeny && ( + + + + )} + + + +
+
+
+
+ )} +
); export default ConfirmationModal; diff --git a/frontend/src/components/ErrorBoundary.tsx b/frontend/src/components/ErrorBoundary.tsx index 8db0f5ece..73ab2807f 100644 --- a/frontend/src/components/ErrorBoundary.tsx +++ b/frontend/src/components/ErrorBoundary.tsx @@ -15,6 +15,7 @@ import { CombinedError } from "@urql/core"; import { Alert } from "@vector-im/compound-web"; import { ErrorInfo, ReactNode, PureComponent } from "react"; +import { Translation } from "react-i18next"; import GraphQLError from "./GraphQLError"; @@ -61,9 +62,13 @@ export default class ErrorBoundary extends PureComponent { } return ( - - {this.state.error.message} - + + {(t): ReactNode => ( + + {this.state.error!.message} + + )} + ); } diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index 145670334..26fae6e0a 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -13,6 +13,7 @@ // limitations under the License. import { useAtomValue } from "jotai"; +import { useTranslation } from "react-i18next"; import { currentUserIdAtom } from "../atoms"; import { isErr, unwrapErr, unwrapOk } from "../result"; @@ -28,6 +29,8 @@ import UserGreeting from "./UserGreeting"; const Layout: React.FC<{ children?: React.ReactNode }> = ({ children }) => { const route = useAtomValue(routeAtom); const result = useAtomValue(currentUserIdAtom); + const { t } = useTranslation(); + if (isErr(result)) return ; // Hide the nav bar & user greeting on the verify-email page @@ -48,8 +51,12 @@ const Layout: React.FC<{ children?: React.ReactNode }> = ({ children }) => { - Profile - Sessions + + {t("frontend.nav.profile")} + + + {t("frontend.nav.sessions")} + )} diff --git a/frontend/src/components/LoadingScreen/__snapshots__/LoadingScreen.test.tsx.snap b/frontend/src/components/LoadingScreen/__snapshots__/LoadingScreen.test.tsx.snap index 59194494a..15b1787f3 100644 --- a/frontend/src/components/LoadingScreen/__snapshots__/LoadingScreen.test.tsx.snap +++ b/frontend/src/components/LoadingScreen/__snapshots__/LoadingScreen.test.tsx.snap @@ -21,7 +21,7 @@ exports[`LoadingScreen > render 1`] = ` - Loading... + Loading… diff --git a/frontend/src/components/LoadingSpinner/LoadingSpinner.tsx b/frontend/src/components/LoadingSpinner/LoadingSpinner.tsx index 887b4da1c..32d150932 100644 --- a/frontend/src/components/LoadingSpinner/LoadingSpinner.tsx +++ b/frontend/src/components/LoadingSpinner/LoadingSpinner.tsx @@ -12,6 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. +import { ReactNode } from "react"; +import { Translation } from "react-i18next"; + import styles from "./LoadingSpinner.module.css"; const LoadingSpinner: React.FC<{ inline?: boolean }> = ({ inline }) => ( @@ -27,7 +30,9 @@ const LoadingSpinner: React.FC<{ inline?: boolean }> = ({ inline }) => ( fill="currentFill" /> - Loading... + + {(t): ReactNode => t("common.loading")} + ); diff --git a/frontend/src/components/NotFound.tsx b/frontend/src/components/NotFound.tsx index 5081fbe3b..90f7e1468 100644 --- a/frontend/src/components/NotFound.tsx +++ b/frontend/src/components/NotFound.tsx @@ -13,7 +13,15 @@ // limitations under the License. import { Alert } from "@vector-im/compound-web"; +import { ReactNode } from "react"; +import { Translation } from "react-i18next"; -const NotFound: React.FC = () => ; +const NotFound: React.FC = () => ( + + {(t): ReactNode => ( + + )} + +); export default NotFound; diff --git a/frontend/src/components/NotLoggedIn.tsx b/frontend/src/components/NotLoggedIn.tsx index 575a591de..7625fa4c9 100644 --- a/frontend/src/components/NotLoggedIn.tsx +++ b/frontend/src/components/NotLoggedIn.tsx @@ -13,9 +13,15 @@ // limitations under the License. import { Alert } from "@vector-im/compound-web"; +import { ReactNode } from "react"; +import { Translation } from "react-i18next"; const NotLoggedIn: React.FC = () => ( - + + {(t): ReactNode => ( + + )} + ); export default NotLoggedIn; diff --git a/frontend/src/components/PaginationControls.tsx b/frontend/src/components/PaginationControls.tsx index e2df45b61..ea8630420 100644 --- a/frontend/src/components/PaginationControls.tsx +++ b/frontend/src/components/PaginationControls.tsx @@ -13,6 +13,7 @@ // limitations under the License. import { Button } from "@vector-im/compound-web"; +import { useTranslation } from "react-i18next"; type Props = { onNext: (() => void) | null; @@ -30,6 +31,8 @@ const PaginationControls: React.FC = ({ count, disabled, }) => { + const { t } = useTranslation(); + if (autoHide && !onNext && !onPrev) { return null; } @@ -41,10 +44,12 @@ const PaginationControls: React.FC = ({ disabled={disabled || !onPrev} onClick={(): void => onPrev?.()} > - Previous + {t("common.previous")}
- {count !== undefined ? <>Total: {count} : null} + {count !== undefined ? ( + <>{t("frontend.pagination_controls.total", { totalCount: count })} + ) : null}
); diff --git a/frontend/src/components/Session/DeviceTypeIcon.tsx b/frontend/src/components/Session/DeviceTypeIcon.tsx index 0417681c1..d26df8ab0 100644 --- a/frontend/src/components/Session/DeviceTypeIcon.tsx +++ b/frontend/src/components/Session/DeviceTypeIcon.tsx @@ -17,6 +17,7 @@ import IconMobile from "@vector-im/compound-design-tokens/icons/mobile.svg?react import IconUnknown from "@vector-im/compound-design-tokens/icons/unknown.svg?react"; import IconBrowser from "@vector-im/compound-design-tokens/icons/web-browser.svg?react"; import { FunctionComponent, SVGProps } from "react"; +import { useTranslation } from "react-i18next"; import { DeviceType } from "../../utils/parseUserAgent"; @@ -32,17 +33,20 @@ const deviceTypeToIcon: Record< [DeviceType.Web]: IconBrowser, }; -const deviceTypeToLabel: Record = { - [DeviceType.Unknown]: "Unknown device type", - [DeviceType.Desktop]: "Desktop", - [DeviceType.Mobile]: "Mobile", - [DeviceType.Web]: "Web", -}; - const DeviceTypeIcon: React.FC<{ deviceType: DeviceType }> = ({ deviceType, }) => { + const { t } = useTranslation(); + const Icon = deviceTypeToIcon[deviceType]; + + const deviceTypeToLabel: Record = { + [DeviceType.Unknown]: t("frontend.device_type_icon_label.unknown"), + [DeviceType.Desktop]: t("frontend.device_type_icon_label.desktop"), + [DeviceType.Mobile]: t("frontend.device_type_icon_label.mobile"), + [DeviceType.Web]: t("frontend.device_type_icon_label.web"), + }; + const label = deviceTypeToLabel[deviceType]; return ; diff --git a/frontend/src/components/Session/EndSessionButton.tsx b/frontend/src/components/Session/EndSessionButton.tsx index 1aa5d1326..7ce024309 100644 --- a/frontend/src/components/Session/EndSessionButton.tsx +++ b/frontend/src/components/Session/EndSessionButton.tsx @@ -14,6 +14,7 @@ import { Button } from "@vector-im/compound-web"; import { useState } from "react"; +import { useTranslation } from "react-i18next"; import ConfirmationModal from "../ConfirmationModal/ConfirmationModal"; import LoadingSpinner from "../LoadingSpinner/LoadingSpinner"; @@ -26,6 +27,7 @@ const EndSessionButton: React.FC<{ endSession: () => Promise }> = ({ endSession, }) => { const [inProgress, setInProgress] = useState(false); + const { t } = useTranslation(); const onConfirm = async (): Promise => { setInProgress(true); @@ -45,10 +47,11 @@ const EndSessionButton: React.FC<{ endSession: () => Promise }> = ({ - {inProgress && }End session + {inProgress && } + {t("frontend.end_session_button.text")} } /> diff --git a/frontend/src/components/Session/LastActive.tsx b/frontend/src/components/Session/LastActive.tsx index 6e06efef9..a841ca8cf 100644 --- a/frontend/src/components/Session/LastActive.tsx +++ b/frontend/src/components/Session/LastActive.tsx @@ -13,6 +13,7 @@ // limitations under the License. import { differenceInSeconds, parseISO } from "date-fns"; +import { useTranslation } from "react-i18next"; import { formatDate, formatReadableDate } from "../DateTime"; @@ -27,6 +28,8 @@ const LastActive: React.FC<{ lastActive: Date | string; now?: Date | string; }> = ({ lastActive: lastActiveProps, now: nowProps }) => { + const { t } = useTranslation(); + const lastActive = typeof lastActiveProps === "string" ? parseISO(lastActiveProps) @@ -42,15 +45,23 @@ const LastActive: React.FC<{ if (differenceInSeconds(now, lastActive) <= ACTIVE_NOW_MAX_AGE) { return ( - Active now + {t("frontend.last_active.active_now")} ); } if (differenceInSeconds(now, lastActive) > INACTIVE_MIN_AGE) { - return Inactive for 90+ days; + return ( + + {t("frontend.last_active.inactive_90_days")} + + ); } const relativeDate = formatReadableDate(lastActive, now); - return {`Active ${relativeDate}`}; + return ( + + {t("frontend.last_active.active_date", { relativeDate })} + + ); }; export default LastActive; diff --git a/frontend/src/components/Session/SelectableSession.tsx b/frontend/src/components/Session/SelectableSession.tsx index d357be04d..faae21765 100644 --- a/frontend/src/components/Session/SelectableSession.tsx +++ b/frontend/src/components/Session/SelectableSession.tsx @@ -13,6 +13,7 @@ // limitations under the License. import { Checkbox } from "@vector-im/compound-web"; +import { useTranslation } from "react-i18next"; import styles from "./SelectableSession.module.css"; @@ -29,13 +30,14 @@ const SelectableSession: React.FC> = ({ onSelect, children, }) => { + const { t } = useTranslation(); return (
{children} diff --git a/frontend/src/components/Session/Session.tsx b/frontend/src/components/Session/Session.tsx index 197585e01..f3d500f45 100644 --- a/frontend/src/components/Session/Session.tsx +++ b/frontend/src/components/Session/Session.tsx @@ -14,6 +14,7 @@ import { H6, Body, Badge } from "@vector-im/compound-web"; import { ReactNode } from "react"; +import { Trans, useTranslation } from "react-i18next"; import { DeviceType } from "../../utils/parseUserAgent"; import Block from "../Block"; @@ -53,20 +54,28 @@ const Session: React.FC> = ({ children, deviceType, }) => { + const { t } = useTranslation(); + return (
- {isCurrent && Current} + {isCurrent && ( + {t("frontend.session.current_badge")} + )}
{name || id}
- Signed in + + Signed in + {!!finishedAt && ( - Finished + + Finished + )} {!!lastActiveAt && ( diff --git a/frontend/src/components/SessionDetail/BrowserSessionDetail.tsx b/frontend/src/components/SessionDetail/BrowserSessionDetail.tsx index b63d8122a..c98968cee 100644 --- a/frontend/src/components/SessionDetail/BrowserSessionDetail.tsx +++ b/frontend/src/components/SessionDetail/BrowserSessionDetail.tsx @@ -14,6 +14,7 @@ import { Badge } from "@vector-im/compound-web"; import { parseISO } from "date-fns"; +import { useTranslation } from "react-i18next"; import { FragmentType, graphql, useFragment } from "../../gql"; import { @@ -57,6 +58,7 @@ type Props = { const BrowserSessionDetail: React.FC = ({ session }) => { const data = useFragment(FRAGMENT, session); const currentBrowserSessionId = useCurrentBrowserSessionId(); + const { t } = useTranslation(); const isCurrent = currentBrowserSessionId === data.id; const onSessionEnd = useEndBrowserSession(data.id, isCurrent); @@ -113,13 +115,16 @@ const BrowserSessionDetail: React.FC = ({ session }) => { {isCurrent && ( - Current + {t("frontend.browser_session_details.current_badge")} )} {sessionName} - + {!data.finishedAt && } ); diff --git a/frontend/src/components/SessionDetail/CompatSessionDetail.tsx b/frontend/src/components/SessionDetail/CompatSessionDetail.tsx index 5d5bced83..d7b832f24 100644 --- a/frontend/src/components/SessionDetail/CompatSessionDetail.tsx +++ b/frontend/src/components/SessionDetail/CompatSessionDetail.tsx @@ -14,6 +14,7 @@ import { parseISO } from "date-fns"; import { useSetAtom } from "jotai"; +import { useTranslation } from "react-i18next"; import { FragmentType, graphql, useFragment } from "../../gql"; import BlockList from "../BlockList/BlockList"; @@ -48,6 +49,7 @@ type Props = { const CompatSessionDetail: React.FC = ({ session }) => { const data = useFragment(FRAGMENT, session); const endSession = useSetAtom(endCompatSessionFamily(data.id)); + const { t } = useTranslation(); const onSessionEnd = async (): Promise => { await endSession(); @@ -91,7 +93,7 @@ const CompatSessionDetail: React.FC = ({ session }) => { if (data.ssoLogin?.redirectUri) { clientDetails.push({ - label: "Name", + label: t("frontend.compat_session_detail.name"), value: simplifyUrl(data.ssoLogin.redirectUri), }); clientDetails.push({ @@ -113,9 +115,15 @@ const CompatSessionDetail: React.FC = ({ session }) => { > {data.deviceId || data.id} - + {clientDetails.length > 0 ? ( - + ) : null} {!data.finishedAt && } diff --git a/frontend/src/components/SessionDetail/OAuth2SessionDetail.tsx b/frontend/src/components/SessionDetail/OAuth2SessionDetail.tsx index 4f89e7424..04c26d435 100644 --- a/frontend/src/components/SessionDetail/OAuth2SessionDetail.tsx +++ b/frontend/src/components/SessionDetail/OAuth2SessionDetail.tsx @@ -14,6 +14,7 @@ import { parseISO } from "date-fns"; import { useSetAtom } from "jotai"; +import { useTranslation } from "react-i18next"; import { FragmentType, graphql, useFragment } from "../../gql"; import { Link } from "../../routing"; @@ -53,6 +54,7 @@ type Props = { const OAuth2SessionDetail: React.FC = ({ session }) => { const data = useFragment(FRAGMENT, session); const endSession = useSetAtom(endSessionFamily(data.id)); + const { t } = useTranslation(); const onSessionEnd = async (): Promise => { await endSession(); @@ -104,11 +106,13 @@ const OAuth2SessionDetail: React.FC = ({ session }) => { ]; const clientTitle = ( - Client + + {t("frontend.oauth2_session_detail.client_title")} + ); const clientDetails = [ { - label: "Name", + label: t("frontend.oauth2_session_detail.client_details_name"), value: ( <> = ({ session }) => { > {deviceId || data.id} - + {!data.finishedAt && } diff --git a/frontend/src/components/SessionDetail/SessionDetail.tsx b/frontend/src/components/SessionDetail/SessionDetail.tsx index 232412422..afa36fbf0 100644 --- a/frontend/src/components/SessionDetail/SessionDetail.tsx +++ b/frontend/src/components/SessionDetail/SessionDetail.tsx @@ -17,6 +17,7 @@ import { useAtomValue } from "jotai"; import { atomFamily } from "jotai/utils"; import { atomWithQuery } from "jotai-urql"; import { useMemo } from "react"; +import { useTranslation } from "react-i18next"; import { graphql } from "../../gql"; import { Link } from "../../routing"; @@ -59,15 +60,19 @@ const SessionDetail: React.FC<{ [deviceId, userId], ); const result = useAtomValue(sessionFamilyAtomWithProps); + const { t } = useTranslation(); const session = result.data?.session; if (!session) { return ( - - This session does not exist, or is no longer active. + + {t("frontend.session_detail.alert.text")} - Go back + {t("frontend.session_detail.alert.button")} ); diff --git a/frontend/src/components/UnverifiedEmailAlert/UnverifiedEmailAlert.tsx b/frontend/src/components/UnverifiedEmailAlert/UnverifiedEmailAlert.tsx index 4372719c8..318b2bce1 100644 --- a/frontend/src/components/UnverifiedEmailAlert/UnverifiedEmailAlert.tsx +++ b/frontend/src/components/UnverifiedEmailAlert/UnverifiedEmailAlert.tsx @@ -14,6 +14,7 @@ import { Alert } from "@vector-im/compound-web"; import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; import { FragmentType, useFragment, graphql } from "../../gql"; import { Link } from "../../routing"; @@ -34,6 +35,7 @@ const UnverifiedEmailAlert: React.FC<{ }> = ({ user }) => { const data = useFragment(UNVERIFIED_EMAILS_FRAGMENT, user); const [dismiss, setDismiss] = useState(false); + const { t } = useTranslation(); const doDismiss = (): void => setDismiss(true); @@ -48,13 +50,15 @@ const UnverifiedEmailAlert: React.FC<{ return ( - You have {data.unverifiedEmails.totalCount} unverified email address(es).{" "} + {t("frontend.unverified_email_alert.text", { + count: data.unverifiedEmails.totalCount, + })}{" "} - Review and verify + {t("frontend.unverified_email_alert.button")} ); diff --git a/frontend/src/components/UnverifiedEmailAlert/__snapshots__/UnverifiedEmailAlert.test.tsx.snap b/frontend/src/components/UnverifiedEmailAlert/__snapshots__/UnverifiedEmailAlert.test.tsx.snap index 91c9dac2b..24b3684c6 100644 --- a/frontend/src/components/UnverifiedEmailAlert/__snapshots__/UnverifiedEmailAlert.test.tsx.snap +++ b/frontend/src/components/UnverifiedEmailAlert/__snapshots__/UnverifiedEmailAlert.test.tsx.snap @@ -29,9 +29,7 @@ exports[` > renders a warning when there are unverified Unverified email

- You have - 2 - unverified email address(es). + You have 2 unverified email addresses. void }> = ({ disabled, onClick, }) => ( - + + {(t): ReactNode => ( + + )} + ); const DeleteButtonWithConfirmation: React.FC< ComponentProps > = ({ onClick, ...rest }) => { + const { t } = useTranslation(); const onConfirm = (): void => { onClick?.(); }; @@ -117,7 +123,9 @@ const DeleteButtonWithConfirmation: React.FC< onDeny={onDeny} onConfirm={onConfirm} > - Are you sure you want to remove this email? + + {t("frontend.user_email.delete_button_confirmation_modal.body")} + ); @@ -134,6 +142,7 @@ const UserEmail: React.FC<{ const data = useFragment(FRAGMENT, email); const setPrimaryEmail = useSetAtom(setPrimaryEmailFamily(data.id)); const removeEmail = useSetAtom(removeEmailFamily(data.id)); + const { t } = useTranslation(); const onRemoveClick = (): void => { startTransition(() => { @@ -155,7 +164,11 @@ const UserEmail: React.FC<{ return (

- {isPrimary ? Primary email : Email} + {isPrimary ? ( + {t("frontend.user_email.primary_email")} + ) : ( + {t("frontend.user_email.email")} + )}
{data.email}
@@ -170,14 +183,17 @@ const UserEmail: React.FC<{ disabled={pending} onClick={onSetPrimaryClick} > - Make primary + {t("frontend.user_email.make_primary_button")} )} {!data.confirmedAt && (
- Unverified |{" "} + + {t("frontend.user_email.unverified")} + {" "} + |{" "} - Retry verification + {t("frontend.user_email.retry_button")}
)} diff --git a/frontend/src/components/UserGreeting.tsx b/frontend/src/components/UserGreeting.tsx index 1b414c964..db5ee77f1 100644 --- a/frontend/src/components/UserGreeting.tsx +++ b/frontend/src/components/UserGreeting.tsx @@ -16,6 +16,7 @@ import { Heading, Body, Avatar } from "@vector-im/compound-web"; import { useAtomValue } from "jotai"; import { atomFamily } from "jotai/utils"; import { atomWithQuery } from "jotai-urql"; +import { useTranslation } from "react-i18next"; import { graphql } from "../gql"; @@ -48,6 +49,7 @@ export const userGreetingFamily = atomFamily((userId: string) => { const UserGreeting: React.FC<{ userId: string }> = ({ userId }) => { const result = useAtomValue(userGreetingFamily(userId)); + const { t } = useTranslation(); if (result.data?.user) { const user = result.data.user; @@ -71,7 +73,7 @@ const UserGreeting: React.FC<{ userId: string }> = ({ userId }) => { ); } - return <>Failed to load user; + return <>{t("frontend.user_greeting.error")}; }; export default UserGreeting; diff --git a/frontend/src/components/UserProfile/AddEmailForm.tsx b/frontend/src/components/UserProfile/AddEmailForm.tsx index 600e2100e..1b171a31f 100644 --- a/frontend/src/components/UserProfile/AddEmailForm.tsx +++ b/frontend/src/components/UserProfile/AddEmailForm.tsx @@ -23,6 +23,7 @@ import { import { useAtom } from "jotai"; import { atomWithMutation } from "jotai-urql"; import { useRef, useTransition } from "react"; +import { useTranslation } from "react-i18next"; import { graphql } from "../../gql"; @@ -49,6 +50,7 @@ const AddEmailForm: React.FC<{ const fieldRef = useRef(null); const [addEmailResult, addEmail] = useAtom(addUserEmailAtom); const [pending, startTransition] = useTransition(); + const { t } = useTranslation(); const handleSubmit = (e: React.FormEvent): void => { e.preventDefault(); @@ -87,20 +89,29 @@ const AddEmailForm: React.FC<{ <> {emailExists && ( - - The entered email is already added to this account + + {t("frontend.add_email_form.email_exists_alert.text")} )} {emailInvalid && ( - - The entered email is invalid + + {t("frontend.add_email_form.email_invalid_alert.text")} )} {emailDenied && ( - - The entered email is not allowed by the server policy. + + {t("frontend.add_email_form.email_denied_alert.text")}
    {violations.map((violation, index) => (
  • • {violation}
  • @@ -110,11 +121,11 @@ const AddEmailForm: React.FC<{ )} - + - Add + {t("common.add")} diff --git a/frontend/src/components/UserProfile/UserEmailList.tsx b/frontend/src/components/UserProfile/UserEmailList.tsx index 55c2f42c3..8bfa6ad3f 100644 --- a/frontend/src/components/UserProfile/UserEmailList.tsx +++ b/frontend/src/components/UserProfile/UserEmailList.tsx @@ -17,6 +17,7 @@ import { atom, useAtom, useAtomValue, useSetAtom } from "jotai"; import { atomFamily } from "jotai/utils"; import { atomWithQuery } from "jotai-urql"; import { useTransition } from "react"; +import { useTranslation } from "react-i18next"; import { graphql } from "../../gql"; import { PageInfo } from "../../gql/graphql"; @@ -135,6 +136,7 @@ const UserEmailList: React.FC<{ const [primaryEmailId, refreshPrimaryEmailId] = useAtom( primaryEmailIdFamily(userId), ); + const { t } = useTranslation(); const paginate = (pagination: Pagination): void => { startTransition(() => { @@ -159,9 +161,12 @@ const UserEmailList: React.FC<{ return ( -

    Emails

    +

    {t("frontend.user_email_list.heading")}

    {showNoPrimaryEmailAlert && ( - + )} {result.data?.user?.emails?.edges?.map((edge) => ( = ({ userId }) => { const [fieldValue, setFieldValue] = useState(displayName); const userGreeting = useSetAtom(userGreetingFamily(userId)); + const { t } = useTranslation(); useEffect(() => { setFieldValue(displayName); @@ -131,7 +133,7 @@ const UserName: React.FC<{ userId: string }> = ({ userId }) => { return ( - + = ({ userId }) => { /> {!inProgress && errorMessage && ( - + {errorMessage} )} @@ -152,7 +154,8 @@ const UserName: React.FC<{ userId: string }> = ({ userId }) => { size="sm" type="submit" > - {!!inProgress && }Save + {!!inProgress && } + {t("action.save")} ); diff --git a/frontend/src/components/UserSessionsOverview/AppSessionsList.tsx b/frontend/src/components/UserSessionsOverview/AppSessionsList.tsx index 9684d4350..a4c879db8 100644 --- a/frontend/src/components/UserSessionsOverview/AppSessionsList.tsx +++ b/frontend/src/components/UserSessionsOverview/AppSessionsList.tsx @@ -17,6 +17,7 @@ import { atom, useAtomValue, useSetAtom } from "jotai"; import { atomFamily } from "jotai/utils"; import { atomWithQuery } from "jotai-urql"; import { useTransition } from "react"; +import { useTranslation } from "react-i18next"; import { mapQueryAtom } from "../../atoms"; import { graphql } from "../../gql"; @@ -120,9 +121,10 @@ const AppSessionsList: React.FC<{ userId: string }> = ({ userId }) => { const result = useAtomValue(appSessionListFamily(userId)); const setPagination = useSetAtom(currentPaginationAtom); const [prevPage, nextPage] = useAtomValue(paginationFamily(userId)); + const { t } = useTranslation(); const appSessions = unwrap(result); - if (!appSessions) return <>Failed to load app sessions; + if (!appSessions) return <>{t("frontend.app_sessions_list.error")}; const paginate = (pagination: Pagination): void => { startTransition(() => { @@ -133,7 +135,7 @@ const AppSessionsList: React.FC<{ userId: string }> = ({ userId }) => { return (
    -
    Apps
    +
    {t("frontend.app_sessions_list.heading")}
    {appSessions.edges.map((session) => { const type = session.node.__typename; diff --git a/frontend/src/components/UserSessionsOverview/BrowserSessionsOverview.tsx b/frontend/src/components/UserSessionsOverview/BrowserSessionsOverview.tsx index dfd56a5f4..c0d8951b5 100644 --- a/frontend/src/components/UserSessionsOverview/BrowserSessionsOverview.tsx +++ b/frontend/src/components/UserSessionsOverview/BrowserSessionsOverview.tsx @@ -13,6 +13,7 @@ // limitations under the License. import { Body, H5 } from "@vector-im/compound-web"; +import { useTranslation } from "react-i18next"; import { FragmentType, graphql, useFragment } from "../../gql"; import { Link } from "../../routing"; @@ -34,22 +35,20 @@ const BrowserSessionsOverview: React.FC<{ user: FragmentType; }> = ({ user }) => { const data = useFragment(FRAGMENT, user); - - // allow this until we get i18n - const pluraliseSession = (count: number): string => - count === 1 ? "session" : "sessions"; + const { t } = useTranslation(); return (
    -
    Browsers
    +
    {t("frontend.browser_sessions_overview.heading")}
    - {data.browserSessions.totalCount} active{" "} - {pluraliseSession(data.browserSessions.totalCount)} + {t("frontend.browser_sessions_overview.body", { + count: data.browserSessions.totalCount, + })}
    - View all + {t("frontend.browser_sessions_overview.view_all_button")}
    ); diff --git a/frontend/src/components/UserSessionsOverview/UserSessionsOverview.tsx b/frontend/src/components/UserSessionsOverview/UserSessionsOverview.tsx index c2a30728c..b66a0297c 100644 --- a/frontend/src/components/UserSessionsOverview/UserSessionsOverview.tsx +++ b/frontend/src/components/UserSessionsOverview/UserSessionsOverview.tsx @@ -13,6 +13,7 @@ // limitations under the License. import { H3 } from "@vector-im/compound-web"; +import { useTranslation } from "react-i18next"; import { FragmentType, useFragment } from "../../gql"; import BlockList from "../BlockList"; @@ -24,10 +25,11 @@ const UserSessionsOverview: React.FC<{ user: FragmentType; }> = ({ user }) => { const data = useFragment(FRAGMENT, user); + const { t } = useTranslation(); return ( -

    Where you're signed in

    +

    {t("frontend.user_sessions_overview.heading")}

    diff --git a/frontend/src/components/UserSessionsOverview/__snapshots__/BrowserSessionsOverview.test.tsx.snap b/frontend/src/components/UserSessionsOverview/__snapshots__/BrowserSessionsOverview.test.tsx.snap index 357c046ff..c3d224c4d 100644 --- a/frontend/src/components/UserSessionsOverview/__snapshots__/BrowserSessionsOverview.test.tsx.snap +++ b/frontend/src/components/UserSessionsOverview/__snapshots__/BrowserSessionsOverview.test.tsx.snap @@ -16,10 +16,7 @@ exports[`BrowserSessionsOverview > renders with no browser sessions 1`] = `

    - 0 - active - - sessions + 0 active sessions

renders with sessions 1`] = `

- 2 - active - - sessions + 2 active sessions

(null); + const { t } = useTranslation(); const onFormSubmit = (e: React.FormEvent): void => { e.preventDefault(); @@ -174,24 +176,32 @@ const VerifyEmail: React.FC<{ resendVerificationEmailResult.data?.sendVerificationEmail.status === "SENT"; const invalidCode = verifyEmailResult.data?.verifyEmail.status === "INVALID_CODE"; + const { email: codeEmail } = data; return (
-

Verify your email

+

{t("frontend.verify_email.heading")}

- Enter the 6-digit code sent to{" "} - - {data.email} - + + Enter the 6-digit code sent to{" "} + + {{ codeEmail }} + +
- {invalidCode && } + {invalidCode && ( + + )} - + - Continue + {t("action.continue")}