From 3f8963273be0c737da78ea47e4bcf7adabd9ca5f Mon Sep 17 00:00:00 2001 From: Robert Dyer <rdyer@unl.edu> Date: Tue, 3 Sep 2024 13:23:15 -0500 Subject: [PATCH] Translation: desktop-client/components/schedules (#3313) * Translation: desktop-client/components/schedules * add release note * better handling of plural * clean plural use * add review suggestions * more review suggestions * change to t() syntax * use basic list format * eslint no longer needs disabled * Fix interpolation of payees list. Co-authored-by: Julian Dominguez-Schatz <jfdoming@uwaterloo.ca> * fix linter * fix typecheck * tighten the types * move to hook * memoize the hook --------- Co-authored-by: Julian Dominguez-Schatz <jfdoming@uwaterloo.ca> --- .../schedules/DiscoverSchedules.tsx | 39 ++++++--- .../schedules/PostsOfflineNotification.jsx | 77 +++++++++-------- .../components/schedules/ScheduleDetails.jsx | 84 +++++++++++-------- .../src/components/schedules/ScheduleLink.tsx | 18 ++-- .../components/schedules/SchedulesTable.tsx | 67 ++++++++++----- .../src/components/schedules/index.tsx | 13 ++- .../desktop-client/src/hooks/useFormatList.ts | 32 +++++++ upcoming-release-notes/3313.md | 6 ++ 8 files changed, 223 insertions(+), 113 deletions(-) create mode 100644 packages/desktop-client/src/hooks/useFormatList.ts create mode 100644 upcoming-release-notes/3313.md diff --git a/packages/desktop-client/src/components/schedules/DiscoverSchedules.tsx b/packages/desktop-client/src/components/schedules/DiscoverSchedules.tsx index 31d7fc10a..dd7aee001 100644 --- a/packages/desktop-client/src/components/schedules/DiscoverSchedules.tsx +++ b/packages/desktop-client/src/components/schedules/DiscoverSchedules.tsx @@ -1,5 +1,6 @@ // @ts-strict-ignore import React, { useState } from 'react'; +import { Trans, useTranslation } from 'react-i18next'; import { runQuery } from 'loot-core/src/client/query-helpers'; import { send } from 'loot-core/src/platform/client/fetch'; @@ -35,6 +36,8 @@ function DiscoverSchedulesTable({ schedules: DiscoverScheduleEntity[]; loading: boolean; }) { + const { t } = useTranslation(); + const selectedItems = useSelectedItems(); const dispatchSelected = useSelectedDispatch(); const dateFormat = useDateFormat() || 'MM/dd/yyyy'; @@ -107,13 +110,17 @@ function DiscoverSchedulesTable({ dispatchSelected({ type: 'select-all', isRangeSelect: e.shiftKey }) } /> - <Field width="flex">Payee</Field> - <Field width="flex">Account</Field> + <Field width="flex"> + <Trans>Payee</Trans> + </Field> + <Field width="flex"> + <Trans>Account</Trans> + </Field> <Field width="auto" style={{ flex: 1.5 }}> - When + <Trans>When</Trans> </Field> <Field width={100} style={{ textAlign: 'right' }}> - Amount + <Trans>Amount</Trans> </Field> </TableHeader> <Table @@ -126,13 +133,15 @@ function DiscoverSchedulesTable({ loading={loading} isSelected={id => selectedItems.has(String(id))} renderItem={renderItem} - renderEmpty="No schedules found" + renderEmpty={t('No schedules found')} /> </View> ); } export function DiscoverSchedules() { + const { t } = useTranslation(); + const { data, isLoading } = useSendPlatformRequest('schedule/discover'); const schedules = data || []; @@ -185,18 +194,22 @@ export function DiscoverSchedules() { {({ state: { close } }) => ( <> <ModalHeader - title="Found Schedules" + title={t('Found Schedules')} rightContent={<ModalCloseButton onClick={close} />} /> <Paragraph> - We found some possible schedules in your current transactions. - Select the ones you want to create. + <Trans> + We found some possible schedules in your current transactions. + Select the ones you want to create. + </Trans> </Paragraph> <Paragraph> - If you expected a schedule here and don’t see it, it might be - because the payees of the transactions don’t match. Make sure you - rename payees on all transactions for a schedule to be the same - payee. + <Trans> + If you expected a schedule here and don’t see it, it might be + because the payees of the transactions don’t match. Make sure you + rename payees on all transactions for a schedule to be the same + payee. + </Trans> </Paragraph> <SelectedProvider instance={selectedInst}> @@ -221,7 +234,7 @@ export function DiscoverSchedules() { close(); }} > - Create schedules + <Trans>Create schedules</Trans> </ButtonWithLoading> </Stack> </> diff --git a/packages/desktop-client/src/components/schedules/PostsOfflineNotification.jsx b/packages/desktop-client/src/components/schedules/PostsOfflineNotification.jsx index 29ad0f5da..a1ba84dd3 100644 --- a/packages/desktop-client/src/components/schedules/PostsOfflineNotification.jsx +++ b/packages/desktop-client/src/components/schedules/PostsOfflineNotification.jsx @@ -1,10 +1,12 @@ import React from 'react'; +import { Trans, useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; import { useLocation } from 'react-router-dom'; import { popModal } from 'loot-core/client/actions'; import { send } from 'loot-core/src/platform/client/fetch'; +import { useFormatList } from '../../hooks/useFormatList'; import { theme } from '../../style'; import { Button } from '../common/Button2'; import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal2'; @@ -14,63 +16,66 @@ import { Text } from '../common/Text'; import { DisplayId } from '../util/DisplayId'; export function PostsOfflineNotification() { + const { t } = useTranslation(); + const location = useLocation(); const dispatch = useDispatch(); const payees = (location.state && location.state.payees) || []; - const plural = payees.length > 1; async function onPost() { await send('schedule/force-run-service'); dispatch(popModal()); } + const payeesList = payees.map(id => ( + <Text key={id} style={{ color: theme.pageTextPositive }}> + <DisplayId id={id} type="payees" /> + </Text> + )); + const payeeNamesList = useFormatList(payeesList, t.language); + return ( <Modal name="schedule-posts-offline-notification"> {({ state: { close } }) => ( <> <ModalHeader - title="Post transactions?" + title={t('Post transactions?')} rightContent={<ModalCloseButton onClick={close} />} /> <Paragraph> - {payees.length > 0 ? ( - <Text> - The {plural ? 'payees ' : 'payee '} - {payees.map((id, idx) => ( - <Text key={id}> - <Text style={{ color: theme.pageTextPositive }}> - <DisplayId id={id} type="payees" /> - </Text> - {idx === payees.length - 1 - ? ' ' - : idx === payees.length - 2 - ? ', and ' - : ', '} - </Text> - ))} - </Text> - ) : ( - <Text>There {plural ? 'are payees ' : 'is a payee '} that </Text> - )} - <Text> - {plural ? 'have ' : 'has '} schedules that are due today. Usually - we automatically post transactions for these, but you are offline - or syncing failed. In order to avoid duplicate transactions, we - let you choose whether or not to create transactions for these - schedules. + {payees.length > 0 ? ( + <Trans count={payees.length}> + The payees <span>{payeeNamesList}</span> have schedules that + are due today. + </Trans> + ) : ( + t('There are payees that have schedules that are due today.', { + count: payees.length, + }) + )}{' '} + <Trans> + Usually we automatically post transactions for these, but you + are offline or syncing failed. In order to avoid duplicate + transactions, we let you choose whether or not to create + transactions for these schedules. + </Trans> </Text> </Paragraph> <Paragraph> - Be aware that other devices may have already created these - transactions. If you have multiple devices, make sure you only do - this on one device or you will have duplicate transactions. + <Trans> + Be aware that other devices may have already created these + transactions. If you have multiple devices, make sure you only do + this on one device or you will have duplicate transactions. + </Trans> </Paragraph> <Paragraph> - You can always manually post a transaction later for a due schedule - by selecting the schedule and clicking “Post transaction†in the - action menu. + <Trans> + You can always manually post a transaction later for a due + schedule by selecting the schedule and clicking “Post transaction†+ in the action menu. + </Trans> </Paragraph> <Stack direction="row" @@ -78,7 +83,9 @@ export function PostsOfflineNotification() { style={{ marginTop: 20 }} spacing={2} > - <Button onPress={close}>Decide later</Button> + <Button onPress={close}> + <Trans>Decide later</Trans> + </Button> <Button variant="primary" autoFocus @@ -87,7 +94,7 @@ export function PostsOfflineNotification() { close(); }} > - Post transactions + <Trans>Post transactions</Trans> </Button> </Stack> </> diff --git a/packages/desktop-client/src/components/schedules/ScheduleDetails.jsx b/packages/desktop-client/src/components/schedules/ScheduleDetails.jsx index 67900bf27..f9419ffdf 100644 --- a/packages/desktop-client/src/components/schedules/ScheduleDetails.jsx +++ b/packages/desktop-client/src/components/schedules/ScheduleDetails.jsx @@ -1,4 +1,5 @@ import React, { useEffect, useReducer } from 'react'; +import { Trans, useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; import { t } from 'i18next'; @@ -49,11 +50,11 @@ function updateScheduleConditions(schedule, fields) { // Validate if (fields.date == null) { - return { error: 'Date is required' }; + return { error: t('Date is required') }; } if (fields.amount == null) { - return { error: 'A valid amount is required' }; + return { error: t('A valid amount is required') }; } return { @@ -73,6 +74,8 @@ function updateScheduleConditions(schedule, fields) { } export function ScheduleDetails({ id, transaction }) { + const { t } = useTranslation(); + const adding = id == null; const fromTrans = transaction != null; const payees = getPayeesById(usePayees()); @@ -365,7 +368,7 @@ export function ScheduleDetails({ id, transaction }) { if (sameName.length > 0 && sameName[0].id !== state.schedule.id) { dispatch({ type: 'form-error', - error: 'There is already a schedule with this name', + error: t('There is already a schedule with this name'), }); return; } @@ -396,8 +399,9 @@ export function ScheduleDetails({ id, transaction }) { if (res.error) { dispatch({ type: 'form-error', - error: + error: t( 'An error occurred while saving. Please visit https://actualbudget.org/contact/ for support.', + ), }); return; } @@ -457,12 +461,16 @@ export function ScheduleDetails({ id, transaction }) { {({ state: { close } }) => ( <> <ModalHeader - title={payee ? `Schedule: ${payee.name}` : 'Schedule'} + title={ + payee + ? t(`Schedule: {{name}}`, { name: payee.name }) + : t('Schedule') + } rightContent={<ModalCloseButton onClick={close} />} /> <Stack direction="row" style={{ marginTop: 10 }}> <FormField style={{ flex: 1 }}> - <FormLabel title="Schedule Name" htmlFor="name-field" /> + <FormLabel title={t('Schedule Name')} htmlFor="name-field" /> <InitialFocus> <GenericInput field="string" @@ -478,11 +486,15 @@ export function ScheduleDetails({ id, transaction }) { </Stack> <Stack direction="row" style={{ marginTop: 20 }}> <FormField style={{ flex: 1 }}> - <FormLabel title="Payee" id="payee-label" htmlFor="payee-field" /> + <FormLabel + title={t('Payee')} + id="payee-label" + htmlFor="payee-field" + /> <PayeeAutocomplete value={state.fields.payee} labelProps={{ id: 'payee-label' }} - inputProps={{ id: 'payee-field', placeholder: '(none)' }} + inputProps={{ id: 'payee-field', placeholder: t('(none)') }} onSelect={id => dispatch({ type: 'set-field', field: 'payee', value: id }) } @@ -491,7 +503,7 @@ export function ScheduleDetails({ id, transaction }) { <FormField style={{ flex: 1 }}> <FormLabel - title="Account" + title={t('Account')} id="account-label" htmlFor="account-field" /> @@ -499,7 +511,7 @@ export function ScheduleDetails({ id, transaction }) { includeClosedAccounts={false} value={state.fields.account} labelProps={{ id: 'account-label' }} - inputProps={{ id: 'account-field', placeholder: '(none)' }} + inputProps={{ id: 'account-field', placeholder: t('(none)') }} onSelect={id => dispatch({ type: 'set-field', field: 'account', value: id }) } @@ -509,7 +521,7 @@ export function ScheduleDetails({ id, transaction }) { <FormField style={{ flex: 1 }}> <Stack direction="row" align="center" style={{ marginBottom: 3 }}> <FormLabel - title="Amount" + title={t('Amount')} htmlFor="amount-field" style={{ margin: 0, flex: 1 }} /> @@ -519,11 +531,11 @@ export function ScheduleDetails({ id, transaction }) { formatOp={op => { switch (op) { case 'is': - return 'is exactly'; + return t('is exactly'); case 'isapprox': - return 'is approximately'; + return t('is approximately'); case 'isbetween': - return 'is between'; + return t('is between'); default: throw new Error('Invalid op for select: ' + op); } @@ -570,7 +582,7 @@ export function ScheduleDetails({ id, transaction }) { </Stack> <View style={{ marginTop: 20 }}> - <FormLabel title="Date" /> + <FormLabel title={t('Date')} /> </View> <Stack direction="row" align="flex-start" justify="space-between"> @@ -595,7 +607,7 @@ export function ScheduleDetails({ id, transaction }) { {state.upcomingDates && ( <View style={{ fontSize: 13, marginTop: 20 }}> <Text style={{ color: theme.pageTextLight, fontWeight: 600 }}> - Upcoming dates + <Trans>Upcoming dates</Trans> </Text> <Stack direction="column" @@ -628,7 +640,7 @@ export function ScheduleDetails({ id, transaction }) { }} /> <label htmlFor="form_repeats" style={{ userSelect: 'none' }}> - Repeats + <Trans>Repeats</Trans> </label> </View> @@ -657,7 +669,7 @@ export function ScheduleDetails({ id, transaction }) { htmlFor="form_posts_transaction" style={{ userSelect: 'none' }} > - Automatically add transaction + <Trans>Automatically add transaction</Trans> </label> </View> @@ -671,8 +683,10 @@ export function ScheduleDetails({ id, transaction }) { lineHeight: '1.4em', }} > - If checked, the schedule will automatically create transactions - for you in the specified account + <Trans> + If checked, the schedule will automatically create + transactions for you in the specified account + </Trans> </Text> {!adding && state.schedule.rule && ( @@ -686,11 +700,13 @@ export function ScheduleDetails({ id, transaction }) { width: 350, }} > - This schedule has custom conditions and actions + <Trans> + This schedule has custom conditions and actions + </Trans> </Text> )} <Button onPress={() => onEditRule()} isDisabled={adding}> - Edit as rule + <Trans>Edit as rule</Trans> </Button> </Stack> )} @@ -702,11 +718,11 @@ export function ScheduleDetails({ id, transaction }) { {adding ? ( <View style={{ flexDirection: 'row', padding: '5px 0' }}> <Text style={{ color: theme.pageTextLight }}> - These transactions match this schedule: + <Trans>These transactions match this schedule:</Trans> </Text> <View style={{ flex: 1 }} /> <Text style={{ color: theme.pageTextLight }}> - Select transactions to link on save + <Trans>Select transactions to link on save</Trans> </Text> </View> ) : ( @@ -723,7 +739,7 @@ export function ScheduleDetails({ id, transaction }) { }} onPress={() => onSwitchTransactions('linked')} > - Linked transactions + <Trans>Linked transactions</Trans> </Button>{' '} <Button variant="bare" @@ -736,7 +752,7 @@ export function ScheduleDetails({ id, transaction }) { }} onPress={() => onSwitchTransactions('matched')} > - Find matching transactions + <Trans>Find matching transactions</Trans> </Button> <View style={{ flex: 1 }} /> <SelectedItemsButton @@ -744,8 +760,8 @@ export function ScheduleDetails({ id, transaction }) { name={count => t('{{count}} transactions', { count })} items={ state.transactionsMode === 'linked' - ? [{ name: 'unlink', text: 'Unlink from schedule' }] - : [{ name: 'link', text: 'Link to schedule' }] + ? [{ name: 'unlink', text: t('Unlink from schedule') }] + : [{ name: 'link', text: t('Link to schedule') }] } onSelect={(name, ids) => { switch (name) { @@ -792,7 +808,7 @@ export function ScheduleDetails({ id, transaction }) { <Text style={{ color: theme.errorText }}>{state.error}</Text> )} <Button style={{ marginRight: 10 }} onPress={close}> - Cancel + <Trans>Cancel</Trans> </Button> <Button variant="primary" @@ -800,7 +816,7 @@ export function ScheduleDetails({ id, transaction }) { onSave(close); }} > - {adding ? 'Add' : 'Save'} + {adding ? t('Add') : t('Save')} </Button> </Stack> </> @@ -810,6 +826,8 @@ export function ScheduleDetails({ id, transaction }) { } function NoTransactionsMessage(props) { + const { t } = useTranslation(); + return ( <View style={{ @@ -820,12 +838,12 @@ function NoTransactionsMessage(props) { > {props.error ? ( <Text style={{ color: theme.errorText }}> - Could not search: {props.error} + <Trans>Could not search: {props.error}</Trans> </Text> ) : props.transactionsMode === 'matched' ? ( - 'No matching transactions' + t('No matching transactions') ) : ( - 'No linked transactions' + t('No linked transactions') )} </View> ); diff --git a/packages/desktop-client/src/components/schedules/ScheduleLink.tsx b/packages/desktop-client/src/components/schedules/ScheduleLink.tsx index 15b3409a7..7b3a3e406 100644 --- a/packages/desktop-client/src/components/schedules/ScheduleLink.tsx +++ b/packages/desktop-client/src/components/schedules/ScheduleLink.tsx @@ -1,5 +1,6 @@ // @ts-strict-ignore import React, { useCallback, useRef, useState } from 'react'; +import { Trans, useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; import { pushModal } from 'loot-core/client/actions'; @@ -32,6 +33,8 @@ export function ScheduleLink({ accountName?: string; onScheduleLinked?: (schedule: ScheduleEntity) => void; }) { + const { t } = useTranslation(); + const dispatch = useDispatch(); const [filter, setFilter] = useState(accountName || ''); @@ -76,7 +79,7 @@ export function ScheduleLink({ {({ state: { close } }) => ( <> <ModalHeader - title="Link Schedule" + title={t('Link Schedule')} rightContent={<ModalCloseButton onClick={close} />} /> <View @@ -88,18 +91,17 @@ export function ScheduleLink({ }} > <Text> - Choose the schedule{' '} - {ids?.length > 1 - ? `these ${ids.length} transactions belong` - : `this transaction belongs`}{' '} - to: + {t( + 'Choose the schedule these {{ count }} transactions belong to:', + { count: ids?.length ?? 0 }, + )} </Text> <InitialFocus> <Search inputRef={searchInput} isInModal width={300} - placeholder="Filter schedules…" + placeholder={t('Filter schedules…')} value={filter} onChange={setFilter} /> @@ -114,7 +116,7 @@ export function ScheduleLink({ }} > <SvgAdd style={{ width: '20', padding: '3' }} /> - Create New + <Trans>Create New</Trans> </Button> )} </View> diff --git a/packages/desktop-client/src/components/schedules/SchedulesTable.tsx b/packages/desktop-client/src/components/schedules/SchedulesTable.tsx index 66bc8fa82..25d97f9e3 100644 --- a/packages/desktop-client/src/components/schedules/SchedulesTable.tsx +++ b/packages/desktop-client/src/components/schedules/SchedulesTable.tsx @@ -1,5 +1,6 @@ // @ts-strict-ignore import React, { useRef, useState, useMemo, type CSSProperties } from 'react'; +import { Trans, useTranslation } from 'react-i18next'; import { type ScheduleStatusType, @@ -61,6 +62,8 @@ function OverflowMenu({ status: ScheduleStatusType; onAction: SchedulesTableProps['onAction']; }) { + const { t } = useTranslation(); + const triggerRef = useRef(null); const [open, setOpen] = useState(false); @@ -69,28 +72,28 @@ function OverflowMenu({ menuItems.push({ name: 'post-transaction', - text: 'Post transaction', + text: t('Post transaction'), }); if (status === 'completed') { menuItems.push({ name: 'restart', - text: 'Restart', + text: t('Restart'), }); } else { menuItems.push( { name: 'skip', - text: 'Skip next date', + text: t('Skip next date'), }, { name: 'complete', - text: 'Complete', + text: t('Complete'), }, ); } - menuItems.push({ name: 'delete', text: 'Delete' }); + menuItems.push({ name: 'delete', text: t('Delete') }); return menuItems; }; @@ -100,7 +103,7 @@ function OverflowMenu({ <Button ref={triggerRef} variant="bare" - aria-label="Menu" + aria-label={t('Menu')} onPress={() => { setOpen(true); }} @@ -136,8 +139,10 @@ export function ScheduleAmountCell({ amount: ScheduleEntity['_amount']; op: ScheduleEntity['_amountOp']; }) { + const { t } = useTranslation(); + const num = getScheduledAmount(amount); - const str = integerToCurrency(Math.abs(num || 0)); + const currencyAmount = integerToCurrency(Math.abs(num || 0)); const isApprox = op === 'isapprox' || op === 'isbetween'; return ( @@ -160,7 +165,11 @@ export function ScheduleAmountCell({ lineHeight: '1em', marginRight: 10, }} - title={(isApprox ? 'Approximately ' : '') + str} + title={ + isApprox + ? t('Approximately {{currencyAmount}}', { currencyAmount }) + : currencyAmount + } > ~ </View> @@ -173,9 +182,15 @@ export function ScheduleAmountCell({ overflow: 'hidden', textOverflow: 'ellipsis', }} - title={(isApprox ? 'Approximately ' : '') + str} + title={ + isApprox + ? t('Approximately {{currencyAmount}}', { currencyAmount }) + : currencyAmount + } > - <PrivacyFilter>{num > 0 ? `+${str}` : `${str}`}</PrivacyFilter> + <PrivacyFilter> + {num > 0 ? `+${currencyAmount}` : `${currencyAmount}`} + </PrivacyFilter> </Text> </Cell> ); @@ -192,6 +207,8 @@ export function SchedulesTable({ onAction, tableStyle, }: SchedulesTableProps) { + const { t } = useTranslation(); + const dateFormat = useDateFormat() || 'MM/dd/yyyy'; const [showCompleted, setShowCompleted] = useState(false); @@ -272,7 +289,7 @@ export function SchedulesTable({ } title={schedule.name ? schedule.name : ''} > - {schedule.name ? schedule.name : 'None'} + {schedule.name ? schedule.name : t('None')} </Text> </Field> <Field width="flex" name="payee"> @@ -331,7 +348,7 @@ export function SchedulesTable({ color: theme.tableText, }} > - Show completed schedules + <Trans>Show completed schedules</Trans> </Field> </Row> ); @@ -342,17 +359,27 @@ export function SchedulesTable({ return ( <View style={{ flex: 1, ...tableStyle }}> <TableHeader height={ROW_HEIGHT} inset={15}> - <Field width="flex">Name</Field> - <Field width="flex">Payee</Field> - <Field width="flex">Account</Field> - <Field width={110}>Next date</Field> - <Field width={120}>Status</Field> + <Field width="flex"> + <Trans>Name</Trans> + </Field> + <Field width="flex"> + <Trans>Payee</Trans> + </Field> + <Field width="flex"> + <Trans>Account</Trans> + </Field> + <Field width={110}> + <Trans>Next date</Trans> + </Field> + <Field width={120}> + <Trans>Status</Trans> + </Field> <Field width={100} style={{ textAlign: 'right' }}> - Amount + <Trans>Amount</Trans> </Field> {!minimal && ( <Field width={80} style={{ textAlign: 'center' }}> - Recurring + <Trans>Recurring</Trans> </Field> )} {!minimal && <Field width={40} />} @@ -363,7 +390,7 @@ export function SchedulesTable({ style={{ flex: 1, backgroundColor: 'transparent', ...style }} items={items as ScheduleEntity[]} renderItem={renderItem} - renderEmpty={filter ? 'No matching schedules' : 'No schedules'} + renderEmpty={filter ? t('No matching schedules') : t('No schedules')} /> </View> ); diff --git a/packages/desktop-client/src/components/schedules/index.tsx b/packages/desktop-client/src/components/schedules/index.tsx index daca57a39..ffa41344e 100644 --- a/packages/desktop-client/src/components/schedules/index.tsx +++ b/packages/desktop-client/src/components/schedules/index.tsx @@ -1,4 +1,5 @@ import React, { useState } from 'react'; +import { Trans, useTranslation } from 'react-i18next'; import { useSchedules } from 'loot-core/src/client/data-hooks/schedules'; import { send } from 'loot-core/src/platform/client/fetch'; @@ -14,6 +15,8 @@ import { Page } from '../Page'; import { SchedulesTable, type ScheduleItemAction } from './SchedulesTable'; export function Schedules() { + const { t } = useTranslation(); + const { pushModal } = useActions(); const [filter, setFilter] = useState(''); @@ -64,7 +67,7 @@ export function Schedules() { } return ( - <Page header="Schedules"> + <Page header={t('Schedules')}> <View style={{ flexDirection: 'row', @@ -80,7 +83,7 @@ export function Schedules() { }} > <Search - placeholder="Filter schedules…" + placeholder={t('Filter schedules…')} value={filter} onChange={setFilter} /> @@ -105,9 +108,11 @@ export function Schedules() { flexShrink: 0, }} > - <Button onPress={onDiscover}>Find schedules</Button> + <Button onPress={onDiscover}> + <Trans>Find schedules</Trans> + </Button> <Button variant="primary" onPress={onAdd}> - Add new schedule + <Trans>Add new schedule</Trans> </Button> </View> </Page> diff --git a/packages/desktop-client/src/hooks/useFormatList.ts b/packages/desktop-client/src/hooks/useFormatList.ts new file mode 100644 index 000000000..251df4fb1 --- /dev/null +++ b/packages/desktop-client/src/hooks/useFormatList.ts @@ -0,0 +1,32 @@ +import { useMemo, type ReactNode } from 'react'; + +const interleaveArrays = (...arrays: ReactNode[][]) => + Array.from( + { + length: Math.max(...arrays.map(array => array.length)), + }, + (_, i) => arrays.map(array => array[i]), + ).flat(); + +export function useFormatList(values: ReactNode[], lng: string, opt = {}) { + const formatter = useMemo( + () => + new Intl.ListFormat(lng, { + style: 'long', + type: 'conjunction', + ...opt, + }), + [lng, opt], + ); + + const parts = useMemo(() => { + const placeholders = Array.from( + { length: values.length }, + (_, i) => `<${i}>`, + ); + const formatted = formatter.format(placeholders); + return formatted.split(/<\d+>/g); + }, [values.length, formatter]); + + return interleaveArrays(parts, values); +} diff --git a/upcoming-release-notes/3313.md b/upcoming-release-notes/3313.md new file mode 100644 index 000000000..555b2ab24 --- /dev/null +++ b/upcoming-release-notes/3313.md @@ -0,0 +1,6 @@ +--- +category: Enhancements +authors: [psybers] +--- + +Support translations in desktop-client/components/schedules. -- GitLab