diff --git a/package.json b/package.json index aab7f2dfa99..ef6ddde4a98 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "build:browser": "./bin/package-browser", "build:desktop": "./bin/package-electron", "build:api": "yarn workspace @actual-app/api build", + "generate:i18n": "yarn workspace @actual-app/web generate:i18n", "test": "yarn workspaces foreach --all --parallel --verbose run test", "test:debug": "yarn workspaces foreach --all --verbose run test", "e2e": "yarn workspaces foreach --all --parallel --verbose run e2e", diff --git a/packages/desktop-client/i18next-parser.config.js b/packages/desktop-client/i18next-parser.config.js new file mode 100644 index 00000000000..eaab75dae07 --- /dev/null +++ b/packages/desktop-client/i18next-parser.config.js @@ -0,0 +1,14 @@ +module.exports = { + input: ['src/**/*.{js,jsx,ts,tsx}', '../loot-core/src/**/*.{js,jsx,ts,tsx}'], + output: 'src/locale/$LOCALE.json', + locales: ['en'], + sort: true, + keySeparator: false, + namespaceSeparator: false, + defaultValue: (locale, ns, key, value) => { + if (locale === 'en') { + return value || key; + } + return ''; + }, +}; diff --git a/packages/desktop-client/package.json b/packages/desktop-client/package.json index 938e51cc0c7..8aa94a5bf88 100644 --- a/packages/desktop-client/package.json +++ b/packages/desktop-client/package.json @@ -39,6 +39,9 @@ "downshift": "7.6.2", "focus-visible": "^4.1.5", "glamor": "^2.20.40", + "i18next": "^23.11.5", + "i18next-parser": "^9.0.0", + "i18next-resources-to-backend": "^1.2.1", "inter-ui": "^3.19.3", "jest": "^27.5.1", "jest-watch-typeahead": "^2.2.2", @@ -55,6 +58,7 @@ "react-dom": "18.2.0", "react-error-boundary": "^4.0.12", "react-hotkeys-hook": "^4.5.0", + "react-i18next": "^14.1.2", "react-markdown": "^8.0.7", "react-modal": "3.16.1", "react-redux": "7.2.9", @@ -86,6 +90,7 @@ "build": "vite build", "build:browser": "cross-env ./bin/build-browser", "generate:icons": "rm src/icons/*/*.tsx; cd src/icons && svgr --template template.ts --index-template index-template.ts --typescript --expand-props start -d . .", + "generate:i18n": "i18next", "test": "vitest", "e2e": "npx playwright test --browser=chromium", "vrt": "cross-env VRT=true npx playwright test --browser=chromium" diff --git a/packages/desktop-client/src/components/App.tsx b/packages/desktop-client/src/components/App.tsx index b6e86047d3f..18ee27bf250 100644 --- a/packages/desktop-client/src/components/App.tsx +++ b/packages/desktop-client/src/components/App.tsx @@ -6,6 +6,7 @@ import { type FallbackProps, } from 'react-error-boundary'; import { HotkeysProvider } from 'react-hotkeys-hook'; +import { useTranslation } from 'react-i18next'; import { useDispatch, useSelector } from 'react-redux'; import { @@ -41,6 +42,7 @@ type AppInnerProps = { }; function AppInner({ budgetId, cloudFileId }: AppInnerProps) { + const { t } = useTranslation(); const [initializing, setInitializing] = useState(true); const { showBoundary: showErrorBoundary } = useErrorBoundary(); const loadingText = useSelector((state: State) => state.app.loadingText); @@ -51,7 +53,7 @@ function AppInner({ budgetId, cloudFileId }: AppInnerProps) { dispatch( setAppState({ - loadingText: 'Initializing the connection to the local database...', + loadingText: t('Initializing the connection to the local database...'), }), ); await initConnection(socketName); @@ -59,7 +61,7 @@ function AppInner({ budgetId, cloudFileId }: AppInnerProps) { // Load any global prefs dispatch( setAppState({ - loadingText: 'Loading global preferences...', + loadingText: t('Loading global preferences...'), }), ); await dispatch(loadGlobalPrefs()); @@ -67,18 +69,20 @@ function AppInner({ budgetId, cloudFileId }: AppInnerProps) { // Open the last opened budget, if any dispatch( setAppState({ - loadingText: 'Opening last budget...', + loadingText: t('Opening last budget...'), }), ); const budgetId = await send('get-last-opened-backup'); if (budgetId) { - await dispatch(loadBudget(budgetId, 'Loading the last budget file...')); + await dispatch( + loadBudget(budgetId, t('Loading the last budget file...')), + ); // Check to see if this file has been remotely deleted (but // don't block on this in case they are offline or something) dispatch( setAppState({ - loadingText: 'Retrieving remote files...', + loadingText: t('Retrieving remote files...'), }), ); send('get-remote-files').then(files => { diff --git a/packages/desktop-client/src/components/common/Menu.tsx b/packages/desktop-client/src/components/common/Menu.tsx index 8320f955675..037a110d748 100644 --- a/packages/desktop-client/src/components/common/Menu.tsx +++ b/packages/desktop-client/src/components/common/Menu.tsx @@ -30,7 +30,7 @@ function Keybinding({ keyName }: KeybindingProps) { ); } -type MenuItem = { +export type MenuItem = { type?: string | symbol; name: string; disabled?: boolean; diff --git a/packages/desktop-client/src/components/manager/ConfigServer.tsx b/packages/desktop-client/src/components/manager/ConfigServer.tsx index 5d4864dbbfd..7ac54bc875c 100644 --- a/packages/desktop-client/src/components/manager/ConfigServer.tsx +++ b/packages/desktop-client/src/components/manager/ConfigServer.tsx @@ -1,5 +1,6 @@ // @ts-strict-ignore import React, { useState, useEffect } from 'react'; +import { Trans, useTranslation } from 'react-i18next'; import { isNonProductionEnvironment, @@ -20,6 +21,7 @@ import { Title } from './subscribe/common'; export function ConfigServer() { useSetThemeColor(theme.mobileConfigServerViewTheme); + const { t } = useTranslation(); const { createBudget, signOut, loggedIn } = useActions(); const navigate = useNavigate(); const [url, setUrl] = useState(''); @@ -34,9 +36,13 @@ export function ConfigServer() { function getErrorMessage(error: string) { switch (error) { case 'network-failure': - return 'Server is not running at this URL. Make sure you have HTTPS set up properly.'; + return t( + 'Server is not running at this URL. Make sure you have HTTPS set up properly.', + ); default: - return 'Server does not look like an Actual server. Is it set up correctly?'; + return t( + 'Server does not look like an Actual server. Is it set up correctly?', + ); } } @@ -91,7 +97,7 @@ export function ConfigServer() { return ( - + <Title text={t('Where’s the server?')} /> <Text style={{ @@ -101,16 +107,16 @@ export function ConfigServer() { }} > {currentUrl ? ( - <> + <Trans> Existing sessions will be logged out and you will log in to this server. We will validate that Actual is running at this URL. - </> + </Trans> ) : ( - <> + <Trans> There is no server configured. After running the server, specify the URL here to use the app. You can always change this later. We will validate that Actual is running at this URL. - </> + </Trans> )} </Text> @@ -130,7 +136,7 @@ export function ConfigServer() { <View style={{ display: 'flex', flexDirection: 'row', marginTop: 30 }}> <BigInput autoFocus={true} - placeholder="https://example.com" + placeholder={t('https://example.com')} value={url || ''} onChangeValue={setUrl} style={{ flex: 1, marginRight: 10 }} @@ -142,7 +148,7 @@ export function ConfigServer() { style={{ fontSize: 15 }} onPress={onSubmit} > - OK + {t('OK')} </ButtonWithLoading> {currentUrl && ( <Button @@ -150,7 +156,7 @@ export function ConfigServer() { style={{ fontSize: 15, marginLeft: 10 }} onPress={() => navigate(-1)} > - Cancel + {t('Cancel')} </Button> )} </View> @@ -169,7 +175,7 @@ export function ConfigServer() { style={{ color: theme.pageTextLight }} onPress={onSkip} > - Stop using a server + {t('Stop using a server')} </Button> ) : ( <> @@ -183,7 +189,7 @@ export function ConfigServer() { }} onPress={onSameDomain} > - Use current domain + {t('Use current domain')} </Button> )} <Button @@ -191,7 +197,7 @@ export function ConfigServer() { style={{ color: theme.pageTextLight, margin: 5 }} onPress={onSkip} > - Don’t use a server + {t('Don’t use a server')} </Button> {isNonProductionEnvironment() && ( @@ -200,7 +206,7 @@ export function ConfigServer() { style={{ marginLeft: 15 }} onPress={onCreateTestFile} > - Create test file + {t('Create test file')} </Button> )} </> diff --git a/packages/desktop-client/src/components/manager/WelcomeScreen.tsx b/packages/desktop-client/src/components/manager/WelcomeScreen.tsx index c98f908d41f..8dcc9f4f3ae 100644 --- a/packages/desktop-client/src/components/manager/WelcomeScreen.tsx +++ b/packages/desktop-client/src/components/manager/WelcomeScreen.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { Trans, useTranslation } from 'react-i18next'; import { useActions } from '../../hooks/useActions'; import { styles, theme } from '../../style'; @@ -9,6 +10,7 @@ import { Text } from '../common/Text'; import { View } from '../common/View'; export function WelcomeScreen() { + const { t } = useTranslation(); const { createBudget, pushModal } = useActions(); return ( @@ -21,39 +23,50 @@ export function WelcomeScreen() { marginBlock: 20, }} > - <Text style={styles.veryLargeText}>Let’s get started!</Text> + <Text style={styles.veryLargeText}>{t('Let’s get started!')}</Text> <View style={{ overflowY: 'auto' }}> <Paragraph> - Actual is a personal finance tool that focuses on beautiful design and - a slick user experience.{' '} - <strong>Editing your data should be as fast as possible.</strong> On - top of that, we want to provide powerful tools to allow you to do - whatever you want with your data. + <Trans> + Actual is a personal finance tool that focuses on beautiful design + and a slick user experience.{' '} + <strong>Editing your data should be as fast as possible.</strong> On + top of that, we want to provide powerful tools to allow you to do + whatever you want with your data. + </Trans> </Paragraph> <Paragraph> - Currently, Actual implements budgeting based on a{' '} - <Link - variant="external" - to="https://actualbudget.org/docs/budgeting/" - linkColor="purple" - > - monthly envelope system - </Link> - . Consider taking our{' '} - <Link - variant="external" - to="https://actualbudget.org/docs/tour/" - linkColor="purple" - > - guided tour - </Link>{' '} - to help you get your bearings, and check out the rest of the - documentation while you’re there to learn more about advanced topics. + <Trans> + Currently, Actual implements budgeting based on a{' '} + <Link + variant="external" + to="https://actualbudget.org/docs/budgeting/" + linkColor="purple" + > + monthly envelope system + </Link> + . + </Trans>{' '} + <Trans> + Consider taking our{' '} + <Link + variant="external" + to="https://actualbudget.org/docs/tour/" + linkColor="purple" + > + guided tour + </Link>{' '} + to help you get your bearings, and check out the rest of the + documentation while you’re there to learn more about advanced + topics. + </Trans> </Paragraph> <Paragraph style={{ color: theme.pageTextLight }}> - Get started by importing an existing budget file from Actual or - another budgeting app, create a demo budget file, or start fresh with - an empty budget. You can always create or import another budget later. + <Trans> + Get started by importing an existing budget file from Actual or + another budgeting app, create a demo budget file, or start fresh + with an empty budget. You can always create or import another budget + later. + </Trans> </Paragraph> </View> <View @@ -64,7 +77,9 @@ export function WelcomeScreen() { flexShrink: 0, }} > - <Button onPress={() => pushModal('import')}>Import my budget</Button> + <Button onPress={() => pushModal('import')}> + {t('Import my budget')} + </Button> <View style={{ flexDirection: 'row', @@ -73,10 +88,10 @@ export function WelcomeScreen() { }} > <Button onPress={() => createBudget({ testMode: true })}> - View demo + {t('View demo')} </Button> <Button variant="primary" onPress={() => createBudget()}> - Start fresh + {t('Start fresh')} </Button> </View> </View> diff --git a/packages/desktop-client/src/components/manager/subscribe/Bootstrap.tsx b/packages/desktop-client/src/components/manager/subscribe/Bootstrap.tsx index 716c67425c3..863c469c1aa 100644 --- a/packages/desktop-client/src/components/manager/subscribe/Bootstrap.tsx +++ b/packages/desktop-client/src/components/manager/subscribe/Bootstrap.tsx @@ -1,5 +1,6 @@ // @ts-strict-ignore import React, { useState } from 'react'; +import { Trans, useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; import { createBudget } from 'loot-core/src/client/actions/budgets'; @@ -17,6 +18,7 @@ import { useBootstrapped, Title } from './common'; import { ConfirmPasswordForm } from './ConfirmPasswordForm'; export function Bootstrap() { + const { t } = useTranslation(); const dispatch = useDispatch(); const [error, setError] = useState(null); @@ -25,13 +27,13 @@ export function Bootstrap() { function getErrorMessage(error) { switch (error) { case 'invalid-password': - return 'Password cannot be empty'; + return t('Password cannot be empty'); case 'password-match': - return 'Passwords do not match'; + return t('Passwords do not match'); case 'network-failure': - return 'Unable to contact the server'; + return t('Unable to contact the server'); default: - return `An unknown error occurred: ${error}`; + return t(`An unknown error occurred: {{error}}`, { error }); } } @@ -56,19 +58,23 @@ export function Bootstrap() { return ( <View style={{ maxWidth: 450, marginTop: -30 }}> - <Title text="Welcome to Actual!" /> + <Title text={t('Welcome to Actual!')} /> <Paragraph style={{ fontSize: 16, color: theme.pageTextDark }}> - Actual is a super fast privacy-focused app for managing your finances. - To secure your data, you’ll need to set a password for your server. + <Trans> + Actual is a super fast privacy-focused app for managing your finances. + To secure your data, you’ll need to set a password for your server. + </Trans> </Paragraph> <Paragraph isLast style={{ fontSize: 16, color: theme.pageTextDark }}> - Consider opening{' '} - <Link variant="external" to="https://actualbudget.org/docs/tour/"> - our tour - </Link>{' '} - in a new tab for some guidance on what to do when you’ve set your - password. + <Trans> + Consider opening{' '} + <Link variant="external" to="https://actualbudget.org/docs/tour/"> + our tour + </Link>{' '} + in a new tab for some guidance on what to do when you’ve set your + password. + </Trans> </Paragraph> {error && ( @@ -91,7 +97,7 @@ export function Bootstrap() { style={{ fontSize: 15, color: theme.pageTextLink, marginRight: 15 }} onPress={onDemo} > - Try Demo + {t('Try Demo')} </Button> } onSetPassword={onSetPassword} diff --git a/packages/desktop-client/src/components/schedules/ScheduleDetails.jsx b/packages/desktop-client/src/components/schedules/ScheduleDetails.jsx index 995a8aa7163..a91aeb3b6d0 100644 --- a/packages/desktop-client/src/components/schedules/ScheduleDetails.jsx +++ b/packages/desktop-client/src/components/schedules/ScheduleDetails.jsx @@ -1,6 +1,8 @@ import React, { useEffect, useReducer } from 'react'; import { useDispatch } from 'react-redux'; +import { t } from 'i18next'; + import { getPayeesById } from 'loot-core/client/reducers/queries'; import { pushModal } from 'loot-core/src/client/actions/modals'; import { runQuery, liveQuery } from 'loot-core/src/client/query-helpers'; @@ -735,7 +737,8 @@ export function ScheduleDetails({ id, transaction }) { </Button> <View style={{ flex: 1 }} /> <SelectedItemsButton - name="transactions" + id="transactions" + name={count => t('{{count}} transactions', { count })} items={ state.transactionsMode === 'linked' ? [{ name: 'unlink', text: 'Unlink from schedule' }] diff --git a/packages/desktop-client/src/components/table.tsx b/packages/desktop-client/src/components/table.tsx index 90fd3fae9a8..4d47f531511 100644 --- a/packages/desktop-client/src/components/table.tsx +++ b/packages/desktop-client/src/components/table.tsx @@ -31,7 +31,7 @@ import { type CSSProperties, styles, theme } from '../style'; import { Button } from './common/Button'; import { Input } from './common/Input'; -import { Menu } from './common/Menu'; +import { Menu, type MenuItem } from './common/Menu'; import { Popover } from './common/Popover'; import { Text } from './common/Text'; import { View } from './common/View'; @@ -817,7 +817,19 @@ export function TableHeader({ ); } -export function SelectedItemsButton({ name, items, onSelect }) { +type SelectedItemsButtonProps<T extends MenuItem = MenuItem> = { + id: string; + name: ((count: number) => string) | string; + items: Array<T | typeof Menu.line>; + onSelect: (name: string, items: Array<string>) => void; +}; + +export function SelectedItemsButton<T extends MenuItem = MenuItem>({ + id, + name, + items, + onSelect, +}: SelectedItemsButtonProps<T>) { const selectedItems = useSelectedItems(); const [menuOpen, setMenuOpen] = useState(null); const triggerRef = useRef(null); @@ -826,6 +838,9 @@ export function SelectedItemsButton({ name, items, onSelect }) { return null; } + const buttonLabel = + typeof name === 'function' ? name(selectedItems.size) : name; + return ( <View style={{ marginLeft: 10, flexShrink: 0 }}> <Button @@ -833,14 +848,14 @@ export function SelectedItemsButton({ name, items, onSelect }) { type="bare" style={{ color: theme.pageTextPositive }} onClick={() => setMenuOpen(true)} - data-testid={name + '-select-button'} + data-testid={id + '-select-button'} > <SvgExpandArrow width={8} height={8} style={{ marginRight: 5, color: theme.pageText }} /> - {selectedItems.size} {name} + {buttonLabel} </Button> <Popover @@ -852,7 +867,7 @@ export function SelectedItemsButton({ name, items, onSelect }) { }} isOpen={menuOpen} onOpenChange={() => setMenuOpen(false)} - data-testid={name + '-select-tooltip'} + data-testid={id + '-select-tooltip'} > <Menu onMenuSelect={name => { @@ -914,13 +929,13 @@ type TableProps<T extends TableItem = TableItem> = { position: number; }) => ReactNode; renderEmpty?: ReactNode | (() => ReactNode); - getItemKey?: (index: number) => TableItem['id']; + getItemKey?: (index: number) => T['id']; loadMore?: () => void; style?: CSSProperties; navigator?: ReturnType<typeof useTableNavigator<T>>; listContainerRef?: MutableRefObject<HTMLDivElement>; onScroll?: () => void; - isSelected?: (id: TableItem['id']) => boolean; + isSelected?: (id: T['id']) => boolean; saveScrollWidth?: (parent, child) => void; }; diff --git a/packages/desktop-client/src/components/transactions/SelectedTransactionsButton.jsx b/packages/desktop-client/src/components/transactions/SelectedTransactionsButton.jsx index a6000bc5267..ad9cded8a56 100644 --- a/packages/desktop-client/src/components/transactions/SelectedTransactionsButton.jsx +++ b/packages/desktop-client/src/components/transactions/SelectedTransactionsButton.jsx @@ -1,5 +1,6 @@ -import React, { useMemo } from 'react'; +import { useMemo } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; +import { useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; import { pushModal } from 'loot-core/client/actions'; @@ -25,6 +26,7 @@ export function SelectedTransactionsButton({ onMakeAsSplitTransaction, onMakeAsNonSplitTransactions, }) { + const { t } = useTranslation(); const dispatch = useDispatch(); const selectedItems = useSelectedItems(); const selectedIds = useMemo(() => [...selectedItems], [selectedItems]); @@ -40,7 +42,7 @@ export function SelectedTransactionsButton({ const ambiguousDuplication = useMemo(() => { const transactions = selectedIds.map(id => getTransaction(id)); - return transactions.some(t => t && t.is_child); + return transactions.some(tx => tx && tx.is_child); }, [selectedIds, getTransaction]); const linked = useMemo(() => { @@ -78,16 +80,16 @@ export function SelectedTransactionsButton({ const [firstTransaction] = transactions; const areAllSameDateAndAccount = transactions.every( - t => - t && - t.date === firstTransaction.date && - t.account === firstTransaction.account, + tx => + tx && + tx.date === firstTransaction.date && + tx.account === firstTransaction.account, ); const areNoSplitTransactions = transactions.every( - t => t && !t.is_parent && !t.is_child, + tx => tx && !tx.is_parent && !tx.is_child, ); const areNoReconciledTransactions = transactions.every( - t => t && !t.reconciled, + tx => tx && !tx.reconciled, ); return ( @@ -105,10 +107,10 @@ export function SelectedTransactionsButton({ const transactions = selectedIds.map(id => getTransaction(id)); const areNoReconciledTransactions = transactions.every( - t => t && !t.reconciled, + tx => tx && !tx.reconciled, ); const areAllSplitTransactions = transactions.every( - t => t && (t.is_parent || t.is_child), + tx => tx && (tx.is_parent || tx.is_child), ); return areNoReconciledTransactions && areAllSplitTransactions; }, [selectedIds, types, getTransaction]); @@ -182,48 +184,49 @@ export function SelectedTransactionsButton({ return ( <SelectedItemsButton - name="transactions" + id="transactions" + name={count => t('{{count}} transactions', { count })} items={[ ...(!types.trans ? [ - { name: 'view-schedule', text: 'View schedule', key: 'S' }, - { name: 'post-transaction', text: 'Post transaction' }, - { name: 'skip', text: 'Skip scheduled date' }, + { name: 'view-schedule', text: t('View schedule'), key: 'S' }, + { name: 'post-transaction', text: t('Post transaction') }, + { name: 'skip', text: t('Skip scheduled date') }, ] : [ - { name: 'show', text: 'Show', key: 'F' }, + { name: 'show', text: t('Show'), key: 'F' }, { name: 'duplicate', - text: 'Duplicate', + text: t('Duplicate'), disabled: ambiguousDuplication, }, - { name: 'delete', text: 'Delete', key: 'D' }, + { name: 'delete', text: t('Delete'), key: 'D' }, ...(linked ? [ { name: 'view-schedule', - text: 'View schedule', + text: t('View schedule'), key: 'S', disabled: selectedIds.length > 1, }, - { name: 'unlink-schedule', text: 'Unlink schedule' }, + { name: 'unlink-schedule', text: t('Unlink schedule') }, ] : [ { name: 'link-schedule', - text: 'Link schedule', + text: t('Link schedule'), key: 'S', }, { name: 'create-rule', - text: 'Create rule', + text: t('Create rule'), }, ]), ...(showMakeTransfer ? [ { name: 'set-transfer', - text: 'Make transfer', + text: t('Make transfer'), disabled: !canBeTransfer, }, ] @@ -232,7 +235,7 @@ export function SelectedTransactionsButton({ ? [ { name: 'make-as-split-transaction', - text: 'Make as split transaction', + text: t('Make as split transaction'), }, ] : []), @@ -240,21 +243,21 @@ export function SelectedTransactionsButton({ ? [ { name: 'unsplit-transactions', - text: - 'Unsplit transaction' + - (selectedIds.length > 1 ? 's' : ''), + text: t('Unsplit {{count}} transactions', { + count: selectedIds.length, + }), }, ] : []), Menu.line, - { type: Menu.label, name: 'Edit field' }, - { name: 'date', text: 'Date' }, - { name: 'account', text: 'Account', key: 'A' }, - { name: 'payee', text: 'Payee', key: 'P' }, - { name: 'notes', text: 'Notes', key: 'N' }, - { name: 'category', text: 'Category', key: 'C' }, - { name: 'amount', text: 'Amount' }, - { name: 'cleared', text: 'Cleared', key: 'L' }, + { type: Menu.label, name: t('Edit field') }, + { name: 'date', text: t('Date') }, + { name: 'account', text: t('Account'), key: 'A' }, + { name: 'payee', text: t('Payee'), key: 'P' }, + { name: 'notes', text: t('Notes'), key: 'N' }, + { name: 'category', text: t('Category'), key: 'C' }, + { name: 'amount', text: t('Amount') }, + { name: 'cleared', text: t('Cleared'), key: 'L' }, ]), ]} onSelect={name => { diff --git a/packages/desktop-client/src/hooks/useSelected.tsx b/packages/desktop-client/src/hooks/useSelected.tsx index 63a0437a815..1b38189e6ae 100644 --- a/packages/desktop-client/src/hooks/useSelected.tsx +++ b/packages/desktop-client/src/hooks/useSelected.tsx @@ -246,7 +246,7 @@ export function useSelected<T extends Item>( } const SelectedDispatch = createContext<(action: Actions) => void>(null); -const SelectedItems = createContext<Set<unknown>>(null); +const SelectedItems = createContext<Set<string>>(null); export function useSelectedDispatch() { return useContext(SelectedDispatch); diff --git a/packages/desktop-client/src/i18n.ts b/packages/desktop-client/src/i18n.ts new file mode 100644 index 00000000000..70ad5456da5 --- /dev/null +++ b/packages/desktop-client/src/i18n.ts @@ -0,0 +1,29 @@ +import { initReactI18next } from 'react-i18next'; + +import i18n from 'i18next'; +import resourcesToBackend from 'i18next-resources-to-backend'; + +const loadLanguage = (language: string) => { + return import(`./locale/${language}.json`); +}; + +i18n + .use(initReactI18next) + .use(resourcesToBackend(loadLanguage)) + .init({ + // While we mark all strings for translations, one can test + // it by setting the language in localStorage to their choice. + lng: localStorage.getItem('language') || 'cimode', + + // allow keys to be phrases having `:`, `.` + nsSeparator: false, + keySeparator: false, + // do not load a fallback + fallbackLng: false, + interpolation: { + escapeValue: false, + }, + react: { + transSupportBasicHtmlNodes: false, + }, + }); diff --git a/packages/desktop-client/src/index.tsx b/packages/desktop-client/src/index.tsx index 3d6a2d3e51f..0d64a88cb50 100644 --- a/packages/desktop-client/src/index.tsx +++ b/packages/desktop-client/src/index.tsx @@ -5,6 +5,8 @@ import './browser-preload'; import './fonts.scss'; +import './i18n'; + import React from 'react'; import { Provider } from 'react-redux'; diff --git a/packages/loot-core/package.json b/packages/loot-core/package.json index a68b93f12a4..37cafefc4c0 100644 --- a/packages/loot-core/package.json +++ b/packages/loot-core/package.json @@ -9,6 +9,7 @@ "build:api": "cross-env NODE_ENV=development ./bin/build-api", "build:browser": "cross-env NODE_ENV=production ./bin/build-browser", "watch:browser": "cross-env NODE_ENV=development ./bin/build-browser", + "generate:i18n": "i18next", "test": "npm-run-all -cp 'test:*'", "test:node": "jest -c jest.config.js", "test:web": "jest -c jest.web.config.js" @@ -58,6 +59,7 @@ "cross-env": "^7.0.3", "fake-indexeddb": "^3.1.8", "fast-check": "3.15.0", + "i18next": "^23.11.5", "jest": "^27.5.1", "jsverify": "^0.8.4", "memfs": "3.5.3", diff --git a/packages/loot-core/src/client/actions/budgets.ts b/packages/loot-core/src/client/actions/budgets.ts index 7d7c8f5a199..e18f2c761a1 100644 --- a/packages/loot-core/src/client/actions/budgets.ts +++ b/packages/loot-core/src/client/actions/budgets.ts @@ -1,4 +1,6 @@ // @ts-strict-ignore +import { t } from 'i18next'; + import { send } from '../../platform/client/fetch'; import { getDownloadError, getSyncError } from '../../shared/errors'; import type { Handlers } from '../../types/handlers'; @@ -60,14 +62,17 @@ export function loadBudget(id: string, loadingText = '', options = {}) { if (typeof window.confirm !== 'undefined') { const showBackups = window.confirm( message + - ' Make sure the app is up-to-date. Do you want to load a backup?', + ' ' + + t( + 'Make sure the app is up-to-date. Do you want to load a backup?', + ), ); if (showBackups) { dispatch(pushModal('load-backup', { budgetId: id })); } } else { - alert(message + ' Make sure the app is up-to-date.'); + alert(message + ' ' + t('Make sure the app is up-to-date.')); } } else { alert(message); @@ -92,7 +97,7 @@ export function closeBudget() { // This clears out all the app state so the user starts fresh dispatch({ type: constants.CLOSE_BUDGET }); - dispatch(setAppState({ loadingText: 'Closing...' })); + dispatch(setAppState({ loadingText: t('Closing...') })); await send('close-budget'); dispatch(setAppState({ loadingText: null })); if (localStorage.getItem('SharedArrayBufferOverride')) { @@ -122,7 +127,7 @@ export function createBudget({ testMode = false, demoMode = false } = {}) { return async (dispatch: Dispatch) => { dispatch( setAppState({ - loadingText: testMode || demoMode ? 'Making demo...' : '', + loadingText: testMode || demoMode ? t('Making demo...') : '', }), ); @@ -175,7 +180,7 @@ export function uploadBudget(id: string) { export function closeAndLoadBudget(fileId: string) { return async (dispatch: Dispatch) => { await dispatch(closeBudget()); - dispatch(loadBudget(fileId, 'Loading...')); + dispatch(loadBudget(fileId, t('Loading...'))); }; } @@ -188,7 +193,11 @@ export function closeAndDownloadBudget(cloudFileId: string) { export function downloadBudget(cloudFileId: string, { replace = false } = {}) { return async (dispatch: Dispatch) => { - dispatch(setAppState({ loadingText: 'Downloading...' })); + dispatch( + setAppState({ + loadingText: t('Downloading...'), + }), + ); const { id, error } = await send('download-budget', { fileId: cloudFileId, @@ -209,9 +218,15 @@ export function downloadBudget(cloudFileId: string, { replace = false } = {}) { dispatch(setAppState({ loadingText: null })); } else if (error.reason === 'file-exists') { alert( - `A file with id “${error.meta.id}” already exists with the name “${error.meta.name}.” ` + - 'This file will be replaced. This probably happened because files were manually ' + - 'moved around outside of Actual.', + t( + 'A file with id “{{id}}” already exists with the name “{{name}}”. ' + + 'This file will be replaced. This probably happened because files were manually ' + + 'moved around outside of Actual.', + { + id: error.meta.id, + name: error.meta.name, + }, + ), ); return dispatch(downloadBudget(cloudFileId, { replace: true })); diff --git a/packages/loot-core/src/client/actions/notifications.ts b/packages/loot-core/src/client/actions/notifications.ts index a2f25825640..981426b0e87 100644 --- a/packages/loot-core/src/client/actions/notifications.ts +++ b/packages/loot-core/src/client/actions/notifications.ts @@ -1,3 +1,4 @@ +import { t } from 'i18next'; import { v4 as uuidv4 } from 'uuid'; import * as constants from '../constants'; @@ -22,9 +23,10 @@ export function addNotification( export function addGenericErrorNotification() { return addNotification({ type: 'error', - message: + message: t( 'Something internally went wrong. You may want to restart the app if anything looks wrong. ' + - 'Please report this as a new issue on Github.', + 'Please report this as a new issue on Github.', + ), }); } diff --git a/packages/loot-core/src/client/actions/queries.ts b/packages/loot-core/src/client/actions/queries.ts index 6e4c2111e8c..100912a0ede 100644 --- a/packages/loot-core/src/client/actions/queries.ts +++ b/packages/loot-core/src/client/actions/queries.ts @@ -1,4 +1,5 @@ // @ts-strict-ignore +import { t } from 'i18next'; import throttle from 'throttleit'; import { send } from '../../platform/client/fetch'; @@ -166,8 +167,9 @@ export function deleteCategory(id: string, transferId?: string) { dispatch( addNotification({ type: 'error', - message: + message: t( 'A category must be transferred to another of the same type (expense or income)', + ), }), ); break; diff --git a/packages/loot-core/src/client/shared-listeners.ts b/packages/loot-core/src/client/shared-listeners.ts index 0f79165ffc0..93f56d15cc1 100644 --- a/packages/loot-core/src/client/shared-listeners.ts +++ b/packages/loot-core/src/client/shared-listeners.ts @@ -1,4 +1,6 @@ // @ts-strict-ignore +import { t } from 'i18next'; + import { listen, send } from '../platform/client/fetch'; import type { Notification } from './state-types/notifications'; @@ -20,8 +22,8 @@ export function listenForSyncEvent(actions, store) { attemptedSyncRepair = false; actions.addNotification({ - title: 'Syncing has been fixed!', - message: 'Happy budgeting!', + title: t('Syncing has been fixed!'), + message: t('Happy budgeting!'), type: 'message', }); } @@ -47,8 +49,9 @@ export function listenForSyncEvent(actions, store) { } } else if (type === 'error') { let notif: Notification | null = null; - const learnMore = - '[Learn more](https://actualbudget.org/docs/getting-started/sync/#debugging-sync-issues)'; + const learnMore = t( + '[Learn more](https://actualbudget.org/docs/getting-started/sync/#debugging-sync-issues)', + ); const githubIssueLink = 'https://github.com/actualbudget/actual/issues/new?assignees=&labels=bug&template=bug-report.yml&title=%5BBug%5D%3A+'; @@ -56,14 +59,17 @@ export function listenForSyncEvent(actions, store) { case 'out-of-sync': if (attemptedSyncRepair) { notif = { - title: 'Your data is still out of sync', + title: t('Your data is still out of sync'), message: - 'We were unable to repair your sync state, sorry! You need to reset your sync state. ' + + t( + 'We were unable to repair your sync state, sorry! You need to reset your sync state.', + ) + + ' ' + learnMore, sticky: true, id: 'reset-sync', button: { - title: 'Reset sync', + title: t('Reset sync'), action: actions.resetSync, }, }; @@ -71,16 +77,18 @@ export function listenForSyncEvent(actions, store) { // A bug happened during the sync process. Sync state needs // to be reset. notif = { - title: 'Your data is out of sync', + title: t('Your data is out of sync'), message: - 'There was a problem syncing your data. We can try to repair your sync state ' + - 'to fix it. ' + + t( + 'There was a problem syncing your data. We can try to repair your sync state to fix it.', + ) + + ' ' + learnMore, type: 'warning', sticky: true, id: 'repair-sync', button: { - title: 'Repair', + title: t('Repair'), action: async () => { attemptedSyncRepair = true; await send('sync-repair'); @@ -95,21 +103,22 @@ export function listenForSyncEvent(actions, store) { // Tell the user something is wrong with the key state on // the server and the key needs to be recreated notif = { - title: 'Actual has updated the syncing format', - message: + title: t('Actual has updated the syncing format'), + message: t( 'This happens rarely (if ever again). The internal syncing format ' + - 'has changed and you need to reset sync. This will upload data from ' + - 'this device and revert all other devices. ' + - '[Learn more about what this means](https://actualbudget.org/docs/getting-started/sync/#what-does-resetting-sync-mean).' + - '\n\nOld encryption keys are not migrated. If using ' + - 'encryption, [reset encryption here](#makeKey).', + 'has changed and you need to reset sync. This will upload data from ' + + 'this device and revert all other devices. ' + + '[Learn more about what this means](https://actualbudget.org/docs/getting-started/sync/#what-does-resetting-sync-mean).' + + '\n\n' + + 'Old encryption keys are not migrated. If using encryption, [reset encryption here](#makeKey).', + ), messageActions: { makeKey: () => actions.pushModal('create-encryption-key'), }, sticky: true, id: 'old-file', button: { - title: 'Reset sync', + title: t('Reset sync'), action: actions.resetSync, }, }; @@ -119,15 +128,16 @@ export function listenForSyncEvent(actions, store) { // Tell the user something is wrong with the key state on // the server and the key needs to be recreated notif = { - title: 'Your encryption key need to be reset', + title: t('Your encryption key need to be reset'), message: - 'Something went wrong when registering your encryption key id. ' + - 'You need to recreate your key. ' + - learnMore, + t( + 'Something went wrong when registering your encryption key id. ' + + 'You need to recreate your key. ', + ) + learnMore, sticky: true, id: 'invalid-key-state', button: { - title: 'Reset key', + title: t('Reset key'), action: () => actions.pushModal('create-encryption-key'), }, }; @@ -136,17 +146,20 @@ export function listenForSyncEvent(actions, store) { case 'file-not-found': notif = { - title: 'This file is not a cloud file', + title: t('This file is not a cloud file'), message: - 'You need to register it to take advantage ' + - 'of syncing which allows you to use it across devices and never worry ' + - 'about losing your data. ' + + t( + 'You need to register it to take advantage ' + + 'of syncing which allows you to use it across devices and never worry ' + + 'about losing your data.', + ) + + ' ' + learnMore, type: 'warning', sticky: true, id: 'register-file', button: { - title: 'Register', + title: t('Register'), action: async () => { await actions.uploadBudget(); actions.sync(); @@ -158,14 +171,20 @@ export function listenForSyncEvent(actions, store) { case 'file-needs-upload': notif = { - title: 'File needs upload', + title: t('File needs upload'), message: - 'Something went wrong when creating this cloud file. You need ' + - 'to upload this file to fix it. ' + + t( + 'Something went wrong when creating this cloud file. You need ' + + 'to upload this file to fix it.', + ) + + ' ' + learnMore, sticky: true, id: 'upload-file', - button: { title: 'Upload', action: actions.resetSync }, + button: { + title: t('Upload'), + action: actions.resetSync, + }, }; break; @@ -178,17 +197,20 @@ export function listenForSyncEvent(actions, store) { const { cloudFileId } = store.getState().prefs.local; notif = { - title: 'Syncing has been reset on this cloud file', + title: t('Syncing has been reset on this cloud file'), message: - 'You need to revert it to continue syncing. Any unsynced ' + - 'data will be lost. If you like, you can instead ' + - '[upload this file](#upload) to be the latest version. ' + + t( + 'You need to revert it to continue syncing. Any unsynced ' + + 'data will be lost. If you like, you can instead ' + + '[upload this file](#upload) to be the latest version.', + ) + + ' ' + learnMore, messageActions: { upload: actions.resetSync }, sticky: true, id: 'needs-revert', button: { - title: 'Revert', + title: t('Revert'), action: () => actions.closeAndDownloadBudget(cloudFileId), }, }; @@ -197,14 +219,15 @@ export function listenForSyncEvent(actions, store) { case 'decrypt-failure': if (meta.isMissingKey) { notif = { - title: 'Missing encryption key', - message: + title: t('Missing encryption key'), + message: t( 'Unable to encrypt your data because you are missing the key. ' + - 'Create your key to sync your data.', + 'Create your key to sync your data.', + ), sticky: true, id: 'encrypt-failure-missing', button: { - title: 'Create key', + title: t('Create key'), action: () => actions.pushModal('fix-encryption-key', { onSuccess: () => actions.sync(), @@ -213,14 +236,15 @@ export function listenForSyncEvent(actions, store) { }; } else { notif = { - message: + message: t( 'Unable to encrypt your data. You have the correct ' + - 'key so this is likely an internal failure. To fix this, ' + - 'reset your sync data with a new key.', + 'key so this is likely an internal failure. To fix this, ' + + 'reset your sync data with a new key.', + ), sticky: true, id: 'encrypt-failure', button: { - title: 'Reset key', + title: t('Reset key'), action: () => actions.pushModal('create-encryption-key', { onSuccess: () => actions.sync(), @@ -232,17 +256,21 @@ export function listenForSyncEvent(actions, store) { case 'invalid-schema': console.trace('invalid-schema', meta); notif = { - title: 'Update required', - message: + title: t('Update required'), + message: t( 'We couldn’t apply changes from the server. This probably means you ' + - 'need to update the app to support the latest database.', + 'need to update the app to support the latest database.', + ), type: 'warning', }; break; case 'apply-failure': console.trace('apply-failure', meta); notif = { - message: `We couldn’t apply that change to the database. Please report this as a bug by [opening a Github issue](${githubIssueLink}).`, + message: t( + 'We couldn’t apply that change to the database. Please report this as a bug by [opening a Github issue]({{githubIssueLink}}).', + { githubIssueLink }, + ), }; break; case 'network': @@ -251,7 +279,10 @@ export function listenForSyncEvent(actions, store) { default: console.trace('unknown error', info); notif = { - message: `We had problems syncing your changes. Please report this as a bug by [opening a Github issue](${githubIssueLink}).`, + message: t( + 'We had problems syncing your changes. Please report this as a bug by [opening a Github issue]({{githubIssueLink}}).', + { githubIssueLink }, + ), }; } diff --git a/packages/loot-core/src/client/update-notification.ts b/packages/loot-core/src/client/update-notification.ts index cb50c44b516..579e0aa8f3f 100644 --- a/packages/loot-core/src/client/update-notification.ts +++ b/packages/loot-core/src/client/update-notification.ts @@ -1,3 +1,5 @@ +import { t } from 'i18next'; + // @ts-strict-ignore export async function checkForUpdateNotification( addNotification, @@ -18,12 +20,14 @@ export async function checkForUpdateNotification( addNotification({ type: 'message', - title: 'A new version of Actual is available!', - message: `Version ${latestVersion} of Actual was recently released.`, + title: t('A new version of Actual is available!'), + message: t('Version {{latestVersion}} of Actual was recently released.', { + latestVersion, + }), sticky: true, id: 'update-notification', button: { - title: 'Open changelog', + title: t('Open changelog'), action: () => { window.open('https://actualbudget.org/docs/releases'); }, diff --git a/packages/loot-core/src/platform/client/fetch/index.browser.ts b/packages/loot-core/src/platform/client/fetch/index.browser.ts index 66d2271a0ee..6f212a34658 100644 --- a/packages/loot-core/src/platform/client/fetch/index.browser.ts +++ b/packages/loot-core/src/platform/client/fetch/index.browser.ts @@ -1,4 +1,5 @@ // @ts-strict-ignore +import { t } from 'i18next'; import { v4 as uuidv4 } from 'uuid'; import { captureException, captureBreadcrumb } from '../../exceptions'; @@ -118,7 +119,9 @@ function connectWorker(worker, onOpen, onError) { if (msg.message && msg.message.includes('indexeddb-quota-error')) { alert( - 'We hit a limit on the local storage available. Edits may not be saved. Please get in touch https://actualbudget.org/contact/ so we can help debug this.', + t( + 'We hit a limit on the local storage available. Edits may not be saved. Please get in touch https://actualbudget.org/contact/ so we can help debug this.', + ), ); } } else if (msg.type === 'capture-breadcrumb') { diff --git a/upcoming-release-notes/3036.md b/upcoming-release-notes/3036.md new file mode 100644 index 00000000000..d0eb9ba00cb --- /dev/null +++ b/upcoming-release-notes/3036.md @@ -0,0 +1,6 @@ +--- +category: Enhancements +authors: [julianwachholz] +--- + +Introduce i18n framework to prepare for translations. diff --git a/yarn.lock b/yarn.lock index a4572c44423..bf70e0d942f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -92,6 +92,9 @@ __metadata: downshift: "npm:7.6.2" focus-visible: "npm:^4.1.5" glamor: "npm:^2.20.40" + i18next: "npm:^23.11.5" + i18next-parser: "npm:^9.0.0" + i18next-resources-to-backend: "npm:^1.2.1" inter-ui: "npm:^3.19.3" jest: "npm:^27.5.1" jest-watch-typeahead: "npm:^2.2.2" @@ -108,6 +111,7 @@ __metadata: react-dom: "npm:18.2.0" react-error-boundary: "npm:^4.0.12" react-hotkeys-hook: "npm:^4.5.0" + react-i18next: "npm:^14.1.2" react-markdown: "npm:^8.0.7" react-modal: "npm:3.16.1" react-redux: "npm:7.2.9" @@ -1718,6 +1722,15 @@ __metadata: languageName: node linkType: hard +"@babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.23.9": + version: 7.25.0 + resolution: "@babel/runtime@npm:7.25.0" + dependencies: + regenerator-runtime: "npm:^0.14.0" + checksum: 10/6870e9e0e9125075b3aeba49a266f442b10820bfc693019eb6c1785c5a0edbe927e98b8238662cdcdba17842107c040386c3b69f39a0a3b217f9d00ffe685b27 + languageName: node + linkType: hard + "@babel/template@npm:^7.22.15, @babel/template@npm:^7.24.0, @babel/template@npm:^7.3.3": version: 7.24.0 resolution: "@babel/template@npm:7.24.0" @@ -2161,6 +2174,15 @@ __metadata: languageName: node linkType: hard +"@gulpjs/to-absolute-glob@npm:^4.0.0": + version: 4.0.0 + resolution: "@gulpjs/to-absolute-glob@npm:4.0.0" + dependencies: + is-negated-glob: "npm:^1.0.0" + checksum: 10/30ec7825064422b6f02c1975ab6c779ff73409411c37bec2e984262459935afd196c1dbe960075e914967a047743ccf726fce3d3ebb4417ca2e3c34538fbceb8 + languageName: node + linkType: hard + "@hapi/hoek@npm:^9.0.0": version: 9.3.0 resolution: "@hapi/hoek@npm:9.3.0" @@ -5474,6 +5496,13 @@ __metadata: languageName: node linkType: hard +"@types/minimatch@npm:^3.0.3": + version: 3.0.5 + resolution: "@types/minimatch@npm:3.0.5" + checksum: 10/c41d136f67231c3131cf1d4ca0b06687f4a322918a3a5adddc87ce90ed9dbd175a3610adee36b106ae68c0b92c637c35e02b58c8a56c424f71d30993ea220b92 + languageName: node + linkType: hard + "@types/ms@npm:*": version: 0.7.31 resolution: "@types/ms@npm:0.7.31" @@ -5622,6 +5651,13 @@ __metadata: languageName: node linkType: hard +"@types/symlink-or-copy@npm:^1.2.0": + version: 1.2.2 + resolution: "@types/symlink-or-copy@npm:1.2.2" + checksum: 10/fb8fc2a356d1d7ab222bbea772ca6bf1b4a90b1f14ea46c1522643d3afd018f3b938ead7ba04af88175432a07497c02904bf428008505acf8a4745f1bc6e3892 + languageName: node + linkType: hard + "@types/trusted-types@npm:^2.0.2": version: 2.0.7 resolution: "@types/trusted-types@npm:2.0.7" @@ -6404,7 +6440,7 @@ __metadata: languageName: node linkType: hard -"anymatch@npm:^3.0.3, anymatch@npm:~3.1.2": +"anymatch@npm:^3.0.3, anymatch@npm:^3.1.3, anymatch@npm:~3.1.2": version: 3.1.3 resolution: "anymatch@npm:3.1.3" dependencies: @@ -6697,6 +6733,13 @@ __metadata: languageName: node linkType: hard +"b4a@npm:^1.6.4": + version: 1.6.6 + resolution: "b4a@npm:1.6.6" + checksum: 10/6154a36bd78b53ecd2843a829352532a1bf9fc8081dab339ba06ca3c9ffcf25d340c3b18fe4ba0fc17a546a54c1ed814cea92cd6b895f6bd2837ca4ee0fc9f52 + languageName: node + linkType: hard + "babel-jest@npm:^27.5.1": version: 27.5.1 resolution: "babel-jest@npm:27.5.1" @@ -6878,6 +6921,13 @@ __metadata: languageName: node linkType: hard +"bare-events@npm:^2.2.0": + version: 2.4.2 + resolution: "bare-events@npm:2.4.2" + checksum: 10/c1006ad13b7e62a412466d4eac8466b4ceb46ce84a5e2fc164cd4b10edaaa5016adc684147134b67a6a3865aaf5aa007191647bdb5dbf859b1d5735d2a9ddf3b + languageName: node + linkType: hard + "base64-arraybuffer-es6@npm:^0.7.0": version: 0.7.0 resolution: "base64-arraybuffer-es6@npm:0.7.0" @@ -6953,6 +7003,17 @@ __metadata: languageName: node linkType: hard +"bl@npm:^5.0.0": + version: 5.1.0 + resolution: "bl@npm:5.1.0" + dependencies: + buffer: "npm:^6.0.3" + inherits: "npm:^2.0.4" + readable-stream: "npm:^3.4.0" + checksum: 10/0340d3d70def4213cd9cbcd8592f7c5922d3668e7b231286c354613fac4a8411ad373cff26e06162da7423035bbd5caafce3e140a5f397be72fcd1e9d86f1179 + languageName: node + linkType: hard + "bluebird-lst@npm:^1.0.9": version: 1.0.9 resolution: "bluebird-lst@npm:1.0.9" @@ -7027,6 +7088,46 @@ __metadata: languageName: node linkType: hard +"broccoli-node-api@npm:^1.7.0": + version: 1.7.0 + resolution: "broccoli-node-api@npm:1.7.0" + checksum: 10/ad6d24f71d9f58ba8a9937ad19b52887f8bc1e431c5387c44e7cba69cf348a7c9f396896d716e8b96e3f309c9733eda6264e07fead77ae4109a83adc28da68ef + languageName: node + linkType: hard + +"broccoli-node-info@npm:^2.1.0": + version: 2.2.0 + resolution: "broccoli-node-info@npm:2.2.0" + checksum: 10/9364e781100c2c9a316b45cd4d342fd97b32713612742326459c411f6d820facbd044e89e4a9d9a03e2274384ecd891125eb52048fde569a56f29f5e755e2f9b + languageName: node + linkType: hard + +"broccoli-output-wrapper@npm:^3.2.5": + version: 3.2.5 + resolution: "broccoli-output-wrapper@npm:3.2.5" + dependencies: + fs-extra: "npm:^8.1.0" + heimdalljs-logger: "npm:^0.1.10" + symlink-or-copy: "npm:^1.2.0" + checksum: 10/39f4ceeb47926250a19d3f4ab858f11854aa7ffa5c31759b0cd416cf92a70d20a1af0c4b3f1dd8daa59d20bb19613f59b0c22913eaf545d0aaff6f7564889004 + languageName: node + linkType: hard + +"broccoli-plugin@npm:^4.0.7": + version: 4.0.7 + resolution: "broccoli-plugin@npm:4.0.7" + dependencies: + broccoli-node-api: "npm:^1.7.0" + broccoli-output-wrapper: "npm:^3.2.5" + fs-merger: "npm:^3.2.1" + promise-map-series: "npm:^0.3.0" + quick-temp: "npm:^0.1.8" + rimraf: "npm:^3.0.2" + symlink-or-copy: "npm:^1.3.1" + checksum: 10/93cb02b1b0211759e2243a3201ee988eeed2dd31f4d6a5cea92f6f4a56a31184421b2798999325df810530d21e120280d34dd8e8f93b16d84877b7307f773046 + languageName: node + linkType: hard + "browser-process-hrtime@npm:^1.0.0": version: 1.0.0 resolution: "browser-process-hrtime@npm:1.0.0" @@ -7409,6 +7510,35 @@ __metadata: languageName: node linkType: hard +"cheerio-select@npm:^2.1.0": + version: 2.1.0 + resolution: "cheerio-select@npm:2.1.0" + dependencies: + boolbase: "npm:^1.0.0" + css-select: "npm:^5.1.0" + css-what: "npm:^6.1.0" + domelementtype: "npm:^2.3.0" + domhandler: "npm:^5.0.3" + domutils: "npm:^3.0.1" + checksum: 10/b5d89208c23468c3a32d1e04f88b9e8c6e332e3649650c5cd29255e2cebc215071ae18563f58c3dc3f6ef4c234488fc486035490fceb78755572288245e2931a + languageName: node + linkType: hard + +"cheerio@npm:^1.0.0-rc.2": + version: 1.0.0-rc.12 + resolution: "cheerio@npm:1.0.0-rc.12" + dependencies: + cheerio-select: "npm:^2.1.0" + dom-serializer: "npm:^2.0.0" + domhandler: "npm:^5.0.3" + domutils: "npm:^3.0.1" + htmlparser2: "npm:^8.0.1" + parse5: "npm:^7.0.0" + parse5-htmlparser2-tree-adapter: "npm:^7.0.0" + checksum: 10/812fed61aa4b669bbbdd057d0d7f73ba4649cabfd4fc3a8f1d5c7499e4613b430636102716369cbd6bbed8f1bdcb06387ae8342289fb908b2743184775f94f18 + languageName: node + linkType: hard + "chokidar@npm:>=3.0.0 <4.0.0, chokidar@npm:^3.5.3": version: 3.5.3 resolution: "chokidar@npm:3.5.3" @@ -7582,6 +7712,13 @@ __metadata: languageName: node linkType: hard +"clone-stats@npm:^1.0.0": + version: 1.0.0 + resolution: "clone-stats@npm:1.0.0" + checksum: 10/654c0425afc5c5c55a4d95b2e0c6eccdd55b5247e7a1e7cca9000b13688b96b0a157950c72c5307f9fd61f17333ad796d3cd654778f2d605438012391cc4ada5 + languageName: node + linkType: hard + "clone@npm:^1.0.2": version: 1.0.4 resolution: "clone@npm:1.0.4" @@ -7589,6 +7726,13 @@ __metadata: languageName: node linkType: hard +"clone@npm:^2.1.2": + version: 2.1.2 + resolution: "clone@npm:2.1.2" + checksum: 10/d9c79efba655f0bf601ab299c57eb54cbaa9860fb011aee9d89ed5ac0d12df1660ab7642fddaabb9a26b7eff0e117d4520512cb70798319ff5d30a111b5310c2 + languageName: node + linkType: hard + "clsx@npm:^2.0.0": version: 2.1.0 resolution: "clsx@npm:2.1.0" @@ -7665,6 +7809,13 @@ __metadata: languageName: node linkType: hard +"colors@npm:1.4.0": + version: 1.4.0 + resolution: "colors@npm:1.4.0" + checksum: 10/90b2d5465159813a3983ea72ca8cff75f784824ad70f2cc2b32c233e95bcfbcda101ebc6d6766bc50f57263792629bfb4f1f8a4dfbd1d240f229fc7f69b785fc + languageName: node + linkType: hard + "combined-stream@npm:^1.0.8": version: 1.0.8 resolution: "combined-stream@npm:1.0.8" @@ -8228,6 +8379,15 @@ __metadata: languageName: node linkType: hard +"debug@npm:^2.2.0": + version: 2.6.9 + resolution: "debug@npm:2.6.9" + dependencies: + ms: "npm:2.0.0" + checksum: 10/e07005f2b40e04f1bd14a3dd20520e9c4f25f60224cb006ce9d6781732c917964e9ec029fc7f1a151083cd929025ad5133814d4dc624a9aaf020effe4914ed14 + languageName: node + linkType: hard + "debug@npm:^3.2.7": version: 3.2.7 resolution: "debug@npm:3.2.7" @@ -8944,6 +9104,13 @@ __metadata: languageName: node linkType: hard +"ensure-posix-path@npm:^1.1.0": + version: 1.1.1 + resolution: "ensure-posix-path@npm:1.1.1" + checksum: 10/90ac69f48a08003abe6f194b75bad78c3320762bd193a063eb76cd8f696be6a34e1524f16435eeee09ccbe3a719a7fb76409dead3ccedd10e32d906ff050457b + languageName: node + linkType: hard + "entities@npm:^4.2.0, entities@npm:^4.4.0": version: 4.5.0 resolution: "entities@npm:4.5.0" @@ -8967,6 +9134,13 @@ __metadata: languageName: node linkType: hard +"eol@npm:^0.9.1": + version: 0.9.1 + resolution: "eol@npm:0.9.1" + checksum: 10/9d3fd93bb2bb5c69c7fe8dfb97b62213ed95857a2e90f5db3110415993e8a989d87fb011755ce22fdb92ca36fbe4e111b395a6f4ce00b9b51d3f00f19c2acf52 + languageName: node + linkType: hard + "err-code@npm:^2.0.2": version: 2.0.3 resolution: "err-code@npm:2.0.3" @@ -9931,6 +10105,13 @@ __metadata: languageName: node linkType: hard +"fast-fifo@npm:^1.3.2": + version: 1.3.2 + resolution: "fast-fifo@npm:1.3.2" + checksum: 10/6bfcba3e4df5af7be3332703b69a7898a8ed7020837ec4395bb341bd96cc3a6d86c3f6071dd98da289618cf2234c70d84b2a6f09a33dd6f988b1ff60d8e54275 + languageName: node + linkType: hard + "fast-glob@npm:^3.2.11, fast-glob@npm:^3.2.9, fast-glob@npm:^3.3.2": version: 3.3.2 resolution: "fast-glob@npm:3.3.2" @@ -9965,6 +10146,15 @@ __metadata: languageName: node linkType: hard +"fastq@npm:^1.13.0": + version: 1.17.1 + resolution: "fastq@npm:1.17.1" + dependencies: + reusify: "npm:^1.0.4" + checksum: 10/a443180068b527dd7b3a63dc7f2a47ceca2f3e97b9c00a1efe5538757e6cc4056a3526df94308075d7727561baf09ebaa5b67da8dcbddb913a021c5ae69d1f69 + languageName: node + linkType: hard + "fastq@npm:^1.6.0": version: 1.15.0 resolution: "fastq@npm:1.15.0" @@ -10220,7 +10410,18 @@ __metadata: languageName: node linkType: hard -"fs-extra@npm:^8.1.0": +"fs-extra@npm:^11.1.0": + version: 11.2.0 + resolution: "fs-extra@npm:11.2.0" + dependencies: + graceful-fs: "npm:^4.2.0" + jsonfile: "npm:^6.0.1" + universalify: "npm:^2.0.0" + checksum: 10/0579bf6726a4cd054d4aa308f10b483f52478bb16284f32cf60b4ce0542063d551fca1a08a2af365e35db21a3fa5a06cf2a6ed614004b4368982bc754cb816b3 + languageName: node + linkType: hard + +"fs-extra@npm:^8.0.1, fs-extra@npm:^8.1.0": version: 8.1.0 resolution: "fs-extra@npm:8.1.0" dependencies: @@ -10243,6 +10444,19 @@ __metadata: languageName: node linkType: hard +"fs-merger@npm:^3.2.1": + version: 3.2.1 + resolution: "fs-merger@npm:3.2.1" + dependencies: + broccoli-node-api: "npm:^1.7.0" + broccoli-node-info: "npm:^2.1.0" + fs-extra: "npm:^8.0.1" + fs-tree-diff: "npm:^2.0.1" + walk-sync: "npm:^2.2.0" + checksum: 10/67d74fa30f4b9a156f045a25ae991fa376701f2de33e4430c3b250532e090adbe623de0fa9aa578c9beeadf164a5004bd3933e8016e8c1cbcb53a4a43efc7001 + languageName: node + linkType: hard + "fs-minipass@npm:^2.0.0, fs-minipass@npm:^2.1.0": version: 2.1.0 resolution: "fs-minipass@npm:2.1.0" @@ -10261,6 +10475,16 @@ __metadata: languageName: node linkType: hard +"fs-mkdirp-stream@npm:^2.0.1": + version: 2.0.1 + resolution: "fs-mkdirp-stream@npm:2.0.1" + dependencies: + graceful-fs: "npm:^4.2.8" + streamx: "npm:^2.12.0" + checksum: 10/9fefd9fa3d6985aea0935944288bd20215779f683ec3af3c157cf4d4d4b0c546caae8219219f47a05a1df3b23f6a605fe64bee6ee14e550f1a670db67359ff27 + languageName: node + linkType: hard + "fs-monkey@npm:^1.0.4": version: 1.0.5 resolution: "fs-monkey@npm:1.0.5" @@ -10268,6 +10492,19 @@ __metadata: languageName: node linkType: hard +"fs-tree-diff@npm:^2.0.1": + version: 2.0.1 + resolution: "fs-tree-diff@npm:2.0.1" + dependencies: + "@types/symlink-or-copy": "npm:^1.2.0" + heimdalljs-logger: "npm:^0.1.7" + object-assign: "npm:^4.1.0" + path-posix: "npm:^1.0.0" + symlink-or-copy: "npm:^1.1.8" + checksum: 10/c1e9e77b8ae933771d69b24b91c6cf58547dc4ce4c2805179423a3c4dc1d155459094b2c603f7015e3af138055db31e351424b3ef0af6d85d53b83b3dd49db29 + languageName: node + linkType: hard + "fs.realpath@npm:^1.0.0": version: 1.0.0 resolution: "fs.realpath@npm:1.0.0" @@ -10522,6 +10759,22 @@ __metadata: languageName: node linkType: hard +"glob-stream@npm:^8.0.0": + version: 8.0.2 + resolution: "glob-stream@npm:8.0.2" + dependencies: + "@gulpjs/to-absolute-glob": "npm:^4.0.0" + anymatch: "npm:^3.1.3" + fastq: "npm:^1.13.0" + glob-parent: "npm:^6.0.2" + is-glob: "npm:^4.0.3" + is-negated-glob: "npm:^1.0.0" + normalize-path: "npm:^3.0.0" + streamx: "npm:^2.12.5" + checksum: 10/cda46c02b6313d4a5cd0a3e67c7a2bd477d5f708904dc761c0d6364611f188a303051ec4e0cd405597522c7f7ffbba530f147754b4bf5af9f18e970c024734d8 + languageName: node + linkType: hard + "glob-to-regexp@npm:^0.4.1": version: 0.4.1 resolution: "glob-to-regexp@npm:0.4.1" @@ -10701,7 +10954,7 @@ __metadata: languageName: node linkType: hard -"graceful-fs@npm:^4.1.10, graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9": +"graceful-fs@npm:^4.1.10, graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.10, graceful-fs@npm:^4.2.11, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.8, graceful-fs@npm:^4.2.9": version: 4.2.11 resolution: "graceful-fs@npm:4.2.11" checksum: 10/bf152d0ed1dc159239db1ba1f74fdbc40cb02f626770dcd5815c427ce0688c2635a06ed69af364396da4636d0408fcf7d4afdf7881724c3307e46aff30ca49e2 @@ -10722,6 +10975,15 @@ __metadata: languageName: node linkType: hard +"gulp-sort@npm:^2.0.0": + version: 2.0.0 + resolution: "gulp-sort@npm:2.0.0" + dependencies: + through2: "npm:^2.0.1" + checksum: 10/8645d80b26990290e8623ccf38420e319a4ea67b64ac3e4f2b5de6c20b9006973001fd989dfe0fe3b2560044d3dfbc005685126826536e01718a2628aa45d0c5 + languageName: node + linkType: hard + "gzip-size@npm:^6.0.0": version: 6.0.0 resolution: "gzip-size@npm:6.0.0" @@ -10832,6 +11094,25 @@ __metadata: languageName: node linkType: hard +"heimdalljs-logger@npm:^0.1.10, heimdalljs-logger@npm:^0.1.7": + version: 0.1.10 + resolution: "heimdalljs-logger@npm:0.1.10" + dependencies: + debug: "npm:^2.2.0" + heimdalljs: "npm:^0.2.6" + checksum: 10/6f0affcaa9f44cc357916059113c8d65087a7047c0e7bff057f9514feb21085749d4529b6bd41ca30b2003e3c0a7548e7291aa7e393e7d71e03928501b257ace + languageName: node + linkType: hard + +"heimdalljs@npm:^0.2.6": + version: 0.2.6 + resolution: "heimdalljs@npm:0.2.6" + dependencies: + rsvp: "npm:~3.2.1" + checksum: 10/ae31e21241a30224657c01c05b4c1cba6b370e502e6b9f00de81df7387b2df960b6c526fe7d1e9456b9375bdc64c3178dcae1ad88cd0d68a1ba295935627ac23 + languageName: node + linkType: hard + "hoist-non-react-statics@npm:^3.3.0, hoist-non-react-statics@npm:^3.3.2": version: 3.3.2 resolution: "hoist-non-react-statics@npm:3.3.2" @@ -10873,6 +11154,27 @@ __metadata: languageName: node linkType: hard +"html-parse-stringify@npm:^3.0.1": + version: 3.0.1 + resolution: "html-parse-stringify@npm:3.0.1" + dependencies: + void-elements: "npm:3.1.0" + checksum: 10/8743b76cc50e46d1956c1ad879d18eb9613b0d2d81e24686d633f9f69bb26b84676f64a926973de793cca479997017a63219278476d617b6c42d68246d7c07fe + languageName: node + linkType: hard + +"htmlparser2@npm:^8.0.1": + version: 8.0.2 + resolution: "htmlparser2@npm:8.0.2" + dependencies: + domelementtype: "npm:^2.3.0" + domhandler: "npm:^5.0.3" + domutils: "npm:^3.0.1" + entities: "npm:^4.4.0" + checksum: 10/ea5512956eee06f5835add68b4291d313c745e8407efa63848f4b8a90a2dee45f498a698bca8614e436f1ee0cfdd609938b71d67c693794545982b76e53e6f11 + languageName: node + linkType: hard + "http-cache-semantics@npm:^4.0.0, http-cache-semantics@npm:^4.1.0, http-cache-semantics@npm:^4.1.1": version: 4.1.1 resolution: "http-cache-semantics@npm:4.1.1" @@ -10968,6 +11270,51 @@ __metadata: languageName: node linkType: hard +"i18next-parser@npm:^9.0.0": + version: 9.0.1 + resolution: "i18next-parser@npm:9.0.1" + dependencies: + "@babel/runtime": "npm:^7.23.2" + broccoli-plugin: "npm:^4.0.7" + cheerio: "npm:^1.0.0-rc.2" + colors: "npm:1.4.0" + commander: "npm:~12.1.0" + eol: "npm:^0.9.1" + esbuild: "npm:^0.20.1" + fs-extra: "npm:^11.1.0" + gulp-sort: "npm:^2.0.0" + i18next: "npm:^23.5.1" + js-yaml: "npm:4.1.0" + lilconfig: "npm:^3.0.0" + rsvp: "npm:^4.8.2" + sort-keys: "npm:^5.0.0" + typescript: "npm:^5.0.4" + vinyl: "npm:~3.0.0" + vinyl-fs: "npm:^4.0.0" + bin: + i18next: bin/cli.js + checksum: 10/d6f13c6cdc98f853b5cc433fb0853a996e9a88f83e9fe26974b4b6649a01713ec09f567869c57f21e57a7efcb731d50f296373f9647deef7a73d0d76fda63388 + languageName: node + linkType: hard + +"i18next-resources-to-backend@npm:^1.2.1": + version: 1.2.1 + resolution: "i18next-resources-to-backend@npm:1.2.1" + dependencies: + "@babel/runtime": "npm:^7.23.2" + checksum: 10/93e3d5ede02f4f9b1515759fe7c4f0b911aec1deceb1836e4b25926ff5c54d27d24e5e42c62a55ad1d911d9f9a0965d48b03fdf0028183215b5cd4f5d9465e5f + languageName: node + linkType: hard + +"i18next@npm:^23.11.5, i18next@npm:^23.5.1": + version: 23.12.2 + resolution: "i18next@npm:23.12.2" + dependencies: + "@babel/runtime": "npm:^7.23.2" + checksum: 10/d7a743c54b83acc1203315e547bfe830bfe825dddd7706646aec2a49cb74254bcda70645b568d1bed55ee3610ba5e6f6012fb3c13f03080c1dd0f99db2c45478 + languageName: node + linkType: hard + "iconv-corefoundation@npm:^1.1.7": version: 1.1.7 resolution: "iconv-corefoundation@npm:1.1.7" @@ -10987,7 +11334,7 @@ __metadata: languageName: node linkType: hard -"iconv-lite@npm:^0.6.2": +"iconv-lite@npm:^0.6.2, iconv-lite@npm:^0.6.3": version: 0.6.3 resolution: "iconv-lite@npm:0.6.3" dependencies: @@ -11431,6 +11778,13 @@ __metadata: languageName: node linkType: hard +"is-negated-glob@npm:^1.0.0": + version: 1.0.0 + resolution: "is-negated-glob@npm:1.0.0" + checksum: 10/752cb846d71403d0a26389d1f56f8e2ffdb110e994dffe41ebacd1ff4953ee1dc8e71438a00a4e398355113a755f05fc91c73da15541a11d2f080f6b39030d91 + languageName: node + linkType: hard + "is-negative-zero@npm:^2.0.2": version: 2.0.2 resolution: "is-negative-zero@npm:2.0.2" @@ -11614,6 +11968,13 @@ __metadata: languageName: node linkType: hard +"is-valid-glob@npm:^1.0.0": + version: 1.0.0 + resolution: "is-valid-glob@npm:1.0.0" + checksum: 10/0155951e89291d405cbb2ff4e25a38ee7a88bc70b05f246c25d31a1d09f13d4207377e5860f67443bbda8e3e353da37047b60e586bd9c97a39c9301c30b67acb + languageName: node + linkType: hard + "is-valid-path@npm:^0.1.1": version: 0.1.1 resolution: "is-valid-path@npm:0.1.1" @@ -12490,26 +12851,26 @@ __metadata: languageName: node linkType: hard -"js-yaml@npm:^3.13.1": - version: 3.14.1 - resolution: "js-yaml@npm:3.14.1" +"js-yaml@npm:4.1.0, js-yaml@npm:^4.1.0": + version: 4.1.0 + resolution: "js-yaml@npm:4.1.0" dependencies: - argparse: "npm:^1.0.7" - esprima: "npm:^4.0.0" + argparse: "npm:^2.0.1" bin: js-yaml: bin/js-yaml.js - checksum: 10/9e22d80b4d0105b9899135365f746d47466ed53ef4223c529b3c0f7a39907743fdbd3c4379f94f1106f02755b5e90b2faaf84801a891135544e1ea475d1a1379 + checksum: 10/c138a34a3fd0d08ebaf71273ad4465569a483b8a639e0b118ff65698d257c2791d3199e3f303631f2cb98213fa7b5f5d6a4621fd0fff819421b990d30d967140 languageName: node linkType: hard -"js-yaml@npm:^4.1.0": - version: 4.1.0 - resolution: "js-yaml@npm:4.1.0" +"js-yaml@npm:^3.13.1": + version: 3.14.1 + resolution: "js-yaml@npm:3.14.1" dependencies: - argparse: "npm:^2.0.1" + argparse: "npm:^1.0.7" + esprima: "npm:^4.0.0" bin: js-yaml: bin/js-yaml.js - checksum: 10/c138a34a3fd0d08ebaf71273ad4465569a483b8a639e0b118ff65698d257c2791d3199e3f303631f2cb98213fa7b5f5d6a4621fd0fff819421b990d30d967140 + checksum: 10/9e22d80b4d0105b9899135365f746d47466ed53ef4223c529b3c0f7a39907743fdbd3c4379f94f1106f02755b5e90b2faaf84801a891135544e1ea475d1a1379 languageName: node linkType: hard @@ -12777,6 +13138,13 @@ __metadata: languageName: node linkType: hard +"lead@npm:^4.0.0": + version: 4.0.0 + resolution: "lead@npm:4.0.0" + checksum: 10/7117297c29b94e4846822e5ae0a25780af834586c0862b89ff899e44547f4f742d67801f19838b34611d36eec44868604c55525e12d2a1fb0c9496a9792ca396 + languageName: node + linkType: hard + "leven@npm:^3.1.0": version: 3.1.0 resolution: "leven@npm:3.1.0" @@ -12804,7 +13172,7 @@ __metadata: languageName: node linkType: hard -"lilconfig@npm:~3.1.1": +"lilconfig@npm:^3.0.0, lilconfig@npm:~3.1.1": version: 3.1.2 resolution: "lilconfig@npm:3.1.2" checksum: 10/8058403850cfad76d6041b23db23f730e52b6c17a8c28d87b90766639ca0ee40c748a3e85c2d7bd133d572efabff166c4b015e5d25e01fd666cb4b13cfada7f0 @@ -13025,6 +13393,7 @@ __metadata: deep-equal: "npm:^2.2.3" fake-indexeddb: "npm:^3.1.8" fast-check: "npm:3.15.0" + i18next: "npm:^23.11.5" jest: "npm:^27.5.1" jsverify: "npm:^0.8.4" lru-cache: "npm:^5.1.1" @@ -13245,6 +13614,16 @@ __metadata: languageName: node linkType: hard +"matcher-collection@npm:^2.0.0": + version: 2.0.1 + resolution: "matcher-collection@npm:2.0.1" + dependencies: + "@types/minimatch": "npm:^3.0.3" + minimatch: "npm:^3.0.2" + checksum: 10/f6d4f94bdcf773f9cbd4b7b10199a7632c434833a4c01bfb29c373e118647bb3b748aa3f20c70d6c3a715915fcc44ad4a77a9f8d5f059f3a0d15c984c0acc83d + languageName: node + linkType: hard + "matcher@npm:^3.0.0": version: 3.0.0 resolution: "matcher@npm:3.0.0" @@ -13920,7 +14299,7 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^3.0.3, minimatch@npm:^3.0.4, minimatch@npm:^3.0.5, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": +"minimatch@npm:^3.0.2, minimatch@npm:^3.0.3, minimatch@npm:^3.0.4, minimatch@npm:^3.0.5, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": version: 3.1.2 resolution: "minimatch@npm:3.1.2" dependencies: @@ -14076,6 +14455,13 @@ __metadata: languageName: node linkType: hard +"mktemp@npm:~0.4.0": + version: 0.4.0 + resolution: "mktemp@npm:0.4.0" + checksum: 10/0d7d4f20befa690bf7ba2f702f81f98b23b4124f19183048752a6ad33a07b5a16e69346245b8e683c70778e8135161ef14e5720b17fdf0b4b4ebd242a6bf2798 + languageName: node + linkType: hard + "mlly@npm:^1.2.0, mlly@npm:^1.4.2": version: 1.4.2 resolution: "mlly@npm:1.4.2" @@ -14109,6 +14495,13 @@ __metadata: languageName: node linkType: hard +"ms@npm:2.0.0": + version: 2.0.0 + resolution: "ms@npm:2.0.0" + checksum: 10/0e6a22b8b746d2e0b65a430519934fefd41b6db0682e3477c10f60c76e947c4c0ad06f63ffdf1d78d335f83edee8c0aa928aa66a36c7cd95b69b26f468d527f4 + languageName: node + linkType: hard + "ms@npm:2.1.2": version: 2.1.2 resolution: "ms@npm:2.1.2" @@ -14378,7 +14771,7 @@ __metadata: languageName: node linkType: hard -"normalize-path@npm:^3.0.0, normalize-path@npm:~3.0.0": +"normalize-path@npm:3.0.0, normalize-path@npm:^3.0.0, normalize-path@npm:~3.0.0": version: 3.0.0 resolution: "normalize-path@npm:3.0.0" checksum: 10/88eeb4da891e10b1318c4b2476b6e2ecbeb5ff97d946815ffea7794c31a89017c70d7f34b3c2ebf23ef4e9fc9fb99f7dffe36da22011b5b5c6ffa34f4873ec20 @@ -14392,6 +14785,15 @@ __metadata: languageName: node linkType: hard +"now-and-later@npm:^3.0.0": + version: 3.0.0 + resolution: "now-and-later@npm:3.0.0" + dependencies: + once: "npm:^1.4.0" + checksum: 10/5300d42932bac5d4f8d19bf90ebb53c3474ba615eab912770d1b8de896baea6dc7ef3b95158aaf601acfb0cd6b573bceb5fe30cf0224cb06ea227ef3e8fc7f3d + languageName: node + linkType: hard + "npm-conf@npm:^1.1.0": version: 1.1.3 resolution: "npm-conf@npm:1.1.3" @@ -14862,6 +15264,16 @@ __metadata: languageName: node linkType: hard +"parse5-htmlparser2-tree-adapter@npm:^7.0.0": + version: 7.0.0 + resolution: "parse5-htmlparser2-tree-adapter@npm:7.0.0" + dependencies: + domhandler: "npm:^5.0.2" + parse5: "npm:^7.0.0" + checksum: 10/23dbe45fdd338fe726cf5c55b236e1f403aeb0c1b926e18ab8ef0aa580980a25f8492d160fe2ed0ec906c3c8e38b51e68ef5620a3b9460d9458ea78946a3f7c0 + languageName: node + linkType: hard + "parse5@npm:6.0.1": version: 6.0.1 resolution: "parse5@npm:6.0.1" @@ -14869,6 +15281,15 @@ __metadata: languageName: node linkType: hard +"parse5@npm:^7.0.0": + version: 7.1.2 + resolution: "parse5@npm:7.1.2" + dependencies: + entities: "npm:^4.4.0" + checksum: 10/3c86806bb0fb1e9a999ff3a4c883b1ca243d99f45a619a0898dbf021a95a0189ed955c31b07fe49d342b54e814f33f2c9d7489198e8630dacd5477d413ec5782 + languageName: node + linkType: hard + "path-browserify@npm:^1.0.1": version: 1.0.1 resolution: "path-browserify@npm:1.0.1" @@ -14925,6 +15346,13 @@ __metadata: languageName: node linkType: hard +"path-posix@npm:^1.0.0": + version: 1.0.0 + resolution: "path-posix@npm:1.0.0" + checksum: 10/b4eae5cd4b7c943719c2f8679c53d02988bf1701583065cc5b301bb671e6ec13d6e4257257fe92a5c7b34c35e215b322a8976ce89d29dcf8801c0ee2cc75ca18 + languageName: node + linkType: hard + "path-scurry@npm:^1.10.1": version: 1.10.1 resolution: "path-scurry@npm:1.10.1" @@ -15278,6 +15706,13 @@ __metadata: languageName: node linkType: hard +"promise-map-series@npm:^0.3.0": + version: 0.3.0 + resolution: "promise-map-series@npm:0.3.0" + checksum: 10/0fc757b54a3b884c10c17854d5360e1c81b65762cbed43f8c4d380f673f30ef490a4c657b8b39552fc9e9fc783109d6beddf6eace849db2f28dffc73b72578ff + languageName: node + linkType: hard + "promise-retry@npm:^2.0.1": version: 2.0.1 resolution: "promise-retry@npm:2.0.1" @@ -15391,6 +15826,13 @@ __metadata: languageName: node linkType: hard +"queue-tick@npm:^1.0.1": + version: 1.0.1 + resolution: "queue-tick@npm:1.0.1" + checksum: 10/f447926c513b64a857906f017a3b350f7d11277e3c8d2a21a42b7998fa1a613d7a829091e12d142bb668905c8f68d8103416c7197856efb0c72fa835b8e254b5 + languageName: node + linkType: hard + "quick-lru@npm:^5.1.1": version: 5.1.1 resolution: "quick-lru@npm:5.1.1" @@ -15398,6 +15840,17 @@ __metadata: languageName: node linkType: hard +"quick-temp@npm:^0.1.8": + version: 0.1.8 + resolution: "quick-temp@npm:0.1.8" + dependencies: + mktemp: "npm:~0.4.0" + rimraf: "npm:^2.5.4" + underscore.string: "npm:~3.3.4" + checksum: 10/ef11755b9c79917390ab8d292f4cc67e2d4c594ced7853b5f6281fb491a4f507dc10cf9b6a57c62fd394502b91a5ca3287d28e793c6ebf930afc6246efe4658b + languageName: node + linkType: hard + "randombytes@npm:^2.1.0": version: 2.1.0 resolution: "randombytes@npm:2.1.0" @@ -15587,6 +16040,24 @@ __metadata: languageName: node linkType: hard +"react-i18next@npm:^14.1.2": + version: 14.1.3 + resolution: "react-i18next@npm:14.1.3" + dependencies: + "@babel/runtime": "npm:^7.23.9" + html-parse-stringify: "npm:^3.0.1" + peerDependencies: + i18next: ">= 23.2.3" + react: ">= 16.8.0" + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + checksum: 10/d0fa0f2717103c60758f9ddc1710e529f52e341465ca3f106ffa9168d88ad2db1bdbae58c77cca389933ae14bc39835abb37d1982049551ca15f6d310e2b3f57 + languageName: node + linkType: hard + "react-is@npm:^16.10.2, react-is@npm:^16.13.1, react-is@npm:^16.7.0": version: 16.13.1 resolution: "react-is@npm:16.13.1" @@ -16091,6 +16562,20 @@ __metadata: languageName: node linkType: hard +"remove-trailing-separator@npm:^1.1.0": + version: 1.1.0 + resolution: "remove-trailing-separator@npm:1.1.0" + checksum: 10/d3c20b5a2d987db13e1cca9385d56ecfa1641bae143b620835ac02a6b70ab88f68f117a0021838db826c57b31373d609d52e4f31aca75fc490c862732d595419 + languageName: node + linkType: hard + +"replace-ext@npm:^2.0.0": + version: 2.0.0 + resolution: "replace-ext@npm:2.0.0" + checksum: 10/ed640ac90d24cce4be977642847d138908d430049cc097633be33b072143515cc7d29699675a0c35f6dc3c3c73cb529ed352d59649cf15931740eb31ae083c1e + languageName: node + linkType: hard + "require-directory@npm:^2.1.1": version: 2.1.1 resolution: "require-directory@npm:2.1.1" @@ -16163,6 +16648,15 @@ __metadata: languageName: node linkType: hard +"resolve-options@npm:^2.0.0": + version: 2.0.0 + resolution: "resolve-options@npm:2.0.0" + dependencies: + value-or-function: "npm:^4.0.0" + checksum: 10/b28584cc089099af42e36292c32bd9af8bc9e28e3ca73c172c0a172d7ed5afb01c75cc2275268c327dceba77a5555b33fbd55617be138874040279fe6ff02fbf + languageName: node + linkType: hard + "resolve-pkg-maps@npm:^1.0.0": version: 1.0.0 resolution: "resolve-pkg-maps@npm:1.0.0" @@ -16279,6 +16773,17 @@ __metadata: languageName: node linkType: hard +"rimraf@npm:^2.5.4": + version: 2.7.1 + resolution: "rimraf@npm:2.7.1" + dependencies: + glob: "npm:^7.1.3" + bin: + rimraf: ./bin.js + checksum: 10/4586c296c736483e297da7cffd19475e4a3e41d07b1ae124aad5d687c79e4ffa716bdac8732ed1db942caf65271cee9dd39f8b639611de161a2753e2112ffe1d + languageName: node + linkType: hard + "rimraf@npm:^3.0.0, rimraf@npm:^3.0.2": version: 3.0.2 resolution: "rimraf@npm:3.0.2" @@ -16377,6 +16882,20 @@ __metadata: languageName: node linkType: hard +"rsvp@npm:^4.8.2": + version: 4.8.5 + resolution: "rsvp@npm:4.8.5" + checksum: 10/3c81905a0c235125cb00e855580ed8fb63d302d69ea6cadb506cea214892665f71c0998350875a6117ccce68d9afca5d996c5c74a5c36ca134cfe141bec4ce1c + languageName: node + linkType: hard + +"rsvp@npm:~3.2.1": + version: 3.2.1 + resolution: "rsvp@npm:3.2.1" + checksum: 10/c85d086bfd92b8997846221b447e41612edb6e5e7566725058af82d7e67a002e7338da8e44de98d0ebaca1519d049a6b620e0078d9ab1c8d1403131fe48e7f42 + languageName: node + linkType: hard + "run-parallel@npm:^1.1.9": version: 1.2.0 resolution: "run-parallel@npm:1.2.0" @@ -16861,6 +17380,15 @@ __metadata: languageName: node linkType: hard +"sort-keys@npm:^5.0.0": + version: 5.0.0 + resolution: "sort-keys@npm:5.0.0" + dependencies: + is-plain-obj: "npm:^4.0.0" + checksum: 10/9c0b7a468312075be03770b260b2cc0e5d55149025e564edaed41c9ff619199698aad6712a6fe4bbc75c541efb081276ac6bbd4cf2723d742f272f7a8fe354f5 + languageName: node + linkType: hard + "source-map-generator@npm:0.8.0": version: 0.8.0 resolution: "source-map-generator@npm:0.8.0" @@ -16956,6 +17484,13 @@ __metadata: languageName: node linkType: hard +"sprintf-js@npm:^1.1.1": + version: 1.1.3 + resolution: "sprintf-js@npm:1.1.3" + checksum: 10/e7587128c423f7e43cc625fe2f87e6affdf5ca51c1cc468e910d8aaca46bb44a7fbcfa552f787b1d3987f7043aeb4527d1b99559e6621e01b42b3f45e5a24cbb + languageName: node + linkType: hard + "sprintf-js@npm:^1.1.2": version: 1.1.2 resolution: "sprintf-js@npm:1.1.2" @@ -17037,6 +17572,30 @@ __metadata: languageName: node linkType: hard +"stream-composer@npm:^1.0.2": + version: 1.0.2 + resolution: "stream-composer@npm:1.0.2" + dependencies: + streamx: "npm:^2.13.2" + checksum: 10/338b8e088f2eb2c91b0e06907db436525da3620991b13499e57441548e62d3585be185505901b0380cad425889572794e5fe178dd326f5efde654b3ab26df3d3 + languageName: node + linkType: hard + +"streamx@npm:^2.12.0, streamx@npm:^2.12.5, streamx@npm:^2.13.2, streamx@npm:^2.14.0": + version: 2.18.0 + resolution: "streamx@npm:2.18.0" + dependencies: + bare-events: "npm:^2.2.0" + fast-fifo: "npm:^1.3.2" + queue-tick: "npm:^1.0.1" + text-decoder: "npm:^1.1.0" + dependenciesMeta: + bare-events: + optional: true + checksum: 10/039e828e7e76399d65fed022ddaeb7ab3ee77f66d170733643b7f7510823a605315f3ee841e5c01f16df5a44dca18a97fc39460a2b42010484e7976f29c79296 + languageName: node + linkType: hard + "string-argv@npm:~0.3.2": version: 0.3.2 resolution: "string-argv@npm:0.3.2" @@ -17454,6 +18013,13 @@ __metadata: languageName: node linkType: hard +"symlink-or-copy@npm:^1.1.8, symlink-or-copy@npm:^1.2.0, symlink-or-copy@npm:^1.3.1": + version: 1.3.1 + resolution: "symlink-or-copy@npm:1.3.1" + checksum: 10/ef6ad5fba2f423bfbd875fc61831521193483ce58f4280cf145e6ffa9d9f9e19f80bcbcbc87612e4c88b5a0a64871168e734b8a85dc04eab90a2201771f07790 + languageName: node + linkType: hard + "synckit@npm:^0.8.5, synckit@npm:^0.8.6": version: 0.8.8 resolution: "synckit@npm:0.8.8" @@ -17525,6 +18091,15 @@ __metadata: languageName: node linkType: hard +"teex@npm:^1.0.1": + version: 1.0.1 + resolution: "teex@npm:1.0.1" + dependencies: + streamx: "npm:^2.12.5" + checksum: 10/36bf7ce8bb5eb428ad7b14b695ee7fb0a02f09c1a9d8181cc42531208543a920b299d711bf78dad4ff9bcf36ac437ae8e138053734746076e3e0e7d6d76eef64 + languageName: node + linkType: hard + "temp-dir@npm:^1.0.0": version: 1.0.0 resolution: "temp-dir@npm:1.0.0" @@ -17638,6 +18213,15 @@ __metadata: languageName: node linkType: hard +"text-decoder@npm:^1.1.0": + version: 1.1.1 + resolution: "text-decoder@npm:1.1.1" + dependencies: + b4a: "npm:^1.6.4" + checksum: 10/c6981b93850daeafc8bd1dbd8f984d4fb2d14632f450de0892692b5bbee2d2f4cbef8a807142527370649fd357f58491ede4915d43669eca624cb52b8dd247b6 + languageName: node + linkType: hard + "text-table@npm:^0.2.0": version: 0.2.0 resolution: "text-table@npm:0.2.0" @@ -17759,6 +18343,15 @@ __metadata: languageName: node linkType: hard +"to-through@npm:^3.0.0": + version: 3.0.0 + resolution: "to-through@npm:3.0.0" + dependencies: + streamx: "npm:^2.12.5" + checksum: 10/404ad1a346babab53d75d3b4deb779916760fc9e605f4e64ec789366edf08e75ad592a262ca566e7864f77c03375151dcfac4744ff7fd52417cb2a2e9fc60795 + languageName: node + linkType: hard + "totalist@npm:^3.0.0": version: 3.0.1 resolution: "totalist@npm:3.0.1" @@ -18120,6 +18713,16 @@ __metadata: languageName: node linkType: hard +"typescript@npm:^5.0.4": + version: 5.5.4 + resolution: "typescript@npm:5.5.4" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 10/1689ccafef894825481fc3d856b4834ba3cc185a9c2878f3c76a9a1ef81af04194849840f3c69e7961e2312771471bb3b460ca92561e1d87599b26c37d0ffb6f + languageName: node + linkType: hard + "typescript@patch:typescript@npm%3A^4.0.2#optional!builtin<compat/typescript>": version: 4.9.5 resolution: "typescript@patch:typescript@npm%3A4.9.5#optional!builtin<compat/typescript>::version=4.9.5&hash=289587" @@ -18140,6 +18743,16 @@ __metadata: languageName: node linkType: hard +"typescript@patch:typescript@npm%3A^5.0.4#optional!builtin<compat/typescript>": + version: 5.5.4 + resolution: "typescript@patch:typescript@npm%3A5.5.4#optional!builtin<compat/typescript>::version=5.5.4&hash=379a07" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 10/746fdd0865c5ce4f15e494c57ede03a9e12ede59cfdb40da3a281807853fe63b00ef1c912d7222143499aa82f18b8b472baa1830df8804746d09b55f6cf5b1cc + languageName: node + linkType: hard + "typeson-registry@npm:^1.0.0-alpha.20": version: 1.0.0-alpha.39 resolution: "typeson-registry@npm:1.0.0-alpha.39" @@ -18201,6 +18814,16 @@ __metadata: languageName: node linkType: hard +"underscore.string@npm:~3.3.4": + version: 3.3.6 + resolution: "underscore.string@npm:3.3.6" + dependencies: + sprintf-js: "npm:^1.1.1" + util-deprecate: "npm:^1.0.2" + checksum: 10/4de7b855ef9890e4db61f3451a86193f4ba398c9615aa78bfb663d24670c3c0614aa02ffa7bd71ec2b1bddb58f41e3f2c074e9b1599722b27d5b5d1895237bfb + languageName: node + linkType: hard + "undici-types@npm:~5.26.4": version: 5.26.5 resolution: "undici-types@npm:5.26.5" @@ -18484,7 +19107,7 @@ __metadata: languageName: node linkType: hard -"util-deprecate@npm:^1.0.1, util-deprecate@npm:~1.0.1": +"util-deprecate@npm:^1.0.1, util-deprecate@npm:^1.0.2, util-deprecate@npm:~1.0.1": version: 1.0.2 resolution: "util-deprecate@npm:1.0.2" checksum: 10/474acf1146cb2701fe3b074892217553dfcf9a031280919ba1b8d651a068c9b15d863b7303cb15bd00a862b498e6cf4ad7b4a08fb134edd5a6f7641681cb54a2 @@ -18564,6 +19187,13 @@ __metadata: languageName: node linkType: hard +"value-or-function@npm:^4.0.0": + version: 4.0.0 + resolution: "value-or-function@npm:4.0.0" + checksum: 10/16b6aed84b8f9732a7eb7a5035a1480be3689d097a73b1154fb827caf021d5f2b6f60c0dfe694bfc8c9605f06cfc093dc428efdc3d24cb2768fbe202ffd42ae1 + languageName: node + linkType: hard + "verror@npm:^1.10.0": version: 1.10.1 resolution: "verror@npm:1.10.1" @@ -18619,6 +19249,65 @@ __metadata: languageName: node linkType: hard +"vinyl-contents@npm:^2.0.0": + version: 2.0.0 + resolution: "vinyl-contents@npm:2.0.0" + dependencies: + bl: "npm:^5.0.0" + vinyl: "npm:^3.0.0" + checksum: 10/10d72a032e6317bf89713565d616df8726ee41601a41c48c7d778e61ab557c0a5fdee883ceecbfb33da4a5e11ea80e76e5ae63c1d13fda61edbb5ef50445c8b2 + languageName: node + linkType: hard + +"vinyl-fs@npm:^4.0.0": + version: 4.0.0 + resolution: "vinyl-fs@npm:4.0.0" + dependencies: + fs-mkdirp-stream: "npm:^2.0.1" + glob-stream: "npm:^8.0.0" + graceful-fs: "npm:^4.2.11" + iconv-lite: "npm:^0.6.3" + is-valid-glob: "npm:^1.0.0" + lead: "npm:^4.0.0" + normalize-path: "npm:3.0.0" + resolve-options: "npm:^2.0.0" + stream-composer: "npm:^1.0.2" + streamx: "npm:^2.14.0" + to-through: "npm:^3.0.0" + value-or-function: "npm:^4.0.0" + vinyl: "npm:^3.0.0" + vinyl-sourcemap: "npm:^2.0.0" + checksum: 10/22ae47c018600e6973b8a0a0c098927b09f60c4963cc5f717be04e774215774aa15ea97400803483d3dadafc5cff1a6744c3a2ab0322528234dc4e93ae1a55aa + languageName: node + linkType: hard + +"vinyl-sourcemap@npm:^2.0.0": + version: 2.0.0 + resolution: "vinyl-sourcemap@npm:2.0.0" + dependencies: + convert-source-map: "npm:^2.0.0" + graceful-fs: "npm:^4.2.10" + now-and-later: "npm:^3.0.0" + streamx: "npm:^2.12.5" + vinyl: "npm:^3.0.0" + vinyl-contents: "npm:^2.0.0" + checksum: 10/f23fc251a3eb72100690e5e93685ef776d8fee20e076f29655536a31b5235426b9404eea76b6b268fa00648437acc98aad54a7e76661b97305706c487a54afdb + languageName: node + linkType: hard + +"vinyl@npm:^3.0.0, vinyl@npm:~3.0.0": + version: 3.0.0 + resolution: "vinyl@npm:3.0.0" + dependencies: + clone: "npm:^2.1.2" + clone-stats: "npm:^1.0.0" + remove-trailing-separator: "npm:^1.1.0" + replace-ext: "npm:^2.0.0" + teex: "npm:^1.0.1" + checksum: 10/3371947a92c4b65c7adb944b22586480ffc723ec62347d09b64e593193cb523ce5f472d52549f0e0bbfa82db6c320cae46739461594b0602bba0419d0d7800fb + languageName: node + linkType: hard + "vite-node@npm:1.6.0": version: 1.6.0 resolution: "vite-node@npm:1.6.0" @@ -18761,6 +19450,13 @@ __metadata: languageName: node linkType: hard +"void-elements@npm:3.1.0": + version: 3.1.0 + resolution: "void-elements@npm:3.1.0" + checksum: 10/0390f818107fa8fce55bb0a5c3f661056001c1d5a2a48c28d582d4d847347c2ab5b7f8272314cac58acf62345126b6b09bea623a185935f6b1c3bbce0dfd7f7f + languageName: node + linkType: hard + "w3c-hr-time@npm:^1.0.2": version: 1.0.2 resolution: "w3c-hr-time@npm:1.0.2" @@ -18779,6 +19475,18 @@ __metadata: languageName: node linkType: hard +"walk-sync@npm:^2.2.0": + version: 2.2.0 + resolution: "walk-sync@npm:2.2.0" + dependencies: + "@types/minimatch": "npm:^3.0.3" + ensure-posix-path: "npm:^1.1.0" + matcher-collection: "npm:^2.0.0" + minimatch: "npm:^3.0.4" + checksum: 10/e579b574f769977a739607d4feba40ded8931ff641f26964ea5a10a280d648d1c1aca260e9ab60288f16d69500ff33687d3ba5fa4dbd427090123189f0f0c9b6 + languageName: node + linkType: hard + "walker@npm:^1.0.7, walker@npm:^1.0.8": version: 1.0.8 resolution: "walker@npm:1.0.8"