From 46d63239c1f9d4cccc18dfb0d8bb58e8495f7cb2 Mon Sep 17 00:00:00 2001 From: Jamie Ruderman Date: Wed, 22 Jul 2026 13:14:52 -0700 Subject: [PATCH 1/9] feat(i18n): app-wide i18next instance with lazy locales, migrate cognito to shared namespace --- frontend/package.json | 1 + .../AccountRecovery/AccountRecovery.tsx | 4 +- .../components/AuthLayout/AuthLayout.tsx | 2 +- .../ForgotPassword/ForgotPassword.tsx | 2 +- .../cognito/components/MFACode/MFACode.tsx | 2 +- .../PasswordStrengthInput.tsx | 2 +- .../PasswordVerify/PasswordVerify.tsx | 2 +- .../src/cognito/components/SignIn/SignIn.tsx | 2 +- .../SignInMessage/SignInMessage.tsx | 2 +- .../src/cognito/components/SignUp/SignUp.tsx | 2 +- .../components/SignUpVerify/SignUpVerify.tsx | 2 +- frontend/src/cognito/i18n.ts | 20 ++------- frontend/src/i18n/i18next.d.ts | 13 ++++++ frontend/src/i18n/index.ts | 45 +++++++++++++++++++ frontend/src/i18n/locales/de/app.json | 1 + frontend/src/i18n/locales/de/cognito.json | 1 + frontend/src/i18n/locales/de/notices.json | 1 + frontend/src/i18n/locales/en/app.json | 8 ++++ .../locales/en/cognito.json} | 0 frontend/src/i18n/locales/en/notices.json | 1 + frontend/src/i18n/locales/es/app.json | 1 + frontend/src/i18n/locales/es/cognito.json | 1 + frontend/src/i18n/locales/es/notices.json | 1 + frontend/src/i18n/locales/ja/app.json | 1 + frontend/src/i18n/locales/ja/cognito.json | 1 + frontend/src/i18n/locales/ja/notices.json | 1 + frontend/src/main.tsx | 1 + package-lock.json | 10 +++++ 28 files changed, 103 insertions(+), 27 deletions(-) create mode 100644 frontend/src/i18n/i18next.d.ts create mode 100644 frontend/src/i18n/index.ts create mode 100644 frontend/src/i18n/locales/de/app.json create mode 100644 frontend/src/i18n/locales/de/cognito.json create mode 100644 frontend/src/i18n/locales/de/notices.json create mode 100644 frontend/src/i18n/locales/en/app.json rename frontend/src/{cognito/locales/en/translations.json => i18n/locales/en/cognito.json} (100%) create mode 100644 frontend/src/i18n/locales/en/notices.json create mode 100644 frontend/src/i18n/locales/es/app.json create mode 100644 frontend/src/i18n/locales/es/cognito.json create mode 100644 frontend/src/i18n/locales/es/notices.json create mode 100644 frontend/src/i18n/locales/ja/app.json create mode 100644 frontend/src/i18n/locales/ja/cognito.json create mode 100644 frontend/src/i18n/locales/ja/notices.json diff --git a/frontend/package.json b/frontend/package.json index e9519e0f6..d5c35974a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -45,6 +45,7 @@ "humanize-duration": "^3.33.0", "i18next": "^22.5.0", "i18next-browser-languagedetector": "^7.0.1", + "i18next-resources-to-backend": "^1.2.1", "immer": "^9.0.21", "localforage": "^1.10.0", "lodash.debounce": "^4.0.8", diff --git a/frontend/src/cognito/components/AccountRecovery/AccountRecovery.tsx b/frontend/src/cognito/components/AccountRecovery/AccountRecovery.tsx index 8577a2bf7..95c3b6274 100644 --- a/frontend/src/cognito/components/AccountRecovery/AccountRecovery.tsx +++ b/frontend/src/cognito/components/AccountRecovery/AccountRecovery.tsx @@ -21,7 +21,7 @@ export function AccountRecovery({ email, fullWidth, }: AccountRecoveryProps): JSX.Element { - const { t } = useTranslation() + const { t } = useTranslation('cognito') const [error, setError] = useState(null) const [loading, setLoading] = useState(false) const [sentEmailVerifyRequest, setSentEmailVerifyRequest] = useState(false) @@ -164,7 +164,7 @@ export function AccountRecovery({ } function SupportRecoveryRequest(): JSX.Element { - const { t } = useTranslation() + const { t } = useTranslation('cognito') return ( diff --git a/frontend/src/cognito/components/AuthLayout/AuthLayout.tsx b/frontend/src/cognito/components/AuthLayout/AuthLayout.tsx index 513021c71..772942be6 100644 --- a/frontend/src/cognito/components/AuthLayout/AuthLayout.tsx +++ b/frontend/src/cognito/components/AuthLayout/AuthLayout.tsx @@ -16,7 +16,7 @@ export type AuthLayoutProps = { } export function AuthLayout({ children, i18nKey, showLogo, back, backLink, fullWidth }: AuthLayoutProps): JSX.Element { - const { t } = useTranslation() + const { t } = useTranslation('cognito') const history = useHistory() let logo: null | React.ReactElement = null diff --git a/frontend/src/cognito/components/ForgotPassword/ForgotPassword.tsx b/frontend/src/cognito/components/ForgotPassword/ForgotPassword.tsx index 6670b6095..1780ee707 100644 --- a/frontend/src/cognito/components/ForgotPassword/ForgotPassword.tsx +++ b/frontend/src/cognito/components/ForgotPassword/ForgotPassword.tsx @@ -23,7 +23,7 @@ export function ForgotPassword({ buttonKey, fullWidth, }: ForgotPasswordProps): JSX.Element { - const { t } = useTranslation() + const { t } = useTranslation('cognito') const history = useHistory() const [stateEmail, setEmail] = React.useState(email ? email : '') const [error, setError] = React.useState(null) diff --git a/frontend/src/cognito/components/MFACode/MFACode.tsx b/frontend/src/cognito/components/MFACode/MFACode.tsx index 19e6cdb0c..593154466 100644 --- a/frontend/src/cognito/components/MFACode/MFACode.tsx +++ b/frontend/src/cognito/components/MFACode/MFACode.tsx @@ -34,7 +34,7 @@ export function MFACode({ onSignInSuccess, cognitoUser, }: MFACodeProps): JSX.Element { - const { t } = useTranslation() + const { t } = useTranslation('cognito') const [localCognitoUser, setAuthUser] = useState(cognitoUser) const [code, setCode] = useState('') const [error, setError] = useState(null) diff --git a/frontend/src/cognito/components/PasswordStrengthInput/PasswordStrengthInput.tsx b/frontend/src/cognito/components/PasswordStrengthInput/PasswordStrengthInput.tsx index 3b65a0055..c200984cd 100644 --- a/frontend/src/cognito/components/PasswordStrengthInput/PasswordStrengthInput.tsx +++ b/frontend/src/cognito/components/PasswordStrengthInput/PasswordStrengthInput.tsx @@ -11,7 +11,7 @@ export type Props = { } export function PasswordStrengthInput({ isNewPassword, onChange }: Props): JSX.Element { - const { t } = useTranslation() + const { t } = useTranslation('cognito') const [password, setPassword] = useState('') const [passwordConfirmation, setPasswordConfirmation] = useState('') const [valid, setValid] = useState(false) diff --git a/frontend/src/cognito/components/PasswordVerify/PasswordVerify.tsx b/frontend/src/cognito/components/PasswordVerify/PasswordVerify.tsx index d3302d3be..611a4c0ef 100644 --- a/frontend/src/cognito/components/PasswordVerify/PasswordVerify.tsx +++ b/frontend/src/cognito/components/PasswordVerify/PasswordVerify.tsx @@ -14,7 +14,7 @@ export type PasswordVerifyProps = { } export function PasswordVerify({ onVerifyPasswordChange, email, fullWidth }: PasswordVerifyProps): JSX.Element { - const { t } = useTranslation() + const { t } = useTranslation('cognito') const history = useHistory() const location = useLocation() const [error, setError] = useState(null) diff --git a/frontend/src/cognito/components/SignIn/SignIn.tsx b/frontend/src/cognito/components/SignIn/SignIn.tsx index a06126302..3a52f902c 100644 --- a/frontend/src/cognito/components/SignIn/SignIn.tsx +++ b/frontend/src/cognito/components/SignIn/SignIn.tsx @@ -55,7 +55,7 @@ export function SignIn({ if (errorMessage) { externalError = new Error(errorMessage) } - const { t } = useTranslation() + const { t } = useTranslation('cognito') const history = useHistory() const [username, setUsername] = React.useState(email || rememberMe.username) const [password, setPassword] = React.useState('') diff --git a/frontend/src/cognito/components/SignInMessage/SignInMessage.tsx b/frontend/src/cognito/components/SignInMessage/SignInMessage.tsx index a68f95a26..1dc1c1f71 100644 --- a/frontend/src/cognito/components/SignInMessage/SignInMessage.tsx +++ b/frontend/src/cognito/components/SignInMessage/SignInMessage.tsx @@ -3,6 +3,6 @@ import { useTranslation } from 'react-i18next' import { LoadingMessage } from '../../../components/LoadingMessage' export function SignInMessage(): JSX.Element { - const { t } = useTranslation() + const { t } = useTranslation('cognito') return } diff --git a/frontend/src/cognito/components/SignUp/SignUp.tsx b/frontend/src/cognito/components/SignUp/SignUp.tsx index b5ef54097..c25461375 100644 --- a/frontend/src/cognito/components/SignUp/SignUp.tsx +++ b/frontend/src/cognito/components/SignUp/SignUp.tsx @@ -18,7 +18,7 @@ export type SignUpProps = { } export function SignUp({ onSignUp, onResend, hideCaptcha, fullWidth }: SignUpProps): JSX.Element { - const { t } = useTranslation() + const { t } = useTranslation('cognito') const history = useHistory() const params = useParams<{ email?: string }>() const [error, setError] = useState(null) diff --git a/frontend/src/cognito/components/SignUpVerify/SignUpVerify.tsx b/frontend/src/cognito/components/SignUpVerify/SignUpVerify.tsx index a7f1a2c5b..9cdd6d421 100644 --- a/frontend/src/cognito/components/SignUpVerify/SignUpVerify.tsx +++ b/frontend/src/cognito/components/SignUpVerify/SignUpVerify.tsx @@ -16,7 +16,7 @@ export type SignUpVerifyProps = { export function SignUpVerify({ email, onResend, fullWidth }: SignUpVerifyProps): JSX.Element { const [sending, setSending] = React.useState(false) const dispatch = useDispatch() - const { t } = useTranslation() + const { t } = useTranslation('cognito') return ( diff --git a/frontend/src/cognito/i18n.ts b/frontend/src/cognito/i18n.ts index b087b8f0f..71ae166e2 100644 --- a/frontend/src/cognito/i18n.ts +++ b/frontend/src/cognito/i18n.ts @@ -1,18 +1,6 @@ -import i18n from 'i18next' -import LanguageDetector from 'i18next-browser-languagedetector' -import enTranslation from './locales/en/translations.json' -import { initReactI18next } from 'react-i18next' - -i18n - .use(LanguageDetector) - .use(initReactI18next) - .init({ - lng: 'en', - fallbackLng: 'en', - debug: false, - interpolation: { escapeValue: false }, - react: { useSuspense: false }, - resources: { en: { translation: enTranslation } }, - }) +// The auth (cognito) screens now share the app-wide i18next instance. Their +// strings live in the `cognito` namespace (frontend/src/i18n/locales/*/cognito.json). +// Kept as a re-export so existing imports (e.g. Wrapper.tsx) keep working. +import i18n from '../i18n' export default i18n diff --git a/frontend/src/i18n/i18next.d.ts b/frontend/src/i18n/i18next.d.ts new file mode 100644 index 000000000..07e03ff4e --- /dev/null +++ b/frontend/src/i18n/i18next.d.ts @@ -0,0 +1,13 @@ +import 'i18next' + +// Only the default namespace is declared. Full `resources` key-typing was +// evaluated but rejected: it forces t() return types into a union of the +// catalog's value shapes (strings, arrays, nested objects), which breaks the +// many existing call sites that assume `t()` returns a string and the dynamic +// (computed) keys used for notices and API-enum mapping. Key correctness and +// en↔ja↔de↔es parity are enforced by the CI catalog-check script instead. +declare module 'i18next' { + interface CustomTypeOptions { + defaultNS: 'app' + } +} diff --git a/frontend/src/i18n/index.ts b/frontend/src/i18n/index.ts new file mode 100644 index 000000000..6be6c62cd --- /dev/null +++ b/frontend/src/i18n/index.ts @@ -0,0 +1,45 @@ +import i18n from 'i18next' +import resourcesToBackend from 'i18next-resources-to-backend' +import { initReactI18next } from 'react-i18next' +import enApp from './locales/en/app.json' +import enNotices from './locales/en/notices.json' +import enCognito from './locales/en/cognito.json' + +// Languages the app ships translations for. English is always the source/fallback. +// Endonyms are shown untranslated in the language picker. +export const SUPPORTED_LANGUAGES: { value: string; label: string }[] = [ + { value: 'en', label: 'English' }, + { value: 'ja', label: '日本語' }, + { value: 'de', label: 'Deutsch' }, + { value: 'es', label: 'Español' }, +] + +export const NAMESPACES = ['app', 'notices', 'cognito'] as const + +// English is bundled eagerly so there is never a flash of translation keys while +// falling back. Other locales are code-split and lazy-loaded on first use. +const lazyBackend = resourcesToBackend( + (language: string, namespace: string) => import(`./locales/${language}/${namespace}.json`) +) + +i18n + .use(lazyBackend) + .use(initReactI18next) + .init({ + // The resolved language is driven by redux (ui.setLanguage); 'en' here is a + // safe boot value overwritten before first paint. + lng: 'en', + fallbackLng: 'en', + ns: NAMESPACES as unknown as string[], + defaultNS: 'app', + fallbackNS: 'app', + debug: false, + interpolation: { escapeValue: false }, + react: { useSuspense: false }, + partialBundledLanguages: true, + resources: { + en: { app: enApp, notices: enNotices, cognito: enCognito }, + }, + }) + +export default i18n diff --git a/frontend/src/i18n/locales/de/app.json b/frontend/src/i18n/locales/de/app.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/frontend/src/i18n/locales/de/app.json @@ -0,0 +1 @@ +{} diff --git a/frontend/src/i18n/locales/de/cognito.json b/frontend/src/i18n/locales/de/cognito.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/frontend/src/i18n/locales/de/cognito.json @@ -0,0 +1 @@ +{} diff --git a/frontend/src/i18n/locales/de/notices.json b/frontend/src/i18n/locales/de/notices.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/frontend/src/i18n/locales/de/notices.json @@ -0,0 +1 @@ +{} diff --git a/frontend/src/i18n/locales/en/app.json b/frontend/src/i18n/locales/en/app.json new file mode 100644 index 000000000..1558d9a9f --- /dev/null +++ b/frontend/src/i18n/locales/en/app.json @@ -0,0 +1,8 @@ +{ + "options": { + "language": { + "label": "Language", + "system": "Same as system" + } + } +} diff --git a/frontend/src/cognito/locales/en/translations.json b/frontend/src/i18n/locales/en/cognito.json similarity index 100% rename from frontend/src/cognito/locales/en/translations.json rename to frontend/src/i18n/locales/en/cognito.json diff --git a/frontend/src/i18n/locales/en/notices.json b/frontend/src/i18n/locales/en/notices.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/frontend/src/i18n/locales/en/notices.json @@ -0,0 +1 @@ +{} diff --git a/frontend/src/i18n/locales/es/app.json b/frontend/src/i18n/locales/es/app.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/frontend/src/i18n/locales/es/app.json @@ -0,0 +1 @@ +{} diff --git a/frontend/src/i18n/locales/es/cognito.json b/frontend/src/i18n/locales/es/cognito.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/frontend/src/i18n/locales/es/cognito.json @@ -0,0 +1 @@ +{} diff --git a/frontend/src/i18n/locales/es/notices.json b/frontend/src/i18n/locales/es/notices.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/frontend/src/i18n/locales/es/notices.json @@ -0,0 +1 @@ +{} diff --git a/frontend/src/i18n/locales/ja/app.json b/frontend/src/i18n/locales/ja/app.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/frontend/src/i18n/locales/ja/app.json @@ -0,0 +1 @@ +{} diff --git a/frontend/src/i18n/locales/ja/cognito.json b/frontend/src/i18n/locales/ja/cognito.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/frontend/src/i18n/locales/ja/cognito.json @@ -0,0 +1 @@ +{} diff --git a/frontend/src/i18n/locales/ja/notices.json b/frontend/src/i18n/locales/ja/notices.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/frontend/src/i18n/locales/ja/notices.json @@ -0,0 +1 @@ +{} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 51bf67ce2..9083780e4 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -13,6 +13,7 @@ import { Provider } from 'react-redux' import { Layout } from './components/Layout' import heartbeat from './services/Heartbeat' import analytics from './services/analytics' +import './i18n' import './initializeCommon' import './services/Controller' diff --git a/package-lock.json b/package-lock.json index 832c6777e..783ca7f5f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -202,6 +202,7 @@ "humanize-duration": "^3.33.0", "i18next": "^22.5.0", "i18next-browser-languagedetector": "^7.0.1", + "i18next-resources-to-backend": "^1.2.1", "immer": "^9.0.21", "localforage": "^1.10.0", "lodash.debounce": "^4.0.8", @@ -12387,6 +12388,15 @@ "@babel/runtime": "^7.23.2" } }, + "node_modules/i18next-resources-to-backend": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/i18next-resources-to-backend/-/i18next-resources-to-backend-1.2.1.tgz", + "integrity": "sha512-okHbVA+HZ7n1/76MsfhPqDou0fptl2dAlhRDu2ideXloRRduzHsqDOznJBef+R3DFZnbvWoBW+KxJ7fnFjd6Yw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, "node_modules/iconv-corefoundation": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", From 3cfffc036b10adecd514ac8f8c6968afc55c278a Mon Sep 17 00:00:00 2001 From: Jamie Ruderman Date: Wed, 22 Jul 2026 13:16:30 -0700 Subject: [PATCH 2/9] feat(i18n): language preference in ui model with OS auto-detect and Electron sync --- frontend/src/i18n/index.ts | 14 ++++++++++++++ frontend/src/models/ui.ts | 16 ++++++++++++++++ types.d.ts | 1 + 3 files changed, 31 insertions(+) diff --git a/frontend/src/i18n/index.ts b/frontend/src/i18n/index.ts index 6be6c62cd..5066201b1 100644 --- a/frontend/src/i18n/index.ts +++ b/frontend/src/i18n/index.ts @@ -16,6 +16,20 @@ export const SUPPORTED_LANGUAGES: { value: string; label: string }[] = [ export const NAMESPACES = ['app', 'notices', 'cognito'] as const +export type LanguageMode = 'system' | 'en' | 'ja' | 'de' | 'es' + +const SUPPORTED_CODES = SUPPORTED_LANGUAGES.map(l => l.value) + +// Resolve a stored language preference to a concrete, supported code. 'system' +// (and any unknown value) follows the OS/browser/device language via +// navigator.language, degrading region tags (ja-JP -> ja) and falling back to +// English. Called fresh on every launch so OS language changes are picked up. +export function resolveLanguage(mode?: LanguageMode | string): string { + if (mode && mode !== 'system' && SUPPORTED_CODES.includes(mode)) return mode + const base = (navigator.language || 'en').toLowerCase().split('-')[0] + return SUPPORTED_CODES.includes(base) ? base : 'en' +} + // English is bundled eagerly so there is never a flash of translation keys while // falling back. Other locales are code-split and lazy-loaded on first use. const lazyBackend = resourcesToBackend( diff --git a/frontend/src/models/ui.ts b/frontend/src/models/ui.ts index 57faf8629..04d4c8cc1 100644 --- a/frontend/src/models/ui.ts +++ b/frontend/src/models/ui.ts @@ -3,6 +3,7 @@ import { emit } from '../services/Controller' import { StatusBar, Style } from '@capacitor/status-bar' import { RootModel } from '.' import { isDarkMode } from '../styling/theme' +import i18n, { resolveLanguage, LanguageMode } from '../i18n' import { NoticeProps } from '../components/Notice' import { createModel } from '@rematch/core' import { SIDEBAR_WIDTH } from '../constants' @@ -19,6 +20,7 @@ const SAVED_ACROSS_LOGOUT = [ 'poppedBubbles', 'expireBubbles', 'themeMode', + 'language', 'accordion', 'drawerMenu', 'drawerAccordion', @@ -39,6 +41,7 @@ const SAVED_ACROSS_LOGOUT = [ export type UIState = { themeMode: 'light' | 'dark' | 'system' themeDark: boolean + language: LanguageMode testUI?: 'ON' | 'HIGHLIGHT' apis: { switchApi?: IPreferences['switchApi'] @@ -117,6 +120,7 @@ export type UIState = { export const defaultState: UIState = { themeMode: 'system', themeDark: isDarkMode(), + language: 'system', testUI: undefined, apis: {}, layout: { @@ -238,6 +242,7 @@ export default createModel()({ states = migrateColumnStates(states) dispatch.ui.set(states) dispatch.ui.setTheme(states.themeMode) + dispatch.ui.setLanguage(states.language) }, async setupUpdated(count: number, state) { if (count !== state.ui.setupServicesCount) { @@ -252,6 +257,17 @@ export default createModel()({ dispatch.ui.set({ themeDark }) if (browser.isAndroid) StatusBar.setStyle({ style: themeDark ? Style.Dark : Style.Light }) }, + async setLanguage(language: UIState['language'] | void, state) { + language = language || state.ui.language + const resolved = resolveLanguage(language) + dispatch.ui.setPersistent({ language }) + if (i18n.resolvedLanguage !== resolved) i18n.changeLanguage(resolved) + // Sync the resolved code to the Electron main process (tray/menu/dialogs). + if (browser.isElectron) { + const { preferences } = state.backend + if (preferences.language !== resolved) emit('preferences', { ...preferences, language: resolved }) + } + }, async resizeColumn(params: { id: string; width: number }, state) { const columnWidths = { ...state.ui.columnWidths, [params.id]: params.width } console.log('SET COLUMN WIDTHS', columnWidths) diff --git a/types.d.ts b/types.d.ts index 977ed4dde..f5f0dcf77 100644 --- a/types.d.ts +++ b/types.d.ts @@ -1018,6 +1018,7 @@ declare global { windowState?: { x?: number; y?: number; width: number; height: number } disableDeepLinks?: boolean sshConfig?: boolean + language?: string // resolved language code (e.g. 'en', 'ja') for the Electron main process; never 'system' } type SegmentContext = { From 43884d62f59dfaabd012f412d6aa5eef19425e40 Mon Sep 17 00:00:00 2001 From: Jamie Ruderman Date: Wed, 22 Jul 2026 13:19:47 -0700 Subject: [PATCH 3/9] feat(i18n): add language picker to OptionsPage with endonyms --- frontend/src/i18n/locales/de/app.json | 9 ++++++++- frontend/src/i18n/locales/es/app.json | 9 ++++++++- frontend/src/i18n/locales/ja/app.json | 9 ++++++++- frontend/src/pages/OptionsPage.tsx | 14 ++++++++++++++ 4 files changed, 38 insertions(+), 3 deletions(-) diff --git a/frontend/src/i18n/locales/de/app.json b/frontend/src/i18n/locales/de/app.json index 0967ef424..b19644cc3 100644 --- a/frontend/src/i18n/locales/de/app.json +++ b/frontend/src/i18n/locales/de/app.json @@ -1 +1,8 @@ -{} +{ + "options": { + "language": { + "label": "Sprache", + "system": "Wie das System" + } + } +} diff --git a/frontend/src/i18n/locales/es/app.json b/frontend/src/i18n/locales/es/app.json index 0967ef424..1a04ea74c 100644 --- a/frontend/src/i18n/locales/es/app.json +++ b/frontend/src/i18n/locales/es/app.json @@ -1 +1,8 @@ -{} +{ + "options": { + "language": { + "label": "Idioma", + "system": "Igual que el sistema" + } + } +} diff --git a/frontend/src/i18n/locales/ja/app.json b/frontend/src/i18n/locales/ja/app.json index 0967ef424..0b0c8c2ed 100644 --- a/frontend/src/i18n/locales/ja/app.json +++ b/frontend/src/i18n/locales/ja/app.json @@ -1 +1,8 @@ -{} +{ + "options": { + "language": { + "label": "言語", + "system": "システムに合わせる" + } + } +} diff --git a/frontend/src/pages/OptionsPage.tsx b/frontend/src/pages/OptionsPage.tsx index 7a635f8a3..38379db38 100644 --- a/frontend/src/pages/OptionsPage.tsx +++ b/frontend/src/pages/OptionsPage.tsx @@ -3,6 +3,8 @@ import browser from '../services/browser' import { emit } from '../services/Controller' import { State, Dispatch } from '../store' import { List, Typography } from '@mui/material' +import { useTranslation } from 'react-i18next' +import { SUPPORTED_LANGUAGES } from '../i18n' import { useSelector, useDispatch } from 'react-redux' import { SettingsDisableNetworkItem } from '../components/SettingsDisableNetworkItem' import { AccordionMenuItem } from '../components/AccordionMenuItem' @@ -26,7 +28,9 @@ export const OptionsPage: React.FC = () => { (state: State) => !!state.backend.thisId && !selectOwnDevices(state).find(d => d.thisDevice) ) const themeMode = useSelector((state: State) => state.ui.themeMode) + const language = useSelector((state: State) => state.ui.language) const remoteUI = useSelector((state: State) => isRemoteUI(state)) + const { t } = useTranslation() const { binaries, ui } = useDispatch() @@ -52,6 +56,16 @@ export const OptionsPage: React.FC = () => { ]} onChange={e => ui.setTheme(e.target.value as State['ui']['themeMode'])} /> + ui.setLanguage(e.target.value as State['ui']['language'])} + /> {browser.isRemote && ( Date: Wed, 22 Jul 2026 13:24:57 -0700 Subject: [PATCH 4/9] feat(i18n): localize Electron tray, menu and dialogs with preference sync --- electron/src/ElectronApp.ts | 12 +- electron/src/TrayMenu.ts | 36 +- electron/src/i18n.ts | 32 + electron/src/locales/de.json | 25 + electron/src/locales/en.json | 25 + electron/src/locales/es.json | 25 + electron/src/locales/ja.json | 25 + frontend/i18next-parser.config.js | 25 + frontend/package.json | 5 +- frontend/scripts/i18n-check.mjs | 60 + frontend/src/i18n/locales/de/cognito.json | 119 +- frontend/src/i18n/locales/es/cognito.json | 119 +- frontend/src/i18n/locales/ja/cognito.json | 119 +- package-lock.json | 1390 ++++++++++++++++++++- 14 files changed, 1956 insertions(+), 61 deletions(-) create mode 100644 electron/src/i18n.ts create mode 100644 electron/src/locales/de.json create mode 100644 electron/src/locales/en.json create mode 100644 electron/src/locales/es.json create mode 100644 electron/src/locales/ja.json create mode 100644 frontend/i18next-parser.config.js create mode 100644 frontend/scripts/i18n-check.mjs diff --git a/electron/src/ElectronApp.ts b/electron/src/ElectronApp.ts index 6c9059389..f23b110e4 100644 --- a/electron/src/ElectronApp.ts +++ b/electron/src/ElectronApp.ts @@ -2,6 +2,7 @@ import electron, { Menu, dialog } from 'electron' import path from 'path' import AutoUpdater from './AutoUpdater' import TrayMenu from './TrayMenu' +import { t, setLanguage } from './i18n' import { EVENTS, PROTOCOL, @@ -35,6 +36,7 @@ export default class ElectronApp { this.isMaximized = false this.autoUpdater = new AutoUpdater() this.protocol = PROTOCOL.substring(0, PROTOCOL.length - 3) + setLanguage(preferences.get().language) if (!this.app.requestSingleInstanceLock()) { Logger.warn('ANOTHER APP INSTANCE IS RUNNING. EXITING.') @@ -79,8 +81,8 @@ export default class ElectronApp { if (!this.quitSelected && !this.errorShown) { this.errorShown = true dialog.showErrorBox( - `${brand.appName} Failed to Start`, - `The app could not start because another instance is already running. Please close any other ${brand.appName} processes, or restart your computer.` + t('dialog.failedToStartTitle', { appName: brand.appName }), + t('dialog.failedToStartMessage', { appName: brand.appName }) ) } @@ -166,9 +168,9 @@ export default class ElectronApp { if (!this.window) return const result = await dialog.showOpenDialog(this.window, { - title: 'Find application', - message: 'Select the application location', - buttonLabel: 'Select', + title: t('dialog.findApplicationTitle'), + message: t('dialog.findApplicationMessage'), + buttonLabel: t('dialog.findApplicationButton'), }) let filePath = result?.filePaths[0] diff --git a/electron/src/TrayMenu.ts b/electron/src/TrayMenu.ts index 1171b4167..628beb9a3 100644 --- a/electron/src/TrayMenu.ts +++ b/electron/src/TrayMenu.ts @@ -11,7 +11,9 @@ import headless, { getApplication, Logger, brand, + preferences, } from './backend' +import { t, setLanguage } from './i18n' const MAX_MENU_SIZE = 10 const MAX_UPDATE_CHECKS = 16 @@ -37,11 +39,15 @@ export default class TrayMenu { }) } + setLanguage(preferences.get().language) this.render() EventBus.on(User.EVENTS.signedIn, this.render) EventBus.on(User.EVENTS.signedOut, this.render) EventBus.on(ConnectionPool.EVENTS.pool, this.updatePool) + EventBus.on(EVENTS.preferences, ({ language }: IPreferences) => { + if (setLanguage(language)) this.render() + }) } private render = () => { @@ -59,7 +65,7 @@ export default class TrayMenu { private remoteitMenu() { return [ { - label: `Open ${brand.appName}...`, + label: t('tray.open', { appName: brand.appName }), type: 'normal', click: () => this.handleOpen(), }, @@ -67,12 +73,12 @@ export default class TrayMenu { label: user.username, submenu: [ { - label: 'Sign out', + label: t('tray.signOut'), type: 'normal', click: () => EventBus.emit(EVENTS.signOut), }, { - label: 'Quit', + label: t('tray.quit'), type: 'normal', click: electron.app.quit, }, @@ -86,8 +92,8 @@ export default class TrayMenu { private connectionsMenu() { let menu = [] const enabled = this.pool.filter(c => c.enabled) - if (enabled.length) menu.push({ label: 'Connections', enabled: false }, ...this.connectionsList(enabled)) - return menu.length ? menu : [{ label: 'No connections', enabled: false }] + if (enabled.length) menu.push({ label: t('tray.connections'), enabled: false }, ...this.connectionsList(enabled)) + return menu.length ? menu : [{ label: t('tray.noConnections'), enabled: false }] } private connectionsList(list: IConnection[]) { @@ -104,23 +110,23 @@ export default class TrayMenu { submenu: [ connection.enabled ? connection.connected - ? { label: 'Stop connection', click: () => this.disconnect(connection) } - : { label: 'Remove from network', click: () => this.remove(connection) } + ? { label: t('tray.stopConnection'), click: () => this.disconnect(connection) } + : { label: t('tray.removeFromNetwork'), click: () => this.remove(connection) } : connection.online - ? { label: 'Add to network', click: () => this.connect(connection) } - : { label: 'Offline', enabled: false }, + ? { label: t('tray.addToNetwork'), click: () => this.connect(connection) } + : { label: t('tray.offline'), enabled: false }, { type: 'separator' }, { label: hostName(connection), enabled: false }, - { label: 'Copy to clipboard', click: () => this.copy(connection) }, + { label: t('tray.copyToClipboard'), click: () => this.copy(connection) }, connection.online - ? { label: 'Launch', enabled: connection.enabled, click: () => this.launch(connection) } - : { label: 'Remove', click: () => EventBus.emit(EVENTS.clear, connection) }, + ? { label: t('tray.launch'), enabled: connection.enabled, click: () => this.launch(connection) } + : { label: t('tray.remove'), click: () => EventBus.emit(EVENTS.clear, connection) }, ], }) } return result }, []) - if (more) menu.push({ label: `and ${more} more...`, click: () => this.handleOpen('connections') }) + if (more) menu.push({ label: t('tray.andMore', { count: more }), click: () => this.handleOpen('connections') }) return menu } @@ -128,12 +134,12 @@ export default class TrayMenu { return [ { label: brand.appName, enabled: false }, { - label: 'Sign in...', + label: t('tray.signIn'), type: 'normal', click: () => this.handleOpen(), }, { - label: 'Quit', + label: t('tray.quit'), type: 'normal', click: electron.app.quit, }, diff --git a/electron/src/i18n.ts b/electron/src/i18n.ts new file mode 100644 index 000000000..2dd6350d1 --- /dev/null +++ b/electron/src/i18n.ts @@ -0,0 +1,32 @@ +// Minimal i18n for the Electron main process (tray, menus, native dialogs). +// The renderer owns language selection and syncs the resolved code to the main +// process via the `language` field on IPreferences (see EVENTS.preferences). +// A full i18next instance is unnecessary here — this is a handful of flat strings. +import en from './locales/en.json' +import ja from './locales/ja.json' +import de from './locales/de.json' +import es from './locales/es.json' + +type Bundle = typeof en +const BUNDLES: { [code: string]: Bundle } = { en, ja, de, es } +const FALLBACK = 'en' + +let current = FALLBACK + +export function setLanguage(code?: string) { + const resolved = code && BUNDLES[code] ? code : FALLBACK + const changed = resolved !== current + current = resolved + return changed +} + +// Dotted key lookup with {{var}} interpolation, falling back to English then the key. +export function t(key: string, vars?: { [k: string]: string | number }): string { + const lookup = (bundle: Bundle) => + key.split('.').reduce((node, part) => (node == null ? undefined : node[part]), bundle) + let value = lookup(BUNDLES[current]) + if (typeof value !== 'string') value = lookup(BUNDLES[FALLBACK]) + if (typeof value !== 'string') return key + if (vars) for (const [k, v] of Object.entries(vars)) value = value.replace(new RegExp(`{{\\s*${k}\\s*}}`, 'g'), String(v)) + return value +} diff --git a/electron/src/locales/de.json b/electron/src/locales/de.json new file mode 100644 index 000000000..0e1cb0fa2 --- /dev/null +++ b/electron/src/locales/de.json @@ -0,0 +1,25 @@ +{ + "tray": { + "open": "{{appName}} öffnen...", + "signOut": "Abmelden", + "quit": "Beenden", + "connections": "Verbindungen", + "noConnections": "Keine Verbindungen", + "stopConnection": "Verbindung trennen", + "removeFromNetwork": "Aus Netzwerk entfernen", + "addToNetwork": "Zum Netzwerk hinzufügen", + "offline": "Offline", + "copyToClipboard": "In Zwischenablage kopieren", + "launch": "Starten", + "remove": "Entfernen", + "andMore": "und {{count}} weitere...", + "signIn": "Anmelden..." + }, + "dialog": { + "failedToStartTitle": "{{appName}} konnte nicht gestartet werden", + "failedToStartMessage": "Die App konnte nicht gestartet werden, da bereits eine andere Instanz läuft. Bitte schließen Sie alle anderen {{appName}}-Prozesse oder starten Sie Ihren Computer neu.", + "findApplicationTitle": "Anwendung suchen", + "findApplicationMessage": "Wählen Sie den Speicherort der Anwendung", + "findApplicationButton": "Auswählen" + } +} diff --git a/electron/src/locales/en.json b/electron/src/locales/en.json new file mode 100644 index 000000000..ec7ba2b58 --- /dev/null +++ b/electron/src/locales/en.json @@ -0,0 +1,25 @@ +{ + "tray": { + "open": "Open {{appName}}...", + "signOut": "Sign out", + "quit": "Quit", + "connections": "Connections", + "noConnections": "No connections", + "stopConnection": "Stop connection", + "removeFromNetwork": "Remove from network", + "addToNetwork": "Add to network", + "offline": "Offline", + "copyToClipboard": "Copy to clipboard", + "launch": "Launch", + "remove": "Remove", + "andMore": "and {{count}} more...", + "signIn": "Sign in..." + }, + "dialog": { + "failedToStartTitle": "{{appName}} Failed to Start", + "failedToStartMessage": "The app could not start because another instance is already running. Please close any other {{appName}} processes, or restart your computer.", + "findApplicationTitle": "Find application", + "findApplicationMessage": "Select the application location", + "findApplicationButton": "Select" + } +} diff --git a/electron/src/locales/es.json b/electron/src/locales/es.json new file mode 100644 index 000000000..94898bf51 --- /dev/null +++ b/electron/src/locales/es.json @@ -0,0 +1,25 @@ +{ + "tray": { + "open": "Abrir {{appName}}...", + "signOut": "Cerrar sesión", + "quit": "Salir", + "connections": "Conexiones", + "noConnections": "Sin conexiones", + "stopConnection": "Detener conexión", + "removeFromNetwork": "Quitar de la red", + "addToNetwork": "Agregar a la red", + "offline": "Sin conexión", + "copyToClipboard": "Copiar al portapapeles", + "launch": "Iniciar", + "remove": "Quitar", + "andMore": "y {{count}} más...", + "signIn": "Iniciar sesión..." + }, + "dialog": { + "failedToStartTitle": "No se pudo iniciar {{appName}}", + "failedToStartMessage": "La aplicación no pudo iniciarse porque ya hay otra instancia en ejecución. Cierre cualquier otro proceso de {{appName}} o reinicie su equipo.", + "findApplicationTitle": "Buscar aplicación", + "findApplicationMessage": "Seleccione la ubicación de la aplicación", + "findApplicationButton": "Seleccionar" + } +} diff --git a/electron/src/locales/ja.json b/electron/src/locales/ja.json new file mode 100644 index 000000000..0ba8febdb --- /dev/null +++ b/electron/src/locales/ja.json @@ -0,0 +1,25 @@ +{ + "tray": { + "open": "{{appName}}を開く...", + "signOut": "サインアウト", + "quit": "終了", + "connections": "接続", + "noConnections": "接続なし", + "stopConnection": "接続を停止", + "removeFromNetwork": "ネットワークから削除", + "addToNetwork": "ネットワークに追加", + "offline": "オフライン", + "copyToClipboard": "クリップボードにコピー", + "launch": "起動", + "remove": "削除", + "andMore": "他{{count}}件...", + "signIn": "サインイン..." + }, + "dialog": { + "failedToStartTitle": "{{appName}}を起動できませんでした", + "failedToStartMessage": "別のインスタンスがすでに実行されているため、アプリを起動できませんでした。他の{{appName}}プロセスを終了するか、コンピューターを再起動してください。", + "findApplicationTitle": "アプリケーションを検索", + "findApplicationMessage": "アプリケーションの場所を選択してください", + "findApplicationButton": "選択" + } +} diff --git a/frontend/i18next-parser.config.js b/frontend/i18next-parser.config.js new file mode 100644 index 000000000..fd4ffbba5 --- /dev/null +++ b/frontend/i18next-parser.config.js @@ -0,0 +1,25 @@ +// Extracts t()/ keys from source into the catalogs so translators always +// have an up-to-date source of truth. Run: npm run i18n:extract +// +// Convention: give every key its English text as the inline default, e.g. +// t('options.language.label', 'Language') +// The parser writes that default into the English catalog and leaves ja/de/es +// empty for translators. `npm run i18n:check` fails CI on any missing key or +// empty English value. +export default { + locales: ['en', 'ja', 'de', 'es'], + defaultNamespace: 'app', + namespaceSeparator: ':', + keySeparator: '.', + input: ['src/**/*.{ts,tsx}'], + output: 'src/i18n/locales/$LOCALE/$NAMESPACE.json', + sort: true, + keepRemoved: false, + // English keeps the inline default text; other locales stay empty until translated. + defaultValue: (locale, _ns, _key, value) => (locale === 'en' ? value?.usageContext?.defaultValue ?? '' : ''), + createOldCatalogs: false, + lexers: { + ts: ['JavascriptLexer'], + tsx: ['JsxLexer'], + }, +} diff --git a/frontend/package.json b/frontend/package.json index d5c35974a..1716d51e9 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,7 +6,9 @@ "scripts": { "start": "vite --port 3003 --host 0.0.0.0 --open", "build": "cross-env NODE_OPTIONS='--max-old-space-size=4096' vite build", - "typecheck": "tsc --noEmit" + "typecheck": "tsc --noEmit", + "i18n:extract": "i18next -c i18next-parser.config.js", + "i18n:check": "node scripts/i18n-check.mjs" }, "dependencies": { "@airbrake/browser": "^2.1.9", @@ -93,6 +95,7 @@ "@types/validator": "^13.12.2", "@vitejs/plugin-react": "^4.3.2", "eslint": "^8.53.0", + "i18next-parser": "^9.4.0", "typescript": "^5.9.2", "vite": "^6.4.3" }, diff --git a/frontend/scripts/i18n-check.mjs b/frontend/scripts/i18n-check.mjs new file mode 100644 index 000000000..19d57ec13 --- /dev/null +++ b/frontend/scripts/i18n-check.mjs @@ -0,0 +1,60 @@ +#!/usr/bin/env node +// Verifies translation-catalog health across all supported locales. +// Fails (exit 1) when: +// - a non-English catalog is missing a key that English has +// - a non-English catalog has a key English no longer has (dead key) +// - an English value is empty (extracted but no source text supplied) +// Run: npm run i18n:check (also used in CI) +import { readFileSync, readdirSync } from 'node:fs' +import { join, dirname } from 'node:path' +import { fileURLToPath } from 'node:url' + +const root = join(dirname(fileURLToPath(import.meta.url)), '..', 'src', 'i18n', 'locales') +const SOURCE = 'en' +const locales = readdirSync(root, { withFileTypes: true }) + .filter(d => d.isDirectory()) + .map(d => d.name) +const namespaces = readdirSync(join(root, SOURCE)) + .filter(f => f.endsWith('.json')) + .map(f => f.replace(/\.json$/, '')) + +const load = (locale, ns) => JSON.parse(readFileSync(join(root, locale, `${ns}.json`), 'utf8')) + +// Flatten nested keys to dotted paths, keeping i18next plural suffixes intact. +const flatten = (obj, prefix = '', out = {}) => { + for (const [k, v] of Object.entries(obj)) { + const key = prefix ? `${prefix}.${k}` : k + if (v && typeof v === 'object' && !Array.isArray(v)) flatten(v, key, out) + else out[key] = v + } + return out +} + +let errors = 0 +const report = msg => { + errors++ + console.error(` ✗ ${msg}`) +} + +for (const ns of namespaces) { + const source = flatten(load(SOURCE, ns)) + const sourceKeys = new Set(Object.keys(source)) + + for (const [key, value] of Object.entries(source)) { + if (typeof value === 'string' && value.trim() === '') report(`[${SOURCE}/${ns}] empty English value: "${key}"`) + } + + for (const locale of locales) { + if (locale === SOURCE) continue + const target = flatten(load(locale, ns)) + const targetKeys = new Set(Object.keys(target)) + for (const key of sourceKeys) if (!targetKeys.has(key)) report(`[${locale}/${ns}] missing key: "${key}"`) + for (const key of targetKeys) if (!sourceKeys.has(key)) report(`[${locale}/${ns}] dead key (not in ${SOURCE}): "${key}"`) + } +} + +if (errors) { + console.error(`\ni18n catalog check failed with ${errors} problem(s). Run "npm run i18n:extract" and fill in translations.`) + process.exit(1) +} +console.log(`i18n catalog check passed (${locales.length} locales, ${namespaces.length} namespaces).`) diff --git a/frontend/src/i18n/locales/de/cognito.json b/frontend/src/i18n/locales/de/cognito.json index 0967ef424..96f64452e 100644 --- a/frontend/src/i18n/locales/de/cognito.json +++ b/frontend/src/i18n/locales/de/cognito.json @@ -1 +1,118 @@ -{} +{ + "app-store-banner": { + "connect": "Verbinden Sie sich von überall mit unserer mobilen App.", + "unknown-os": "Jetzt verfügbar für iOS und Android." + }, + "global": { + "action": { + "continue": "Weiter", + "continue-signup": "Weiter zur Anmeldung" + }, + "actions": { + "cancel": "Abbrechen", + "signing-in": "Anmeldung läuft...", + "submit": "Absenden" + }, + "user": { + "email": "E-Mail", + "new-password-confirm-placeholder": "Geben Sie Ihr neues Passwort erneut ein", + "new-password-placeholder": "Geben Sie Ihr neues Passwort ein", + "password": "Passwort", + "password-confirm": "Passwort bestätigen", + "password-confirm-placeholder": "Geben Sie Ihr Passwort erneut ein", + "password-current": "Aktuelles Passwort", + "password-current-placeholder": "Geben Sie Ihr aktuelles Passwort ein", + "password-placeholder": "Geben Sie ein Passwort ein", + "username": "E-Mail oder Benutzername" + } + }, + "pages": { + "account-recovery": { + "account-recovery-code": "Wenn Sie nicht auf Ihr Mobilgerät zugreifen können, um den\n Zwei-Faktor-Bestätigungscode zu erhalten, können Sie einen Bestätigungscode verwenden,\n der an Ihre E-Mail-Adresse gesendet wird, sowie den Wiederherstellungscode,\n den Sie bei der Aktivierung der Zwei-Faktor-Authentifizierung erhalten haben.", + "description": "Wir haben einen Anmeldecode an Ihre E-Mail-Adresse gesendet. Geben Sie unten sowohl den Anmeldecode als auch den Wiederherstellungscode ein", + "email-recovery-code-placeholder": "Anmeldecode eingeben", + "lost-recovery-code": "Wenn Sie weder auf Ihr Mobilgerät noch auf Ihren Wiederherstellungscode zugreifen können, können Sie eine Zurücksetzung Ihrer Authentifizierungseinstellungen beantragen. Aus Sicherheitsgründen kann dies 1–3 Werktage dauern.", + "lost-recovery-code-button": "Kontozurücksetzung anfordern", + "lost-recovery-code-heading": "Mobilgerät und Wiederherstellungscodes verloren?", + "recovery-code-placeholder": "Wiederherstellungscode eingeben", + "start-recovery": "Wiederherstellung starten", + "title": "Zwei-Faktor-Kontowiederherstellung" + }, + "auth-mfa-totp": { + "invalid-code": "Ungültiger Code", + "title": "Zwei-Faktor-Authentifizierung" + }, + "auth-mfa": { + "errors": { + "BACKUP_CODE_INVALID": "Ungültiger Sicherungscode", + "EMAIL_CODE_INVALID": "Ungültiger E-Mail-Code" + }, + "having-problems": "Haben Sie Probleme?", + "mfa-verification-sent": "Ein Bestätigungscode wurde an Ihr Mobilgerät{{number}} gesendet. Dieser Code ist nur 3 Minuten lang gültig.", + "no-device-access": "Kein Zugriff auf Ihr Zwei-Faktor-Gerät", + "title": "SMS-Zwei-Faktor-Authentifizierung", + "totp-mfa-verification": "Geben Sie das von Ihrer Authentifizierungs-App generierte Einmalpasswort ein.", + "verification-code-placeholder": "Bestätigungscode eingeben" + }, + "auth": { + "create-account": { + "button-label": "Konto erstellen", + "button-loading": "Wird erstellt..." + } + }, + "forgot-password-verify": { + "password-error": { + "missing-lower": "Das Passwort muss einen Kleinbuchstaben enthalten.", + "missing-number": "Das Passwort muss eine Zahl enthalten.", + "missing-special-char": "Das Passwort muss mindestens eines der folgenden Zeichen enthalten ", + "missing-upper": "Das Passwort muss einen Großbuchstaben enthalten.", + "passwords-do-not-match": "Die Passwörter stimmen nicht überein", + "too-long": "Das Passwort darf höchstens {{max_length}} Zeichen lang sein.", + "too-short": "Das Passwort muss mindestens {{min_length}} Zeichen lang sein." + }, + "password-rules": "Passwörter müssen {{min_length}}–{{max_length}} Zeichen lang sein und eine Zahl, einen Groß- und einen Kleinbuchstaben sowie mindestens eines der folgenden Zeichen enthalten ", + "password-rules-reduced": "Passwörter müssen {{min_length}}–{{max_length}} Zeichen lang sein.", + "password-strength-html": "Passwortstärke: {{strength}}", + "password-strength-score": ["Sehr schwach", "Schwach", "Ausreichend", "Gut", "Stark"], + "success-message": "Ihr Passwort wurde zurückgesetzt. Bitte melden Sie sich an", + "title": "Konto bestätigen", + "verification-link-message": "Ein Bestätigungslink wurde an Sie per E-Mail gesendet. Klicken Sie auf den Link und kehren Sie dann hierher zurück. Klicken Sie anschließend auf „Weiter“, um sich anzumelden." + }, + "forgot-password": { + "email": "E-Mail-Adresse", + "reset-password": "Passwort zurücksetzen", + "title": "Passwort vergessen" + }, + "password-reset": { + "cognito-reset-password-required": "Aus Sicherheitsgründen müssen Benutzer ihr Passwort regelmäßig ändern. Ein Bestätigungscode wurde an {{email}} gesendet. Bitte prüfen Sie Ihre E-Mails, geben Sie den Bestätigungscode ein und aktualisieren Sie Ihr Passwort.", + "signout-all-text": "Aktuelle Sitzungen bleiben aktiv. Falls Sie von einem Sicherheitsvorfall betroffen waren, empfehlen wir, sich überall abzumelden. Um sich überall abzumelden, melden Sie sich bei app.remote.it an und rufen Sie dann die Konten-Seite auf, um sich überall abzumelden.", + "update-button": "Passwort aktualisieren", + "verification-code": "Bestätigungscode", + "verification-code-message": "Ein Bestätigungscode wurde an {{email}} gesendet. Bitte prüfen Sie Ihre E-Mails, geben Sie den Bestätigungscode ein und aktualisieren Sie Ihr Passwort." + }, + "sign-in": { + "create-account": "Noch kein Konto?", + "forgot-password": "Passwort vergessen?", + "or": "ODER", + "saml-link": "SAML-Anmeldung", + "sign-in": "Anmelden", + "sign-up-link": "Kostenlos registrieren!", + "signing-in": "Anmeldung läuft...", + "user-password-link": "Anmeldung mit Benutzername/Passwort" + }, + "sign-up": { + "title": "Registrieren" + }, + "update-password": { + "reset-password": "Passwort aktualisieren", + "title": "Passwort aktualisieren" + }, + "verify-account": { + "sending": "Wird gesendet...", + "resent-notice": "Bestätigungs-E-Mail erneut gesendet.", + "check-spam": "Überprüfen Sie Ihren Spam-Ordner", + "resend-confirmation": "Kontobestätigung erneut senden.", + "verification-received-message": "Keinen Bestätigungslink erhalten?" + } + } +} diff --git a/frontend/src/i18n/locales/es/cognito.json b/frontend/src/i18n/locales/es/cognito.json index 0967ef424..b81a5ad5b 100644 --- a/frontend/src/i18n/locales/es/cognito.json +++ b/frontend/src/i18n/locales/es/cognito.json @@ -1 +1,118 @@ -{} +{ + "app-store-banner": { + "connect": "Conéctate desde cualquier lugar con nuestra aplicación móvil.", + "unknown-os": "Ya disponible para iOS y Android." + }, + "global": { + "action": { + "continue": "Continuar", + "continue-signup": "Continuar para iniciar sesión" + }, + "actions": { + "cancel": "Cancelar", + "signing-in": "Iniciando sesión...", + "submit": "Enviar" + }, + "user": { + "email": "Correo electrónico", + "new-password-confirm-placeholder": "Vuelve a introducir tu nueva contraseña", + "new-password-placeholder": "Introduce tu nueva contraseña", + "password": "Contraseña", + "password-confirm": "Confirmar contraseña", + "password-confirm-placeholder": "Vuelve a introducir tu contraseña", + "password-current": "Contraseña actual", + "password-current-placeholder": "Introduce tu contraseña actual", + "password-placeholder": "Introduce una contraseña", + "username": "Correo electrónico o nombre de usuario" + } + }, + "pages": { + "account-recovery": { + "account-recovery-code": "Si no puedes acceder a tu dispositivo móvil para recibir el\n código de verificación de dos factores, puedes usar un código de verificación\n que se enviará a tu correo electrónico, junto con el código de recuperación\n proporcionado al activar la autenticación de dos factores.", + "description": "Te enviamos un código de inicio de sesión a tu correo electrónico. Introduce a continuación tanto el código de inicio de sesión como el código de recuperación", + "email-recovery-code-placeholder": "Introduce el código de inicio de sesión", + "lost-recovery-code": "Si no puedes acceder a tu dispositivo móvil ni a tu código de recuperación, puedes solicitar un restablecimiento de tu configuración de autenticación. Por motivos de seguridad, esto puede tardar de 1 a 3 días hábiles.", + "lost-recovery-code-button": "Solicitar restablecimiento de cuenta", + "lost-recovery-code-heading": "¿Perdiste tu dispositivo móvil y tus códigos de recuperación?", + "recovery-code-placeholder": "Introduce el código de recuperación", + "start-recovery": "Iniciar recuperación", + "title": "Recuperación de cuenta con doble factor" + }, + "auth-mfa-totp": { + "invalid-code": "Código no válido", + "title": "Autenticación de dos factores" + }, + "auth-mfa": { + "errors": { + "BACKUP_CODE_INVALID": "Código de respaldo no válido", + "EMAIL_CODE_INVALID": "Código de correo electrónico no válido" + }, + "having-problems": "¿Tienes problemas?", + "mfa-verification-sent": "Se ha enviado un código de verificación a tu dispositivo móvil{{number}}. Este código solo es válido durante 3 minutos.", + "no-device-access": "No puedo acceder a mi dispositivo de doble factor", + "title": "Autenticación de dos factores por SMS", + "totp-mfa-verification": "Introduce la contraseña de un solo uso generada por tu aplicación de autenticación.", + "verification-code-placeholder": "Introduce el código de verificación" + }, + "auth": { + "create-account": { + "button-label": "Crear cuenta", + "button-loading": "Creando..." + } + }, + "forgot-password-verify": { + "password-error": { + "missing-lower": "La contraseña debe contener una letra minúscula.", + "missing-number": "La contraseña debe contener un número.", + "missing-special-char": "La contraseña debe contener al menos uno de los siguientes caracteres ", + "missing-upper": "La contraseña debe contener una letra mayúscula.", + "passwords-do-not-match": "Las contraseñas no coinciden", + "too-long": "La contraseña no debe tener más de {{max_length}} caracteres.", + "too-short": "La contraseña debe tener al menos {{min_length}} caracteres." + }, + "password-rules": "Las contraseñas deben tener entre {{min_length}} y {{max_length}} caracteres, contener un número, una letra mayúscula y una minúscula, y al menos uno de los siguientes caracteres ", + "password-rules-reduced": "Las contraseñas deben tener entre {{min_length}} y {{max_length}} caracteres.", + "password-strength-html": "Seguridad de la contraseña: {{strength}}", + "password-strength-score": ["Muy débil", "Débil", "Aceptable", "Buena", "Fuerte"], + "success-message": "Tu contraseña se ha restablecido; inicia sesión", + "title": "Verifica tu cuenta", + "verification-link-message": "Te hemos enviado un enlace de verificación por correo electrónico. Haz clic en el enlace y vuelve aquí. Luego pulsa continuar para iniciar sesión." + }, + "forgot-password": { + "email": "Correo electrónico", + "reset-password": "Restablecer contraseña", + "title": "Olvidé mi contraseña" + }, + "password-reset": { + "cognito-reset-password-required": "Por motivos de seguridad, pedimos a los usuarios que cambien su contraseña periódicamente. Se ha enviado un código de verificación a {{email}}; revisa tu correo, introduce el código de verificación y actualiza tu contraseña.", + "signout-all-text": "Las sesiones actuales permanecerán activas. Si has sufrido una brecha de seguridad, se recomienda cerrar sesión en todos los dispositivos. Para cerrar sesión en todos los dispositivos, inicia sesión en app.remote.it y luego ve a la página de cuentas para cerrar sesión en todas partes.", + "update-button": "Actualizar contraseña", + "verification-code": "Código de verificación", + "verification-code-message": "Se ha enviado un código de verificación a {{email}}; revisa tu correo, introduce el código de verificación y actualiza tu contraseña." + }, + "sign-in": { + "create-account": "¿Aún no tienes una cuenta?", + "forgot-password": "¿Olvidaste tu contraseña?", + "or": "O", + "saml-link": "Inicio de sesión SAML", + "sign-in": "Iniciar sesión", + "sign-up-link": "¡Regístrate gratis!", + "signing-in": "Iniciando sesión...", + "user-password-link": "Inicio de sesión con usuario/contraseña" + }, + "sign-up": { + "title": "Registrarse" + }, + "update-password": { + "reset-password": "Actualizar contraseña", + "title": "Actualizar contraseña" + }, + "verify-account": { + "sending": "Enviando...", + "resent-notice": "Se ha reenviado el correo de verificación.", + "check-spam": "Revisa tu carpeta de spam", + "resend-confirmation": "Reenviar confirmación de cuenta.", + "verification-received-message": "¿No recibiste tu enlace de verificación?" + } + } +} diff --git a/frontend/src/i18n/locales/ja/cognito.json b/frontend/src/i18n/locales/ja/cognito.json index 0967ef424..54596f6fe 100644 --- a/frontend/src/i18n/locales/ja/cognito.json +++ b/frontend/src/i18n/locales/ja/cognito.json @@ -1 +1,118 @@ -{} +{ + "app-store-banner": { + "connect": "モバイルアプリを使えば、どこからでも接続できます。", + "unknown-os": "iOSおよびAndroidに対応しています。" + }, + "global": { + "action": { + "continue": "続ける", + "continue-signup": "続けてサインイン" + }, + "actions": { + "cancel": "キャンセル", + "signing-in": "サインインしています...", + "submit": "送信" + }, + "user": { + "email": "メールアドレス", + "new-password-confirm-placeholder": "新しいパスワードを再入力してください", + "new-password-placeholder": "新しいパスワードを入力してください", + "password": "パスワード", + "password-confirm": "パスワードの確認", + "password-confirm-placeholder": "パスワードを再入力してください", + "password-current": "現在のパスワード", + "password-current-placeholder": "現在のパスワードを入力してください", + "password-placeholder": "パスワードを入力してください", + "username": "メールアドレスまたはユーザー名" + } + }, + "pages": { + "account-recovery": { + "account-recovery-code": "モバイルデバイスで二要素認証コードを受け取れない場合は、\n メールに送信される確認コードと、二要素認証を\n 有効にした際に提供されたリカバリーコードを\n 使用できます。", + "description": "ログインコードをメールに送信しました。以下にログインコードとリカバリーコードの両方を入力してください", + "email-recovery-code-placeholder": "ログインコードを入力", + "lost-recovery-code": "モバイルデバイスやリカバリーコードにアクセスできない場合は、認証設定のリセットをリクエストできます。セキュリティ上の理由により、リセットには1〜3営業日かかることがあります。", + "lost-recovery-code-button": "アカウントのリセットをリクエスト", + "lost-recovery-code-heading": "モバイルデバイスとリカバリーコードを紛失しましたか?", + "recovery-code-placeholder": "リカバリーコードを入力", + "start-recovery": "復旧を開始", + "title": "二要素認証によるアカウント復旧" + }, + "auth-mfa-totp": { + "invalid-code": "無効なコードです", + "title": "二要素認証" + }, + "auth-mfa": { + "errors": { + "BACKUP_CODE_INVALID": "無効なバックアップコードです", + "EMAIL_CODE_INVALID": "無効なメールコードです" + }, + "having-problems": "問題が発生していますか?", + "mfa-verification-sent": "確認コードをモバイルデバイス{{number}}に送信しました。このコードの有効期限は3分間です。", + "no-device-access": "二要素認証デバイスにアクセスできない場合", + "title": "SMSによる二要素認証", + "totp-mfa-verification": "認証アプリで生成されたワンタイムパスワードを入力してください。", + "verification-code-placeholder": "確認コードを入力" + }, + "auth": { + "create-account": { + "button-label": "アカウントを作成", + "button-loading": "作成中..." + } + }, + "forgot-password-verify": { + "password-error": { + "missing-lower": "パスワードには小文字を1文字含める必要があります。", + "missing-number": "パスワードには数字を含める必要があります。", + "missing-special-char": "パスワードには次の記号のいずれかを1つ以上含める必要があります ", + "missing-upper": "パスワードには大文字を1文字含める必要があります。", + "passwords-do-not-match": "パスワードが一致しません", + "too-long": "パスワードは{{max_length}}文字以内で入力してください。", + "too-short": "パスワードは{{min_length}}文字以上で入力してください。" + }, + "password-rules": "パスワードは{{min_length}}〜{{max_length}}文字で、数字、大文字と小文字、そして次の記号のいずれかを1つ以上含める必要があります ", + "password-rules-reduced": "パスワードは{{min_length}}〜{{max_length}}文字である必要があります。", + "password-strength-html": "パスワードの強度: {{strength}}", + "password-strength-score": ["非常に弱い", "弱い", "普通", "良い", "強い"], + "success-message": "パスワードがリセットされました。サインインしてください", + "title": "アカウントを確認", + "verification-link-message": "確認用のリンクをメールで送信しました。リンクをクリックした後、このページに戻ってください。その後「続ける」を押してサインインしてください。" + }, + "forgot-password": { + "email": "メールアドレス", + "reset-password": "パスワードをリセット", + "title": "パスワードをお忘れですか" + }, + "password-reset": { + "cognito-reset-password-required": "セキュリティ上の理由により、パスワードは定期的に変更していただく必要があります。確認コードを{{email}}宛てに送信しましたので、メールをご確認のうえ、確認コードを入力してパスワードを更新してください。", + "signout-all-text": "現在のセッションは有効なままです。セキュリティ侵害が発生した場合は、すべての端末からサインアウトすることをお勧めします。すべての端末からサインアウトするには、app.remote.itにサインインし、アカウントページからサインアウトを行ってください。", + "update-button": "パスワードを更新", + "verification-code": "確認コード", + "verification-code-message": "確認コードを{{email}}宛てに送信しましたので、メールをご確認のうえ、確認コードを入力してパスワードを更新してください。" + }, + "sign-in": { + "create-account": "アカウントをお持ちでないですか?", + "forgot-password": "パスワードをお忘れですか?", + "or": "または", + "saml-link": "SAMLログイン", + "sign-in": "サインイン", + "sign-up-link": "無料で登録!", + "signing-in": "サインイン中...", + "user-password-link": "ユーザー名/パスワードでログイン" + }, + "sign-up": { + "title": "サインアップ" + }, + "update-password": { + "reset-password": "パスワードを更新", + "title": "パスワードの更新" + }, + "verify-account": { + "sending": "送信中...", + "resent-notice": "確認メールを再送信しました。", + "check-spam": "迷惑メールフォルダをご確認ください", + "resend-confirmation": "アカウント確認メールを再送信する", + "verification-received-message": "確認用リンクが届きませんか?" + } + } +} diff --git a/package-lock.json b/package-lock.json index 783ca7f5f..f04bbfc74 100644 --- a/package-lock.json +++ b/package-lock.json @@ -250,6 +250,7 @@ "@types/validator": "^13.12.2", "@vitejs/plugin-react": "^4.3.2", "eslint": "^8.53.0", + "i18next-parser": "^9.4.0", "typescript": "^5.9.2", "vite": "^6.4.3" } @@ -3320,6 +3321,19 @@ "react": ">=16.3" } }, + "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, + "license": "MIT", + "dependencies": { + "is-negated-glob": "^1.0.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", @@ -6468,6 +6482,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/symlink-or-copy": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@types/symlink-or-copy/-/symlink-or-copy-1.2.2.tgz", + "integrity": "sha512-MQ1AnmTLOncwEf9IVU+B2e4Hchrku5N67NkgcAHW0p3sdzPe0FNMANxEm6OJUzPniEQGkeT3OROLlCwZJLWFZA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/tough-cookie": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", @@ -7794,6 +7815,109 @@ "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, + "license": "MIT" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/browserslist": { "version": "4.28.6", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.6.tgz", @@ -8295,6 +8419,185 @@ "node": "*" } }, + "node_modules/cheerio": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz", + "integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "encoding-sniffer": "^0.2.1", + "htmlparser2": "^10.1.0", + "parse5": "^7.3.0", + "parse5-htmlparser2-tree-adapter": "^7.1.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^7.19.0", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=20.18.1" + }, + "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, + "license": "BSD-2-Clause", + "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.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "dev": true, + "license": "BSD-2-Clause", + "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, + "license": "MIT", + "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, + "license": "BSD-2-Clause", + "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.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "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-select/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?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, + "license": "MIT", + "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, + "license": "BSD-2-Clause", + "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.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "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/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/chevrotain": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-7.1.1.tgz", @@ -8571,6 +8874,16 @@ "node": ">=18" } }, + "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, + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, "node_modules/colorspace": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", @@ -10345,6 +10658,20 @@ "iconv-lite": "^0.6.2" } }, + "node_modules/encoding-sniffer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, "node_modules/end-of-stream": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", @@ -10443,6 +10770,13 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "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, + "license": "ISC" + }, "node_modules/entities": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", @@ -10461,6 +10795,13 @@ "node": ">=6" } }, + "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, + "license": "MIT" + }, "node_modules/err-code": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", @@ -11537,6 +11878,55 @@ "node": ">=12" } }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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", @@ -11567,6 +11957,37 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "license": "ISC" }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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", @@ -11927,6 +12348,26 @@ "node": ">=10.13.0" } }, + "node_modules/glob-stream": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-8.0.3.tgz", + "integrity": "sha512-fqZVj22LtFJkHODT+M4N1RJQ3TjnnQhfE9GwZI8qXscYarnhpip70poMldRnP8ipQ/w0B621kOhfc53/J9bd/A==", + "dev": true, + "license": "MIT", + "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/global-agent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", @@ -12048,6 +12489,60 @@ "dev": true, "license": "MIT" }, + "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, + "license": "MIT", + "dependencies": { + "through2": "^2.0.1" + } + }, + "node_modules/gulp-sort/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/gulp-sort/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/gulp-sort/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/gulp-sort/node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, "node_modules/handlebars": { "version": "4.7.9", "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", @@ -12187,6 +12682,51 @@ "he": "bin/he" } }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT" + }, + "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, + "license": "MIT" + }, "node_modules/history": { "version": "4.10.1", "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", @@ -12262,6 +12802,98 @@ "void-elements": "3.1.0" } }, + "node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, + "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, + "license": "MIT", + "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/dom-serializer/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?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, + "license": "BSD-2-Clause", + "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.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "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/htmlparser2/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/http-cache-semantics": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", @@ -12388,6 +13020,98 @@ "@babel/runtime": "^7.23.2" } }, + "node_modules/i18next-parser": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/i18next-parser/-/i18next-parser-9.4.0.tgz", + "integrity": "sha512-SLQJGDj/baBIB9ALmJVXSOXWh3Zn9+wH7J2IuQ4rvx8yuQYpUWitmt8cHFjj6FExjgr8dHfd1SGeQgkowXDO1Q==", + "deprecated": "Project is deprecated, use i18next-cli instead", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.0", + "broccoli-plugin": "^4.0.7", + "cheerio": "^1.0.0", + "colors": "^1.4.0", + "commander": "^12.1.0", + "eol": "^0.9.1", + "esbuild": "^0.25.0", + "fs-extra": "^11.2.0", + "gulp-sort": "^2.0.0", + "i18next": "^23.5.1 || ^24.2.0", + "js-yaml": "^4.1.0", + "lilconfig": "^3.1.3", + "rsvp": "^4.8.5", + "sort-keys": "^5.0.0", + "typescript": "^5.0.4", + "vinyl": "^3.0.0", + "vinyl-fs": "^4.0.0" + }, + "bin": { + "i18next": "bin/cli.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || ^22.0.0", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/i18next-parser/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/i18next-parser/node_modules/fs-extra": { + "version": "11.3.6", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.6.tgz", + "integrity": "sha512-w8ZNZr2mKIc7qeNaQ9AVPT1+iFaI+Avd4xudVOvdDJ8VytREi1Ft5Ih7hd9jjehod8vAM5GMsfQ/TpPf4EyoEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/i18next-parser/node_modules/i18next": { + "version": "24.2.3", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-24.2.3.tgz", + "integrity": "sha512-lfbf80OzkocvX7nmZtu7nSTNbrTYR52sLWxPtlXX1zAhVw8WEnFk4puUkCR4B1dNQwbSpEHHHemcZu//7EcB7A==", + "dev": true, + "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" + } + ], + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.26.10" + }, + "peerDependencies": { + "typescript": "^5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/i18next-resources-to-backend": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/i18next-resources-to-backend/-/i18next-resources-to-backend-1.2.1.tgz", @@ -12908,6 +13632,16 @@ "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, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-negative-zero": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", @@ -13126,6 +13860,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-weakmap": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", @@ -14185,6 +14929,16 @@ "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", "license": "MIT" }, + "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, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -14224,6 +14978,19 @@ "immediate": "~3.0.5" } }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -14530,6 +15297,27 @@ "node": ">=10" } }, + "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, + "license": "ISC", + "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, + "license": "MIT" + }, "node_modules/matcher/node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -15122,6 +15910,16 @@ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", "license": "MIT" }, + "node_modules/mktemp": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mktemp/-/mktemp-2.0.3.tgz", + "integrity": "sha512-Bq72L2oi/isYSy0guN9ihNhAMQOyZEwts+Bezm/1U+wh8bQ+fVQ2ZiUoJJjceOMiiKv/BUrA0NF98jFc81CB6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "20 || 22 || 24" + } + }, "node_modules/modify-values": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/modify-values/-/modify-values-1.0.1.tgz", @@ -15575,6 +16373,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, "node_modules/npm-run-all": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz", @@ -16075,6 +16886,75 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "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, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -16117,6 +16997,13 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "license": "MIT" }, + "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, + "license": "ISC" + }, "node_modules/path-scurry": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", @@ -16502,6 +17389,16 @@ "node": ">=0.4.0" } }, + "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, + "license": "MIT", + "engines": { + "node": "10.* || >= 12.*" + } + }, "node_modules/promise-polyfill": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.3.0.tgz", @@ -16660,48 +17557,150 @@ "side-channel": "^1.1.0" }, "engines": { - "node": ">=0.6" + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==", + "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/quick-lru": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", + "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/quick-temp": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/quick-temp/-/quick-temp-0.1.9.tgz", + "integrity": "sha512-yI0h7tIhKVObn03kD+Ln9JFi4OljD28lfaOsTdfpTR0xzrhGOod+q66CjGafUqYX2juUfT9oHIGrTBBo22mkRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mktemp": "^2.0.1", + "rimraf": "^5.0.10", + "underscore.string": "~3.3.6" + } + }, + "node_modules/quick-temp/node_modules/brace-expansion": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/quick-temp/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/quick-temp/node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/quick-temp/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/querystring": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==", - "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", + "node_modules/quick-temp/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", "engines": { - "node": ">=0.4.x" + "node": ">=16 || 14 >=14.17" } }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/quick-lru": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", - "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", - "license": "MIT", - "engines": { - "node": ">=8" + "node_modules/quick-temp/node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/range-parser": { @@ -17317,6 +18316,13 @@ "resolved": "frontend", "link": true }, + "node_modules/remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==", + "dev": true, + "license": "ISC" + }, "node_modules/replace": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/replace/-/replace-1.2.2.tgz", @@ -17335,6 +18341,16 @@ "node": ">= 6" } }, + "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, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, "node_modules/replace/node_modules/ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", @@ -17667,6 +18683,19 @@ "node": ">=4" } }, + "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, + "license": "MIT", + "dependencies": { + "value-or-function": "^4.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, "node_modules/resolve-pathname": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz", @@ -17913,6 +18942,16 @@ "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, + "license": "MIT", + "engines": { + "node": "6.* || >= 7.*" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -18755,6 +19794,35 @@ "node": ">= 14" } }, + "node_modules/sort-keys": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-5.1.0.tgz", + "integrity": "sha512-aSbHV0DaBcr7u0PVHXzM6NbZNAtrr9sF6+Qfs9UUVG7Ll3jQ6hHi8F/xqIIcn2rvIVbr0v/2zyjSdwSV47AgLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-obj": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sort-keys/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, + "license": "MIT", + "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", @@ -18853,8 +19921,7 @@ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", "dev": true, - "license": "BSD-3-Clause", - "optional": true + "license": "BSD-3-Clause" }, "node_modules/ssri": { "version": "12.0.0", @@ -18959,6 +20026,16 @@ "node": ">= 0.10.0" } }, + "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, + "license": "MIT", + "dependencies": { + "streamx": "^2.13.2" + } + }, "node_modules/streamx": { "version": "2.22.1", "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.1.tgz", @@ -19225,6 +20302,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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, + "license": "MIT" + }, "node_modules/tar": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", @@ -19282,6 +20366,16 @@ "bintrees": "1.0.2" } }, + "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, + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, "node_modules/temp-dir": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-3.0.0.tgz", @@ -19511,6 +20605,19 @@ "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, + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -19882,6 +20989,30 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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, + "license": "MIT", + "dependencies": { + "sprintf-js": "^1.1.1", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/undici": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", + "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", @@ -20150,6 +21281,16 @@ "integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==", "license": "MIT" }, + "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, + "license": "MIT", + "engines": { + "node": ">= 10.13.0" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -20183,6 +21324,134 @@ "license": "MIT", "optional": true }, + "node_modules/vinyl": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-3.0.1.tgz", + "integrity": "sha512-0QwqXteBNXgnLCdWdvPQBX6FXRHtIH3VhJPTd5Lwn28tJXc34YqSCWUmkOvtJHBmB3gGoPtrOKk3Ts8/kEZ9aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^2.1.2", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/vinyl-fs": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-4.0.2.tgz", + "integrity": "sha512-XRFwBLLTl8lRAOYiBqxY279wY46tVxLaRhSwo3GzKEuLz1giffsOquWWboD/haGf5lx+JyTigCFfe7DWHoARIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fs-mkdirp-stream": "^2.0.1", + "glob-stream": "^8.0.3", + "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.1", + "vinyl-sourcemap": "^2.0.0" + }, + "engines": { + "node": ">=10.13.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, + "license": "MIT", + "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-sourcemap/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, "node_modules/vite": { "version": "6.4.3", "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", @@ -20292,6 +21561,29 @@ "node": ">=0.10.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, + "license": "MIT", + "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, + "license": "MIT" + }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", @@ -20318,6 +21610,30 @@ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", "license": "BSD-2-Clause" }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", From f098d2a2cf1402d26e9edde2d93a23db5cc7ca78 Mon Sep 17 00:00:00 2001 From: Jamie Ruderman Date: Wed, 22 Jul 2026 13:27:31 -0700 Subject: [PATCH 5/9] feat(i18n): route durations and date formatting through the app locale --- frontend/src/components/Countdown.tsx | 2 +- frontend/src/components/Duration/Duration.tsx | 3 ++- frontend/src/components/GraphColumn.tsx | 2 +- frontend/src/components/TimeSeries.tsx | 2 +- frontend/src/helpers/dateHelper.ts | 14 ++++++++++++-- frontend/src/helpers/usageHelper.ts | 2 +- frontend/src/models/plans.ts | 2 +- frontend/src/models/ui.ts | 2 ++ 8 files changed, 21 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/Countdown.tsx b/frontend/src/components/Countdown.tsx index 1e5d3d75b..8f7ab91e1 100644 --- a/frontend/src/components/Countdown.tsx +++ b/frontend/src/components/Countdown.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react' import { useInterval } from '../hooks/useInterval' -import humanize from 'humanize-duration' +import { humanizeDuration as humanize } from '../helpers/dateHelper' type Props = { endTime?: number diff --git a/frontend/src/components/Duration/Duration.tsx b/frontend/src/components/Duration/Duration.tsx index 5ab6c36ee..9128e5b32 100644 --- a/frontend/src/components/Duration/Duration.tsx +++ b/frontend/src/components/Duration/Duration.tsx @@ -1,6 +1,7 @@ import React, { useState } from 'react' import { useInterval } from '../../hooks/useInterval' -import humanize, { HumanizerOptions } from 'humanize-duration' +import { HumanizerOptions } from 'humanize-duration' +import { humanizeDuration as humanize } from '../../helpers/dateHelper' export const dateDefaults: Intl.DateTimeFormatOptions = { year: 'numeric', diff --git a/frontend/src/components/GraphColumn.tsx b/frontend/src/components/GraphColumn.tsx index 6d1fd3d68..731410f56 100644 --- a/frontend/src/components/GraphColumn.tsx +++ b/frontend/src/components/GraphColumn.tsx @@ -2,7 +2,7 @@ import React from 'react' import { humanizeResolutionLookup, TimeSeriesTypeLookup } from '../helpers/dateHelper' import { IconButton } from '../buttons/IconButton' import { Box } from '@mui/material' -import humanize from 'humanize-duration' +import { humanizeDuration as humanize } from '../helpers/dateHelper' export const GraphColumn: React.FC<{ title: string; timeSeries?: ITimeSeries }> = ({ title, timeSeries }) => { if (!timeSeries) return <>{title} diff --git a/frontend/src/components/TimeSeries.tsx b/frontend/src/components/TimeSeries.tsx index 2ce2a62ab..25d6442ff 100644 --- a/frontend/src/components/TimeSeries.tsx +++ b/frontend/src/components/TimeSeries.tsx @@ -8,7 +8,7 @@ import { import { BarGraph, BarGraphProps } from './BarGraph' import { Typography, Stack, Box } from '@mui/material' import { Timestamp } from './Timestamp' -import humanize from 'humanize-duration' +import { humanizeDuration as humanize } from '../helpers/dateHelper' import * as d3 from 'd3' type Props = Omit & { diff --git a/frontend/src/helpers/dateHelper.ts b/frontend/src/helpers/dateHelper.ts index 45154ccc8..d0857f7da 100644 --- a/frontend/src/helpers/dateHelper.ts +++ b/frontend/src/helpers/dateHelper.ts @@ -1,5 +1,15 @@ import { DateTime, Duration } from 'luxon' -import { Unit } from 'humanize-duration' +import humanize, { Unit, HumanizerOptions } from 'humanize-duration' +import i18n from '../i18n' + +// The active locale for all date/duration formatting. Driven by the app language +// preference (ui.setLanguage), falling back to the OS/browser language. +export const getLocale = () => i18n.resolvedLanguage || window.navigator.language || 'en' + +// Localized humanize-duration. Use this everywhere instead of importing +// humanize-duration directly so durations ("3 days", "2 hours") translate. +export const humanizeDuration = (ms: number, options: HumanizerOptions = {}) => + humanize(ms, { language: getLocale(), fallbacks: ['en'], ...options }) export function isToday(dateToCheck: Date): boolean { const today = new Date().toLocaleDateString() @@ -9,7 +19,7 @@ export function isToday(dateToCheck: Date): boolean { } export const getDateFormatString = () => { - const formatObj = new Intl.DateTimeFormat(window.navigator.language).formatToParts(new Date()) + const formatObj = new Intl.DateTimeFormat(getLocale()).formatToParts(new Date()) return formatObj .map(obj => { switch (obj.type) { diff --git a/frontend/src/helpers/usageHelper.ts b/frontend/src/helpers/usageHelper.ts index b5ebcc012..f42e025e7 100644 --- a/frontend/src/helpers/usageHelper.ts +++ b/frontend/src/helpers/usageHelper.ts @@ -1,4 +1,4 @@ -import humanize from 'humanize-duration' +import { humanizeDuration as humanize } from './dateHelper' export function isUsageNumber(value?: number | null): value is number { return typeof value === 'number' && Number.isFinite(value) && value >= 0 diff --git a/frontend/src/models/plans.ts b/frontend/src/models/plans.ts index 79b503a2a..739dfc604 100644 --- a/frontend/src/models/plans.ts +++ b/frontend/src/models/plans.ts @@ -20,7 +20,7 @@ import { graphQLFetchPlans } from '../services/graphQLRequest' import { getDevices } from '../selectors/devices' import { RootModel } from '.' import cloudSync from '../services/CloudSync' -import humanize from 'humanize-duration' +import { humanizeDuration as humanize } from '../helpers/dateHelper' type ILicenseLookup = { productId: string; platform?: number } diff --git a/frontend/src/models/ui.ts b/frontend/src/models/ui.ts index 04d4c8cc1..981ac70db 100644 --- a/frontend/src/models/ui.ts +++ b/frontend/src/models/ui.ts @@ -4,6 +4,7 @@ import { StatusBar, Style } from '@capacitor/status-bar' import { RootModel } from '.' import { isDarkMode } from '../styling/theme' import i18n, { resolveLanguage, LanguageMode } from '../i18n' +import { Settings as LuxonSettings } from 'luxon' import { NoticeProps } from '../components/Notice' import { createModel } from '@rematch/core' import { SIDEBAR_WIDTH } from '../constants' @@ -261,6 +262,7 @@ export default createModel()({ language = language || state.ui.language const resolved = resolveLanguage(language) dispatch.ui.setPersistent({ language }) + LuxonSettings.defaultLocale = resolved if (i18n.resolvedLanguage !== resolved) i18n.changeLanguage(resolved) // Sync the resolved code to the Electron main process (tray/menu/dialogs). if (browser.isElectron) { From 5d85a26073b4f44ae108e24c201ecbe3ce657c5a Mon Sep 17 00:00:00 2001 From: Jamie Ruderman Date: Wed, 22 Jul 2026 13:35:28 -0700 Subject: [PATCH 6/9] feat(i18n): extract OptionsPage strings and seed app/cognito translations (ja/de/es) --- frontend/i18next-parser.config.js | 5 +- frontend/src/i18n/index.ts | 3 + frontend/src/i18n/locales/de/app.json | 57 +++++++++++ frontend/src/i18n/locales/de/cognito.json | 39 ++++---- frontend/src/i18n/locales/en/app.json | 57 +++++++++++ frontend/src/i18n/locales/en/cognito.json | 39 ++++---- frontend/src/i18n/locales/es/app.json | 57 +++++++++++ frontend/src/i18n/locales/es/cognito.json | 39 ++++---- frontend/src/i18n/locales/ja/app.json | 57 +++++++++++ frontend/src/i18n/locales/ja/cognito.json | 39 ++++---- frontend/src/pages/OptionsPage.tsx | 112 ++++++++++++++-------- 11 files changed, 400 insertions(+), 104 deletions(-) diff --git a/frontend/i18next-parser.config.js b/frontend/i18next-parser.config.js index fd4ffbba5..b70376acf 100644 --- a/frontend/i18next-parser.config.js +++ b/frontend/i18next-parser.config.js @@ -14,7 +14,10 @@ export default { input: ['src/**/*.{ts,tsx}'], output: 'src/i18n/locales/$LOCALE/$NAMESPACE.json', sort: true, - keepRemoved: false, + // Never auto-prune: some keys are built dynamically (e.g. cognito error keys) + // and the parser can't see them statically. `npm run i18n:check` reports dead + // keys so they can be removed deliberately. + keepRemoved: true, // English keeps the inline default text; other locales stay empty until translated. defaultValue: (locale, _ns, _key, value) => (locale === 'en' ? value?.usageContext?.defaultValue ?? '' : ''), createOldCatalogs: false, diff --git a/frontend/src/i18n/index.ts b/frontend/src/i18n/index.ts index 5066201b1..2ac03f79e 100644 --- a/frontend/src/i18n/index.ts +++ b/frontend/src/i18n/index.ts @@ -50,6 +50,9 @@ i18n debug: false, interpolation: { escapeValue: false }, react: { useSuspense: false }, + // An untranslated (empty-string) value must fall back to English, not render + // blank — catalogs are seeded incrementally, so empties are expected. + returnEmptyString: false, partialBundledLanguages: true, resources: { en: { app: enApp, notices: enNotices, cognito: enCognito }, diff --git a/frontend/src/i18n/locales/de/app.json b/frontend/src/i18n/locales/de/app.json index b19644cc3..42d306f8b 100644 --- a/frontend/src/i18n/locales/de/app.json +++ b/frontend/src/i18n/locales/de/app.json @@ -1,8 +1,65 @@ { + "common": { + "areYouSure": "Sind Sie sicher?" + }, "options": { + "advanced": "Erweitert", + "clearErrors": { + "label": "Alle Verbindungsfehler löschen", + "subLabel": "Die Verbindungen bleiben unverändert, aber die gespeicherten Fehlermeldungen werden gelöscht." + }, + "disableDeepLinks": { + "confirm": "Bitte starten Sie die Desktop-Anwendung neu, damit die Änderungen wirksam werden. Unter Linux müssen Sie möglicherweise auch Ihren Standard-Dateihandler zurücksetzen.", + "confirmTitle": "Neustart erforderlich", + "label": "Deep-Link-Verarbeitung deaktivieren", + "subLabel": "Sie können die Deep-Link-Verarbeitung deaktivieren, um einen Linux-Fehler zu beheben, bei dem die Desktop-App zum Öffnen von html-Dateitypen eingestellt wird." + }, "language": { "label": "Sprache", "system": "Wie das System" + }, + "namedConnections": { + "confirm": "Eine Änderung der Zertifikatsverarbeitung erfordert einen Neustart aller Verbindungen.", + "confirmTitle": "Fortfahren?", + "label": "Benannte Verbindungen", + "subLabel": "Verwenden Sie ein remote.it-HTTPS-Zertifikat, um lokale Verbindungen zu verarbeiten und zu benennen" + }, + "openAtLogin": "Bei Anmeldung öffnen", + "reinstallAgent": { + "confirm": "Der Systemagent wird gestoppt, und es wird versucht, ihn neu zu installieren.", + "installing": "Wird installiert …", + "label": "Systemagent neu installieren", + "version": "Version {{version}}" + }, + "resetGuides": { + "label": "Interaktive Anleitungen zurücksetzen", + "subLabel": "Schaltet die Hilfe-Sprechblasen in der App wieder ein." + }, + "showFullInterface": { + "confirm": "Neue Verbindungen erfolgen von {{device}} und nicht von Ihrem lokalen Rechner.", + "label": "Vollständige Oberfläche anzeigen", + "subLabel": "Remote-Geräte zeigen nur die Konfigurationsoptionen des Ziels an. Aktivieren Sie diese Option für vollen Zugriff." + }, + "showSupportFiles": { + "label": "Support-Dateien anzeigen", + "subLabel": "Zeigt die Ordner an, die die Anwendungsprotokolle und die Konfigurationsdatei enthalten." + }, + "sshConfig": { + "label": "Verwaltete SSH-Konfiguration", + "subLabel": "Erlauben Sie remote.it, seine verwaltete SSH-Konfigurationsdatei einzubinden, um leichter auf Ihre Geräte zuzugreifen." + }, + "theme": { + "dark": "Dunkel", + "label": "Design", + "light": "Hell", + "system": "Wie das System" + }, + "thisDevice": "dieses Gerät", + "title": "Anwendung", + "uninstall": { + "confirm": "Dieses System wird als Host entfernt, ebenso wie Ihre Verbindungen und die Kommandozeilen-Tools.", + "label": "Deinstallieren", + "subLabel": "Meldet dieses Gerät ab, entfernt vollständig alle gespeicherten Daten und deinstalliert den Systemagenten sowie die Kommandozeilen-Tools. Führen Sie dies aus, bevor Sie die Anwendung von Ihrem System entfernen. Kann nur vom Gerätebesitzer ausgeführt werden." } } } diff --git a/frontend/src/i18n/locales/de/cognito.json b/frontend/src/i18n/locales/de/cognito.json index 96f64452e..29054bfc0 100644 --- a/frontend/src/i18n/locales/de/cognito.json +++ b/frontend/src/i18n/locales/de/cognito.json @@ -3,6 +3,7 @@ "connect": "Verbinden Sie sich von überall mit unserer mobilen App.", "unknown-os": "Jetzt verfügbar für iOS und Android." }, + "CUSTOM CHALLENGE": "", "global": { "action": { "continue": "Weiter", @@ -38,9 +39,11 @@ "start-recovery": "Wiederherstellung starten", "title": "Zwei-Faktor-Kontowiederherstellung" }, - "auth-mfa-totp": { - "invalid-code": "Ungültiger Code", - "title": "Zwei-Faktor-Authentifizierung" + "auth": { + "create-account": { + "button-label": "Konto erstellen", + "button-loading": "Wird erstellt..." + } }, "auth-mfa": { "errors": { @@ -54,11 +57,14 @@ "totp-mfa-verification": "Geben Sie das von Ihrer Authentifizierungs-App generierte Einmalpasswort ein.", "verification-code-placeholder": "Bestätigungscode eingeben" }, - "auth": { - "create-account": { - "button-label": "Konto erstellen", - "button-loading": "Wird erstellt..." - } + "auth-mfa-totp": { + "invalid-code": "Ungültiger Code", + "title": "Zwei-Faktor-Authentifizierung" + }, + "forgot-password": { + "email": "E-Mail-Adresse", + "reset-password": "Passwort zurücksetzen", + "title": "Passwort vergessen" }, "forgot-password-verify": { "password-error": { @@ -73,16 +79,17 @@ "password-rules": "Passwörter müssen {{min_length}}–{{max_length}} Zeichen lang sein und eine Zahl, einen Groß- und einen Kleinbuchstaben sowie mindestens eines der folgenden Zeichen enthalten ", "password-rules-reduced": "Passwörter müssen {{min_length}}–{{max_length}} Zeichen lang sein.", "password-strength-html": "Passwortstärke: {{strength}}", - "password-strength-score": ["Sehr schwach", "Schwach", "Ausreichend", "Gut", "Stark"], + "password-strength-score": { + "0": "Sehr schwach", + "1": "Schwach", + "2": "Ausreichend", + "3": "Gut", + "4": "Stark" + }, "success-message": "Ihr Passwort wurde zurückgesetzt. Bitte melden Sie sich an", "title": "Konto bestätigen", "verification-link-message": "Ein Bestätigungslink wurde an Sie per E-Mail gesendet. Klicken Sie auf den Link und kehren Sie dann hierher zurück. Klicken Sie anschließend auf „Weiter“, um sich anzumelden." }, - "forgot-password": { - "email": "E-Mail-Adresse", - "reset-password": "Passwort zurücksetzen", - "title": "Passwort vergessen" - }, "password-reset": { "cognito-reset-password-required": "Aus Sicherheitsgründen müssen Benutzer ihr Passwort regelmäßig ändern. Ein Bestätigungscode wurde an {{email}} gesendet. Bitte prüfen Sie Ihre E-Mails, geben Sie den Bestätigungscode ein und aktualisieren Sie Ihr Passwort.", "signout-all-text": "Aktuelle Sitzungen bleiben aktiv. Falls Sie von einem Sicherheitsvorfall betroffen waren, empfehlen wir, sich überall abzumelden. Um sich überall abzumelden, melden Sie sich bei app.remote.it an und rufen Sie dann die Konten-Seite auf, um sich überall abzumelden.", @@ -108,10 +115,10 @@ "title": "Passwort aktualisieren" }, "verify-account": { - "sending": "Wird gesendet...", - "resent-notice": "Bestätigungs-E-Mail erneut gesendet.", "check-spam": "Überprüfen Sie Ihren Spam-Ordner", "resend-confirmation": "Kontobestätigung erneut senden.", + "resent-notice": "Bestätigungs-E-Mail erneut gesendet.", + "sending": "Wird gesendet...", "verification-received-message": "Keinen Bestätigungslink erhalten?" } } diff --git a/frontend/src/i18n/locales/en/app.json b/frontend/src/i18n/locales/en/app.json index 1558d9a9f..9b8261098 100644 --- a/frontend/src/i18n/locales/en/app.json +++ b/frontend/src/i18n/locales/en/app.json @@ -1,8 +1,65 @@ { + "common": { + "areYouSure": "Are you sure?" + }, "options": { + "advanced": "Advanced", + "clearErrors": { + "label": "Clear all connection errors", + "subLabel": "Will leave connections all unchanged, but clear the saved error messages." + }, + "disableDeepLinks": { + "confirm": "Please restart the desktop application for changes to take affect. You may also have to reset your default file handler on Linux.", + "confirmTitle": "Restart required", + "label": "Disable deep link handling", + "subLabel": "You can disable deep link handling to fix a bug in Linux where the desktop app is set to open html file types." + }, "language": { "label": "Language", "system": "Same as system" + }, + "namedConnections": { + "confirm": "Changing the certificate handling will require we restart all connections.", + "confirmTitle": "Continue?", + "label": "Named connections", + "subLabel": "Use a Remote.It HTTPS certificate to handle and name local connections" + }, + "openAtLogin": "Open at login", + "reinstallAgent": { + "confirm": "This will stop and attempt to re-install the system agent.", + "installing": "Installing...", + "label": "Re-install system agent", + "version": "Version {{version}}" + }, + "resetGuides": { + "label": "Reset interactive guides", + "subLabel": "Turn back on the in-app help bubbles." + }, + "showFullInterface": { + "confirm": "New connections will be from {{device}} and not your local machine.", + "label": "Show full interface", + "subLabel": "Remote devices only show target configuration options. Enable for full access." + }, + "showSupportFiles": { + "label": "Show support files", + "subLabel": "Will show the folders that contain the application logs and config file." + }, + "sshConfig": { + "label": "Managed SSH config", + "subLabel": "Allow Remote.it to include it's managed SSH config file for easier access to your devices." + }, + "theme": { + "dark": "Dark", + "label": "Theme", + "light": "Light", + "system": "Same as system" + }, + "thisDevice": "this device", + "title": "Application", + "uninstall": { + "confirm": "You will remove this system as a host, your connections and command line utilities.", + "label": "Uninstall", + "subLabel": "De-register this device, completely remove all saved data, and uninstall the system agent and command line tools. Do this before removing the application from your system. Can only be done by the device owner." } } } diff --git a/frontend/src/i18n/locales/en/cognito.json b/frontend/src/i18n/locales/en/cognito.json index e9ab32219..927c8e2e5 100644 --- a/frontend/src/i18n/locales/en/cognito.json +++ b/frontend/src/i18n/locales/en/cognito.json @@ -3,6 +3,7 @@ "connect": "Connect from anywhere using our mobile app.", "unknown-os": "Now available for iOS and Android." }, + "CUSTOM CHALLENGE": "CUSTOM CHALLENGE", "global": { "action": { "continue": "Continue", @@ -38,9 +39,11 @@ "start-recovery": "Start Recovery", "title": "Two-Factor Account Recovery" }, - "auth-mfa-totp": { - "invalid-code": "Invalid Code", - "title": "Two-Factor Authentication" + "auth": { + "create-account": { + "button-label": "Create Account", + "button-loading": "Creating..." + } }, "auth-mfa": { "errors": { @@ -54,11 +57,14 @@ "totp-mfa-verification": "Enter the one-time password generated by your authentication app.", "verification-code-placeholder": "Enter verification code" }, - "auth": { - "create-account": { - "button-label": "Create Account", - "button-loading": "Creating..." - } + "auth-mfa-totp": { + "invalid-code": "Invalid Code", + "title": "Two-Factor Authentication" + }, + "forgot-password": { + "email": "Email Address", + "reset-password": "Reset Password", + "title": "Forgot Password" }, "forgot-password-verify": { "password-error": { @@ -73,16 +79,17 @@ "password-rules": "Passwords must be {{min_length}}-{{max_length}} characters, contain a number, an uppercase and lowercase letter, and at least one of the the following characters ", "password-rules-reduced": "Passwords must be {{min_length}}-{{max_length}} characters.", "password-strength-html": "Password Strength: {{strength}}", - "password-strength-score": ["Very Weak", "Weak", "Fair", "Good", "Strong"], + "password-strength-score": { + "0": "Very Weak", + "1": "Weak", + "2": "Fair", + "3": "Good", + "4": "Strong" + }, "success-message": "Your password has been reset, please sign in", "title": "Verify Your Account", "verification-link-message": "An email verification link has been sent to you. Click the link and return here. Then press continue to sign in." }, - "forgot-password": { - "email": "Email Address", - "reset-password": "Reset Password", - "title": "Forgot Password" - }, "password-reset": { "cognito-reset-password-required": "For security purposes, we require users to change their passwords periodically. A verification code has been sent to {{email}}, please check your email then enter the verification code and update your password.", "signout-all-text": "Current sessions will remain active. If you have experienced a security breach, signing out everywhere is recommended. To sign out everywhere, sign in to app.remote.it and then go to the accounts page to sign out everywhere.", @@ -108,10 +115,10 @@ "title": "Update Password" }, "verify-account": { - "sending": "Sending...", - "resent-notice": "Email verification resent.", "check-spam": "Check your spam", "resend-confirmation": "Resend account confirmation.", + "resent-notice": "Email verification resent.", + "sending": "Sending...", "verification-received-message": "Didn’t receive your verification link?" } } diff --git a/frontend/src/i18n/locales/es/app.json b/frontend/src/i18n/locales/es/app.json index 1a04ea74c..b9cfb46ec 100644 --- a/frontend/src/i18n/locales/es/app.json +++ b/frontend/src/i18n/locales/es/app.json @@ -1,8 +1,65 @@ { + "common": { + "areYouSure": "¿Estás seguro?" + }, "options": { + "advanced": "Avanzado", + "clearErrors": { + "label": "Borrar todos los errores de conexión", + "subLabel": "Las conexiones no se modificarán, pero se borrarán los mensajes de error guardados." + }, + "disableDeepLinks": { + "confirm": "Reinicia la aplicación de escritorio para que los cambios surtan efecto. Es posible que también debas restablecer tu controlador de archivos predeterminado en Linux.", + "confirmTitle": "Reinicio necesario", + "label": "Deshabilitar el manejo de enlaces profundos", + "subLabel": "Puedes deshabilitar el manejo de enlaces profundos para solucionar un error en Linux por el cual la app de escritorio queda configurada para abrir archivos html." + }, "language": { "label": "Idioma", "system": "Igual que el sistema" + }, + "namedConnections": { + "confirm": "Cambiar el manejo de certificados requerirá reiniciar todas las conexiones.", + "confirmTitle": "¿Continuar?", + "label": "Conexiones con nombre", + "subLabel": "Usa un certificado HTTPS de remote.it para gestionar y nombrar las conexiones locales" + }, + "openAtLogin": "Abrir al iniciar sesión", + "reinstallAgent": { + "confirm": "Esto detendrá el agente del sistema e intentará reinstalarlo.", + "installing": "Instalando…", + "label": "Reinstalar agente del sistema", + "version": "Versión {{version}}" + }, + "resetGuides": { + "label": "Restablecer guías interactivas", + "subLabel": "Vuelve a activar las burbujas de ayuda dentro de la app." + }, + "showFullInterface": { + "confirm": "Las nuevas conexiones se realizarán desde {{device}} y no desde tu equipo local.", + "label": "Mostrar interfaz completa", + "subLabel": "Los dispositivos remotos solo muestran las opciones de configuración del destino. Actívalo para obtener acceso completo." + }, + "showSupportFiles": { + "label": "Mostrar archivos de soporte", + "subLabel": "Mostrará las carpetas que contienen los registros de la aplicación y el archivo de configuración." + }, + "sshConfig": { + "label": "Configuración SSH administrada", + "subLabel": "Permite que remote.it incluya su archivo de configuración SSH administrado para facilitar el acceso a tus dispositivos." + }, + "theme": { + "dark": "Oscuro", + "label": "Tema", + "light": "Claro", + "system": "Igual que el sistema" + }, + "thisDevice": "este dispositivo", + "title": "Aplicación", + "uninstall": { + "confirm": "Se eliminará este sistema como host, junto con tus conexiones y las utilidades de línea de comandos.", + "label": "Desinstalar", + "subLabel": "Da de baja este dispositivo, elimina por completo todos los datos guardados y desinstala el agente del sistema y las herramientas de línea de comandos. Haz esto antes de eliminar la aplicación de tu sistema. Solo puede hacerlo el propietario del dispositivo." } } } diff --git a/frontend/src/i18n/locales/es/cognito.json b/frontend/src/i18n/locales/es/cognito.json index b81a5ad5b..122a346ca 100644 --- a/frontend/src/i18n/locales/es/cognito.json +++ b/frontend/src/i18n/locales/es/cognito.json @@ -3,6 +3,7 @@ "connect": "Conéctate desde cualquier lugar con nuestra aplicación móvil.", "unknown-os": "Ya disponible para iOS y Android." }, + "CUSTOM CHALLENGE": "", "global": { "action": { "continue": "Continuar", @@ -38,9 +39,11 @@ "start-recovery": "Iniciar recuperación", "title": "Recuperación de cuenta con doble factor" }, - "auth-mfa-totp": { - "invalid-code": "Código no válido", - "title": "Autenticación de dos factores" + "auth": { + "create-account": { + "button-label": "Crear cuenta", + "button-loading": "Creando..." + } }, "auth-mfa": { "errors": { @@ -54,11 +57,14 @@ "totp-mfa-verification": "Introduce la contraseña de un solo uso generada por tu aplicación de autenticación.", "verification-code-placeholder": "Introduce el código de verificación" }, - "auth": { - "create-account": { - "button-label": "Crear cuenta", - "button-loading": "Creando..." - } + "auth-mfa-totp": { + "invalid-code": "Código no válido", + "title": "Autenticación de dos factores" + }, + "forgot-password": { + "email": "Correo electrónico", + "reset-password": "Restablecer contraseña", + "title": "Olvidé mi contraseña" }, "forgot-password-verify": { "password-error": { @@ -73,16 +79,17 @@ "password-rules": "Las contraseñas deben tener entre {{min_length}} y {{max_length}} caracteres, contener un número, una letra mayúscula y una minúscula, y al menos uno de los siguientes caracteres ", "password-rules-reduced": "Las contraseñas deben tener entre {{min_length}} y {{max_length}} caracteres.", "password-strength-html": "Seguridad de la contraseña: {{strength}}", - "password-strength-score": ["Muy débil", "Débil", "Aceptable", "Buena", "Fuerte"], + "password-strength-score": { + "0": "Muy débil", + "1": "Débil", + "2": "Aceptable", + "3": "Buena", + "4": "Fuerte" + }, "success-message": "Tu contraseña se ha restablecido; inicia sesión", "title": "Verifica tu cuenta", "verification-link-message": "Te hemos enviado un enlace de verificación por correo electrónico. Haz clic en el enlace y vuelve aquí. Luego pulsa continuar para iniciar sesión." }, - "forgot-password": { - "email": "Correo electrónico", - "reset-password": "Restablecer contraseña", - "title": "Olvidé mi contraseña" - }, "password-reset": { "cognito-reset-password-required": "Por motivos de seguridad, pedimos a los usuarios que cambien su contraseña periódicamente. Se ha enviado un código de verificación a {{email}}; revisa tu correo, introduce el código de verificación y actualiza tu contraseña.", "signout-all-text": "Las sesiones actuales permanecerán activas. Si has sufrido una brecha de seguridad, se recomienda cerrar sesión en todos los dispositivos. Para cerrar sesión en todos los dispositivos, inicia sesión en app.remote.it y luego ve a la página de cuentas para cerrar sesión en todas partes.", @@ -108,10 +115,10 @@ "title": "Actualizar contraseña" }, "verify-account": { - "sending": "Enviando...", - "resent-notice": "Se ha reenviado el correo de verificación.", "check-spam": "Revisa tu carpeta de spam", "resend-confirmation": "Reenviar confirmación de cuenta.", + "resent-notice": "Se ha reenviado el correo de verificación.", + "sending": "Enviando...", "verification-received-message": "¿No recibiste tu enlace de verificación?" } } diff --git a/frontend/src/i18n/locales/ja/app.json b/frontend/src/i18n/locales/ja/app.json index 0b0c8c2ed..91a09461c 100644 --- a/frontend/src/i18n/locales/ja/app.json +++ b/frontend/src/i18n/locales/ja/app.json @@ -1,8 +1,65 @@ { + "common": { + "areYouSure": "本当によろしいですか?" + }, "options": { + "advanced": "詳細設定", + "clearErrors": { + "label": "接続エラーをすべてクリア", + "subLabel": "接続自体は変更されませんが、保存されたエラーメッセージはクリアされます。" + }, + "disableDeepLinks": { + "confirm": "変更を反映するには、デスクトップアプリケーションを再起動してください。Linuxでは、デフォルトのファイルハンドラーをリセットする必要がある場合もあります。", + "confirmTitle": "再起動が必要です", + "label": "ディープリンク処理を無効化", + "subLabel": "デスクトップアプリがhtmlファイルを開くよう設定されてしまうLinuxの不具合を回避するため、ディープリンク処理を無効にできます。" + }, "language": { "label": "言語", "system": "システムに合わせる" + }, + "namedConnections": { + "confirm": "証明書の処理方法を変更すると、すべての接続を再起動する必要があります。", + "confirmTitle": "続行しますか?", + "label": "名前付き接続", + "subLabel": "remote.itのHTTPS証明書を使用して、ローカル接続を処理し名前を付けます" + }, + "openAtLogin": "ログイン時に起動", + "reinstallAgent": { + "confirm": "システムエージェントを停止し、再インストールを試みます。", + "installing": "インストール中…", + "label": "システムエージェントを再インストール", + "version": "バージョン {{version}}" + }, + "resetGuides": { + "label": "インタラクティブガイドをリセット", + "subLabel": "アプリ内のヘルプ吹き出しを再度表示します。" + }, + "showFullInterface": { + "confirm": "新しい接続は{{device}}からのものになり、ローカルマシンからではなくなります。", + "label": "フルインターフェースを表示", + "subLabel": "リモートデバイスではターゲットの構成オプションのみが表示されます。フルアクセスを有効にするにはオンにしてください。" + }, + "showSupportFiles": { + "label": "サポートファイルを表示", + "subLabel": "アプリケーションのログと設定ファイルが含まれるフォルダーを表示します。" + }, + "sshConfig": { + "label": "管理対象SSH設定", + "subLabel": "remote.itが管理するSSH設定ファイルを使用して、デバイスへのアクセスを容易にすることを許可します。" + }, + "theme": { + "dark": "ダーク", + "label": "テーマ", + "light": "ライト", + "system": "システムに合わせる" + }, + "thisDevice": "このデバイス", + "title": "アプリケーション", + "uninstall": { + "confirm": "このシステムをホストとして削除し、接続とコマンドラインユーティリティも削除されます。", + "label": "アンインストール", + "subLabel": "このデバイスの登録を解除し、保存されたすべてのデータを完全に削除して、システムエージェントとコマンドラインツールをアンインストールします。アプリケーションをシステムから削除する前に実行してください。デバイスの所有者のみが実行できます。" } } } diff --git a/frontend/src/i18n/locales/ja/cognito.json b/frontend/src/i18n/locales/ja/cognito.json index 54596f6fe..4e84b3739 100644 --- a/frontend/src/i18n/locales/ja/cognito.json +++ b/frontend/src/i18n/locales/ja/cognito.json @@ -3,6 +3,7 @@ "connect": "モバイルアプリを使えば、どこからでも接続できます。", "unknown-os": "iOSおよびAndroidに対応しています。" }, + "CUSTOM CHALLENGE": "", "global": { "action": { "continue": "続ける", @@ -38,9 +39,11 @@ "start-recovery": "復旧を開始", "title": "二要素認証によるアカウント復旧" }, - "auth-mfa-totp": { - "invalid-code": "無効なコードです", - "title": "二要素認証" + "auth": { + "create-account": { + "button-label": "アカウントを作成", + "button-loading": "作成中..." + } }, "auth-mfa": { "errors": { @@ -54,11 +57,14 @@ "totp-mfa-verification": "認証アプリで生成されたワンタイムパスワードを入力してください。", "verification-code-placeholder": "確認コードを入力" }, - "auth": { - "create-account": { - "button-label": "アカウントを作成", - "button-loading": "作成中..." - } + "auth-mfa-totp": { + "invalid-code": "無効なコードです", + "title": "二要素認証" + }, + "forgot-password": { + "email": "メールアドレス", + "reset-password": "パスワードをリセット", + "title": "パスワードをお忘れですか" }, "forgot-password-verify": { "password-error": { @@ -73,16 +79,17 @@ "password-rules": "パスワードは{{min_length}}〜{{max_length}}文字で、数字、大文字と小文字、そして次の記号のいずれかを1つ以上含める必要があります ", "password-rules-reduced": "パスワードは{{min_length}}〜{{max_length}}文字である必要があります。", "password-strength-html": "パスワードの強度: {{strength}}", - "password-strength-score": ["非常に弱い", "弱い", "普通", "良い", "強い"], + "password-strength-score": { + "0": "非常に弱い", + "1": "弱い", + "2": "普通", + "3": "良い", + "4": "強い" + }, "success-message": "パスワードがリセットされました。サインインしてください", "title": "アカウントを確認", "verification-link-message": "確認用のリンクをメールで送信しました。リンクをクリックした後、このページに戻ってください。その後「続ける」を押してサインインしてください。" }, - "forgot-password": { - "email": "メールアドレス", - "reset-password": "パスワードをリセット", - "title": "パスワードをお忘れですか" - }, "password-reset": { "cognito-reset-password-required": "セキュリティ上の理由により、パスワードは定期的に変更していただく必要があります。確認コードを{{email}}宛てに送信しましたので、メールをご確認のうえ、確認コードを入力してパスワードを更新してください。", "signout-all-text": "現在のセッションは有効なままです。セキュリティ侵害が発生した場合は、すべての端末からサインアウトすることをお勧めします。すべての端末からサインアウトするには、app.remote.itにサインインし、アカウントページからサインアウトを行ってください。", @@ -108,10 +115,10 @@ "title": "パスワードの更新" }, "verify-account": { - "sending": "送信中...", - "resent-notice": "確認メールを再送信しました。", "check-spam": "迷惑メールフォルダをご確認ください", "resend-confirmation": "アカウント確認メールを再送信する", + "resent-notice": "確認メールを再送信しました。", + "sending": "送信中...", "verification-received-message": "確認用リンクが届きませんか?" } } diff --git a/frontend/src/pages/OptionsPage.tsx b/frontend/src/pages/OptionsPage.tsx index 38379db38..854aca621 100644 --- a/frontend/src/pages/OptionsPage.tsx +++ b/frontend/src/pages/OptionsPage.tsx @@ -40,28 +40,28 @@ export const OptionsPage: React.FC = () => { bodyProps={{ gutterTop: true, verticalOverflow: true }} header={ - Application + {t('options.title', 'Application')} } > ui.setTheme(e.target.value as State['ui']['themeMode'])} /> ui.setLanguage(e.target.value as State['ui']['language'])} @@ -69,27 +69,36 @@ export const OptionsPage: React.FC = () => { {browser.isRemote && ( emit('preferences', { ...preferences, remoteUIOverride: !preferences.remoteUIOverride })} confirmProps={{ - title: 'Are you sure?', - children: `New connections will be from ${thisDevice?.name || 'this device'} and not your local machine.`, + title: t('common.areYouSure', 'Are you sure?'), + children: t('options.showFullInterface.confirm', { + device: thisDevice?.name || t('options.thisDevice', 'this device'), + defaultValue: 'New connections will be from {{device}} and not your local machine.', + }), }} /> )} emit('preferences', { ...preferences, openAtLogin: !preferences.openAtLogin })} /> emit('sshConfig', !preferences.sshConfig)} @@ -98,20 +107,26 @@ export const OptionsPage: React.FC = () => { /> emit('useCertificate', !preferences.useCertificate)} confirmProps={{ - title: 'Continue?', - children: 'Changing the certificate handling will require we restart all connections.', + title: t('options.namedConnections.confirmTitle', 'Continue?'), + children: t( + 'options.namedConnections.confirm', + 'Changing the certificate handling will require we restart all connections.' + ), }} /> ui.resetHelp()} /> @@ -119,52 +134,68 @@ export const OptionsPage: React.FC = () => { {!remoteUI && ( - + emit('service/clearErrors')} /> emit('preferences', { ...preferences, disableDeepLinks: !preferences.disableDeepLinks })} confirm confirmProps={{ - title: 'Restart required', - children: - 'Please restart the desktop application for changes to take affect. You may also have to reset your default file handler on Linux.', + title: t('options.disableDeepLinks.confirmTitle', 'Restart required'), + children: t( + 'options.disableDeepLinks.confirm', + 'Please restart the desktop application for changes to take affect. You may also have to reset your default file handler on Linux.' + ), }} /> binaries.install()} confirmProps={{ - title: 'Are you sure?', - children: 'This will stop and attempt to re-install the system agent.', + title: t('common.areYouSure', 'Are you sure?'), + children: t('options.reinstallAgent.confirm', 'This will stop and attempt to re-install the system agent.'), }} /> {os !== 'windows' && ( { emit('uninstall') @@ -173,8 +204,11 @@ export const OptionsPage: React.FC = () => { /> )} emit('showFolder', 'logs')} /> From 872112a03792af6a25e8ce053f2e5f51badfe3b8 Mon Sep 17 00:00:00 2001 From: Jamie Ruderman Date: Wed, 22 Jul 2026 16:25:16 -0700 Subject: [PATCH 7/9] feat(i18n): move app language to Profile, persist override to account cloud --- frontend/src/i18n/i18next.d.ts | 2 ++ frontend/src/i18n/index.ts | 2 ++ frontend/src/models/user.ts | 14 +++++++++++++- frontend/src/pages/OptionsPage.tsx | 12 ------------ frontend/src/pages/ProfilePage.tsx | 13 +++++++++++++ 5 files changed, 30 insertions(+), 13 deletions(-) diff --git a/frontend/src/i18n/i18next.d.ts b/frontend/src/i18n/i18next.d.ts index 07e03ff4e..64108d041 100644 --- a/frontend/src/i18n/i18next.d.ts +++ b/frontend/src/i18n/i18next.d.ts @@ -9,5 +9,7 @@ import 'i18next' declare module 'i18next' { interface CustomTypeOptions { defaultNS: 'app' + // Mirror the runtime init so t() is typed as always returning a string. + returnNull: false } } diff --git a/frontend/src/i18n/index.ts b/frontend/src/i18n/index.ts index 2ac03f79e..e53bc815b 100644 --- a/frontend/src/i18n/index.ts +++ b/frontend/src/i18n/index.ts @@ -53,6 +53,8 @@ i18n // An untranslated (empty-string) value must fall back to English, not render // blank — catalogs are seeded incrementally, so empties are expected. returnEmptyString: false, + // t() always returns a string (never null), so results slot into string props. + returnNull: false, partialBundledLanguages: true, resources: { en: { app: enApp, notices: enNotices, cognito: enCognito }, diff --git a/frontend/src/models/user.ts b/frontend/src/models/user.ts index 9b80cbd28..ad856a056 100644 --- a/frontend/src/models/user.ts +++ b/frontend/src/models/user.ts @@ -4,6 +4,7 @@ import { API_URL, DEVELOPER_KEY, LANGUAGES } from '../constants' import { graphQLNotificationSettings, graphQLSetAttributes, graphQLLeaveReseller } from '../services/graphQLMutation' import { graphQLUser } from '../services/graphQLRequest' import { RootModel } from '.' +import { LanguageMode } from '../i18n' import { getToken } from '../services/remoteit' type IUserState = { @@ -37,7 +38,12 @@ export default createModel()({ const result = await graphQLUser(account) if (result === 'ERROR') return const data = await dispatch.user.parse(result) - if (data) dispatch.user.set(data) + if (data) { + dispatch.user.set(data) + // The account is the source of truth for the app-language override, so it + // wins across devices. Absent override (null/undefined) means follow the OS. + dispatch.ui.setLanguage((data.attributes?.language ?? 'system') as LanguageMode) + } }, async parse(result: AxiosResponse | undefined) { const data = result?.data?.data?.login?.account @@ -61,6 +67,12 @@ export default createModel()({ dispatch.user.set({ attributes: { ...state.user.attributes, ...attribute } }) await graphQLSetAttributes(attribute) }, + // Persists the app-language override to the account (cloud) and applies it now. + // 'system' clears the override so the app follows the OS language everywhere. + async setAppLanguage(language: string) { + dispatch.user.setAttribute({ language: language === 'system' ? null : language }) + dispatch.ui.setLanguage(language as LanguageMode) + }, async updateNotificationSettings(metadata: INotificationSetting) { const result = await graphQLNotificationSettings(metadata) if (result === 'ERROR') return diff --git a/frontend/src/pages/OptionsPage.tsx b/frontend/src/pages/OptionsPage.tsx index 854aca621..4d012fd63 100644 --- a/frontend/src/pages/OptionsPage.tsx +++ b/frontend/src/pages/OptionsPage.tsx @@ -4,7 +4,6 @@ import { emit } from '../services/Controller' import { State, Dispatch } from '../store' import { List, Typography } from '@mui/material' import { useTranslation } from 'react-i18next' -import { SUPPORTED_LANGUAGES } from '../i18n' import { useSelector, useDispatch } from 'react-redux' import { SettingsDisableNetworkItem } from '../components/SettingsDisableNetworkItem' import { AccordionMenuItem } from '../components/AccordionMenuItem' @@ -28,7 +27,6 @@ export const OptionsPage: React.FC = () => { (state: State) => !!state.backend.thisId && !selectOwnDevices(state).find(d => d.thisDevice) ) const themeMode = useSelector((state: State) => state.ui.themeMode) - const language = useSelector((state: State) => state.ui.language) const remoteUI = useSelector((state: State) => isRemoteUI(state)) const { t } = useTranslation() @@ -56,16 +54,6 @@ export const OptionsPage: React.FC = () => { ]} onChange={e => ui.setTheme(e.target.value as State['ui']['themeMode'])} /> - ui.setLanguage(e.target.value as State['ui']['language'])} - /> {browser.isRemote && ( { deleteAccount: state.ui.deleteAccount, })) const dispatch = useDispatch() + const { t } = useTranslation() if (!user) return null @@ -61,6 +64,16 @@ export const ProfilePage: React.FC = () => { displayValue={} displayOnly /> + ({ key: l.value, name: l.label })), + ]} + onChange={value => dispatch.user.setAppLanguage(value)} + /> Date: Fri, 24 Jul 2026 09:38:46 -0700 Subject: [PATCH 8/9] style(i18n): use language icon for Profile app-language picker --- frontend/src/pages/ProfilePage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/ProfilePage.tsx b/frontend/src/pages/ProfilePage.tsx index 34786dbe0..e63cafb9b 100644 --- a/frontend/src/pages/ProfilePage.tsx +++ b/frontend/src/pages/ProfilePage.tsx @@ -65,7 +65,7 @@ export const ProfilePage: React.FC = () => { displayOnly /> Date: Fri, 24 Jul 2026 10:01:22 -0700 Subject: [PATCH 9/9] style(i18n): use envelope icon for Email Language --- frontend/src/pages/ProfilePage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/ProfilePage.tsx b/frontend/src/pages/ProfilePage.tsx index e63cafb9b..a6046a4a2 100644 --- a/frontend/src/pages/ProfilePage.tsx +++ b/frontend/src/pages/ProfilePage.tsx @@ -75,7 +75,7 @@ export const ProfilePage: React.FC = () => { onChange={value => dispatch.user.setAppLanguage(value)} />