#199 Adding translation to schedules list (#219)

* #199 Adding translation to schedules list

* #199 Complete translation on EditSchedule Form

* #199 Translation for status badge

* #199 Minor changes, suggested by @j-f1

Co-authored-by: Tom French <15848336+TomAFrench@users.noreply.github.com>
This commit is contained in:
Manuel Eduardo Cánepa Cihuelo 2022-08-30 19:22:43 -03:00 committed by GitHub
parent 304a384b6c
commit e436c01430
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 195 additions and 49 deletions

View file

@ -27,6 +27,7 @@ import { usePageType } from '../Page';
import { Page } from '../Page'; import { Page } from '../Page';
import { OpSelect } from '../modals/EditRule'; import { OpSelect } from '../modals/EditRule';
import { AmountInput, BetweenAmountInput } from '../util/AmountInput'; import { AmountInput, BetweenAmountInput } from '../util/AmountInput';
import { useTranslation } from 'react-i18next';
function mergeFields(defaults, initial) { function mergeFields(defaults, initial) {
let res = { ...defaults }; let res = { ...defaults };
@ -92,6 +93,7 @@ export default function ScheduleDetails() {
}); });
let pageType = usePageType(); let pageType = usePageType();
const { t } = useTranslation();
let [state, dispatch] = useReducer( let [state, dispatch] = useReducer(
(state, action) => { (state, action) => {
@ -365,8 +367,10 @@ export default function ScheduleDetails() {
if (res.error) { if (res.error) {
dispatch({ dispatch({
type: 'form-error', type: 'form-error',
error: // Note: email is outside of translation to be easily replace on future
'An error occurred while saving. Please contact help@actualbudget.com for support.' error: t('support.anErrorOccuredWhileSaving', {
email: 'help@actualbudget.com'
})
}); });
} else { } else {
if (adding) { if (adding) {
@ -423,15 +427,19 @@ export default function ScheduleDetails() {
return ( return (
<Page <Page
title={payee ? `Schedule: ${payee.name}` : 'Schedule'} title={
payee
? t('schedules.scheduleNamed', { name: payee.name })
: t('general.schedule')
}
modalSize="medium" modalSize="medium"
> >
<Stack direction="row" style={{ marginTop: 20 }}> <Stack direction="row" style={{ marginTop: 20 }}>
<FormField style={{ flex: 1 }}> <FormField style={{ flex: 1 }}>
<FormLabel title="Payee" /> <FormLabel title={t('general.payee')} />
<PayeeAutocomplete <PayeeAutocomplete
value={state.fields.payee} value={state.fields.payee}
inputProps={{ placeholder: '(none)' }} inputProps={{ placeholder: t('schedules.none') }}
onSelect={id => onSelect={id =>
dispatch({ type: 'set-field', field: 'payee', value: id }) dispatch({ type: 'set-field', field: 'payee', value: id })
} }
@ -439,10 +447,10 @@ export default function ScheduleDetails() {
</FormField> </FormField>
<FormField style={{ flex: 1 }}> <FormField style={{ flex: 1 }}>
<FormLabel title="Account" /> <FormLabel title={t('general.account')} />
<AccountAutocomplete <AccountAutocomplete
value={state.fields.account} value={state.fields.account}
inputProps={{ placeholder: '(none)' }} inputProps={{ placeholder: t('schedules.none') }}
onSelect={id => onSelect={id =>
dispatch({ type: 'set-field', field: 'account', value: id }) dispatch({ type: 'set-field', field: 'account', value: id })
} }
@ -451,18 +459,21 @@ export default function ScheduleDetails() {
<FormField style={{ flex: 1 }}> <FormField style={{ flex: 1 }}>
<Stack direction="row" align="center" style={{ marginBottom: 3 }}> <Stack direction="row" align="center" style={{ marginBottom: 3 }}>
<FormLabel title="Amount" style={{ margin: 0, flex: 1 }} /> <FormLabel
title={t('general.amount')}
style={{ margin: 0, flex: 1 }}
/>
<OpSelect <OpSelect
ops={['is', 'isapprox', 'isbetween']} ops={['is', 'isapprox', 'isbetween']}
value={state.fields.amountOp} value={state.fields.amountOp}
formatOp={op => { formatOp={op => {
switch (op) { switch (op) {
case 'is': case 'is':
return 'is exactly'; return t('schedules.isExactly');
case 'isapprox': case 'isapprox':
return 'is approximately'; return t('schedules.isApproximately');
case 'isbetween': case 'isbetween':
return 'is between'; return t('schedules.isBetween');
default: default:
throw new Error('Invalid op for select: ' + op); throw new Error('Invalid op for select: ' + op);
} }
@ -504,7 +515,7 @@ export default function ScheduleDetails() {
</Stack> </Stack>
<View style={{ marginTop: 20 }}> <View style={{ marginTop: 20 }}>
<FormLabel title="Date" /> <FormLabel title={t('general.date')} />
</View> </View>
<Stack direction="row" align="flex-start"> <Stack direction="row" align="flex-start">
@ -529,7 +540,7 @@ export default function ScheduleDetails() {
{state.upcomingDates && ( {state.upcomingDates && (
<View style={{ fontSize: 13, marginTop: 20 }}> <View style={{ fontSize: 13, marginTop: 20 }}>
<Text style={{ color: colors.n4, fontWeight: 600 }}> <Text style={{ color: colors.n4, fontWeight: 600 }}>
Upcoming dates {t('schedules.upcomingDates')}
</Text> </Text>
<Stack <Stack
direction="column" direction="column"
@ -561,7 +572,7 @@ export default function ScheduleDetails() {
}} }}
/> />
<label for="form_repeats" style={{ userSelect: 'none' }}> <label for="form_repeats" style={{ userSelect: 'none' }}>
Repeats {t('general.repeats')}
</label> </label>
</View> </View>
@ -592,7 +603,7 @@ export default function ScheduleDetails() {
}} }}
/> />
<label for="form_posts_transaction" style={{ userSelect: 'none' }}> <label for="form_posts_transaction" style={{ userSelect: 'none' }}>
Automatically add transaction {t('schedules.automaticallyAddTransaction')}
</label> </label>
</View> </View>
@ -606,8 +617,7 @@ export default function ScheduleDetails() {
lineHeight: '1.4em' lineHeight: '1.4em'
}} }}
> >
If checked, the schedule will automatically create transactions for {t('schedules.automaticallyAddTransactionAdvice')}
you in the specified account
</Text> </Text>
{!adding && state.schedule.rule && ( {!adding && state.schedule.rule && (
@ -621,11 +631,11 @@ export default function ScheduleDetails() {
width: 350 width: 350
}} }}
> >
This schedule has custom conditions and actions {t('schedules.thisScheduleHasCustomConditionsAndActions')}
</Text> </Text>
)} )}
<Button onClick={() => onEditRule()} disabled={adding}> <Button onClick={() => onEditRule()} disabled={adding}>
Edit as rule {t('schedules.editAsRule')}
</Button> </Button>
</Stack> </Stack>
)} )}
@ -637,11 +647,11 @@ export default function ScheduleDetails() {
{adding ? ( {adding ? (
<View style={{ flexDirection: 'row', padding: '5px 0' }}> <View style={{ flexDirection: 'row', padding: '5px 0' }}>
<Text style={{ color: colors.n4 }}> <Text style={{ color: colors.n4 }}>
These transactions match this schedule: {t('schedules.theseTransactionsMatchThisSchedule')}
</Text> </Text>
<View style={{ flex: 1 }} /> <View style={{ flex: 1 }} />
<Text style={{ color: colors.n6 }}> <Text style={{ color: colors.n6 }}>
Select transactions to link on save {t('schedules.selectTransactionsToLinkOnSave')}
</Text> </Text>
</View> </View>
) : ( ) : (
@ -656,7 +666,7 @@ export default function ScheduleDetails() {
}} }}
onClick={() => onSwitchTransactions('linked')} onClick={() => onSwitchTransactions('linked')}
> >
Linked transactions {t('schedules.linkedTransactions')}
</Button>{' '} </Button>{' '}
<Button <Button
bare bare
@ -669,15 +679,20 @@ export default function ScheduleDetails() {
}} }}
onClick={() => onSwitchTransactions('matched')} onClick={() => onSwitchTransactions('matched')}
> >
Find matching transactions {t('schedules.findMatchingTransactions')}
</Button> </Button>
<View style={{ flex: 1 }} /> <View style={{ flex: 1 }} />
<SelectedItemsButton <SelectedItemsButton
name="transactions" name="transactions"
items={ items={
state.transactionsMode === 'linked' state.transactionsMode === 'linked'
? [{ name: 'unlink', text: 'Unlink from schedule' }] ? [
: [{ name: 'link', text: 'Link to schedule' }] {
name: 'unlink',
text: t('schedules.unlinkFromSchedule')
}
]
: [{ name: 'link', text: t('schedules.linkToSchedule') }]
} }
onSelect={(name, ids) => { onSelect={(name, ids) => {
switch (name) { switch (name) {
@ -715,10 +730,10 @@ export default function ScheduleDetails() {
> >
{state.error && <Text style={{ color: colors.r4 }}>{state.error}</Text>} {state.error && <Text style={{ color: colors.r4 }}>{state.error}</Text>}
<Button style={{ marginRight: 10 }} onClick={() => history.goBack()}> <Button style={{ marginRight: 10 }} onClick={() => history.goBack()}>
Cancel {t('general.cancel')}
</Button> </Button>
<Button primary onClick={onSave}> <Button primary onClick={onSave}>
{adding ? 'Add' : 'Save'} {adding ? t('general.add') : t('general.save')}
</Button> </Button>
</Stack> </Stack>
</Page> </Page>

View file

@ -22,12 +22,15 @@ import DotsHorizontalTriple from 'loot-design/src/svg/v1/DotsHorizontalTriple';
import Check from 'loot-design/src/svg/v2/Check'; import Check from 'loot-design/src/svg/v2/Check';
import DisplayId from '../util/DisplayId'; import DisplayId from '../util/DisplayId';
import { StatusBadge } from './StatusBadge'; import { StatusBadge } from './StatusBadge';
import { useTranslation } from 'react-i18next';
export let ROW_HEIGHT = 43; export let ROW_HEIGHT = 43;
function OverflowMenu({ schedule, status, onAction }) { function OverflowMenu({ schedule, status, onAction }) {
let [open, setOpen] = useState(false); let [open, setOpen] = useState(false);
const { t } = useTranslation();
return ( return (
<View> <View>
<Button <Button
@ -58,15 +61,15 @@ function OverflowMenu({ schedule, status, onAction }) {
items={[ items={[
status === 'due' && { status === 'due' && {
name: 'post-transaction', name: 'post-transaction',
text: 'Post transaction' text: t('schedules.postTransaction')
}, },
...(schedule.completed ...(schedule.completed
? [{ name: 'restart', text: 'Restart' }] ? [{ name: 'restart', text: t('general.restart') }]
: [ : [
{ name: 'skip', text: 'Skip next date' }, { name: 'skip', text: t('schedules.skipNextDate') },
{ name: 'complete', text: 'Complete' } { name: 'complete', text: t('general.complete') }
]), ]),
{ name: 'delete', text: 'Delete' } { name: 'delete', text: t('general.delete') }
]} ]}
/> />
</Tooltip> </Tooltip>
@ -80,6 +83,8 @@ export function ScheduleAmountCell({ amount, op }) {
let str = integerToCurrency(Math.abs(num || 0)); let str = integerToCurrency(Math.abs(num || 0));
let isApprox = op === 'isapprox' || op === 'isbetween'; let isApprox = op === 'isapprox' || op === 'isbetween';
const { t } = useTranslation();
return ( return (
<Cell <Cell
width={100} width={100}
@ -99,7 +104,7 @@ export function ScheduleAmountCell({ amount, op }) {
lineHeight: '1em', lineHeight: '1em',
marginRight: 10 marginRight: 10
}} }}
title={(isApprox ? 'Approximately ' : '') + str} title={t('general.approximatelyWithAmount', { amount: str })}
> >
~ ~
</View> </View>
@ -112,7 +117,9 @@ export function ScheduleAmountCell({ amount, op }) {
overflow: 'hidden', overflow: 'hidden',
textOverflow: 'ellipsis' textOverflow: 'ellipsis'
}} }}
title={(isApprox ? 'Approximately ' : '') + str} title={
isApprox ? t('general.approximatelyWithAmount', { amount: str }) : str
}
> >
{num > 0 ? `+${str}` : `${str}`} {num > 0 ? `+${str}` : `${str}`}
</Text> </Text>
@ -135,6 +142,8 @@ export function SchedulesTable({
let [showCompleted, setShowCompleted] = useState(false); let [showCompleted, setShowCompleted] = useState(false);
const { t } = useTranslation();
let items = useMemo(() => { let items = useMemo(() => {
if (!allowCompleted) { if (!allowCompleted) {
return schedules.filter(s => !s.completed); return schedules.filter(s => !s.completed);
@ -219,7 +228,7 @@ export function SchedulesTable({
color: colors.n6 color: colors.n6
}} }}
> >
Show completed schedules {t('schedules.showCompletedSchedules')}
</Field> </Field>
</Row> </Row>
); );
@ -230,16 +239,16 @@ export function SchedulesTable({
return ( return (
<> <>
<TableHeader height={ROW_HEIGHT} inset={15} version="v2"> <TableHeader height={ROW_HEIGHT} inset={15} version="v2">
<Field width="flex">Payee</Field> <Field width="flex">{t('general.payee')}</Field>
<Field width="flex">Account</Field> <Field width="flex">{t('general.account')}</Field>
<Field width={110}>Next date</Field> <Field width={110}>{t('schedules.nextDate')}</Field>
<Field width={120}>Status</Field> <Field width={120}>{t('general.status')}</Field>
<Field width={100} style={{ textAlign: 'right' }}> <Field width={100} style={{ textAlign: 'right' }}>
Amount {t('general.amount')}
</Field> </Field>
{!minimal && ( {!minimal && (
<Field width={80} style={{ textAlign: 'center' }}> <Field width={80} style={{ textAlign: 'center' }}>
Recurring {t('general.recurring')}
</Field> </Field>
)} )}
{!minimal && <Field width={40}></Field>} {!minimal && <Field width={40}></Field>}
@ -251,7 +260,7 @@ export function SchedulesTable({
style={[{ flex: 1, backgroundColor: 'transparent' }, style]} style={[{ flex: 1, backgroundColor: 'transparent' }, style]}
items={items} items={items}
renderItem={renderItem} renderItem={renderItem}
renderEmpty="No schedules" renderEmpty={t('schedules.noSchedules')}
allowPopupsEscape={items.length < 6} allowPopupsEscape={items.length < 6}
/> />
</> </>

View file

@ -9,54 +9,65 @@ import CalendarIcon from 'loot-design/src/svg/v2/Calendar';
import ValidationCheck from 'loot-design/src/svg/v2/ValidationCheck'; import ValidationCheck from 'loot-design/src/svg/v2/ValidationCheck';
import FavoriteStar from 'loot-design/src/svg/v2/FavoriteStar'; import FavoriteStar from 'loot-design/src/svg/v2/FavoriteStar';
import CheckCircle1 from 'loot-design/src/svg/v2/CheckCircle1'; import CheckCircle1 from 'loot-design/src/svg/v2/CheckCircle1';
import { useTranslation } from 'react-i18next';
export function getStatusProps(status) { export function getStatusProps(status) {
let color, backgroundColor, Icon; let color, backgroundColor, Icon, title;
const { t } = useTranslation();
switch (status) { switch (status) {
case 'missed': case 'missed':
color = colors.r1; color = colors.r1;
backgroundColor = colors.r10; backgroundColor = colors.r10;
Icon = EditSkull1; Icon = EditSkull1;
title = t('status.missed');
break; break;
case 'due': case 'due':
color = colors.y1; color = colors.y1;
backgroundColor = colors.y9; backgroundColor = colors.y9;
Icon = AlertTriangle; Icon = AlertTriangle;
title = t('status.due');
break; break;
case 'upcoming': case 'upcoming':
color = colors.p1; color = colors.p1;
backgroundColor = colors.p10; backgroundColor = colors.p10;
Icon = CalendarIcon; Icon = CalendarIcon;
title = t('status.upcoming');
break; break;
case 'paid': case 'paid':
color = colors.g2; color = colors.g2;
backgroundColor = colors.g10; backgroundColor = colors.g10;
Icon = ValidationCheck; Icon = ValidationCheck;
title = t('status.paid');
break; break;
case 'completed': case 'completed':
color = colors.n4; color = colors.n4;
backgroundColor = colors.n11; backgroundColor = colors.n11;
Icon = FavoriteStar; Icon = FavoriteStar;
title = t('status.completed');
break; break;
case 'pending': case 'pending':
color = colors.g4; color = colors.g4;
backgroundColor = colors.g11; backgroundColor = colors.g11;
Icon = CalendarIcon; Icon = CalendarIcon;
title = t('status.pending');
break; break;
case 'scheduled': case 'scheduled':
color = colors.n1; color = colors.n1;
backgroundColor = colors.n11; backgroundColor = colors.n11;
Icon = CalendarIcon; Icon = CalendarIcon;
title = t('status.scheduled');
break; break;
default: default:
color = colors.n1; color = colors.n1;
backgroundColor = colors.n11; backgroundColor = colors.n11;
Icon = CheckCircle1; Icon = CheckCircle1;
title = status;
break; break;
} }
return { color, backgroundColor, Icon }; return { title, color, backgroundColor, Icon };
} }
export function StatusIcon({ status }) { export function StatusIcon({ status }) {
@ -66,7 +77,7 @@ export function StatusIcon({ status }) {
} }
export function StatusBadge({ status, style }) { export function StatusBadge({ status, style }) {
let { color, backgroundColor, Icon } = getStatusProps(status); let { title, color, backgroundColor, Icon } = getStatusProps(status);
return ( return (
<View <View
style={[ style={[
@ -90,7 +101,7 @@ export function StatusBadge({ status, style }) {
marginRight: 7 marginRight: 7
}} }}
/> />
<Text style={{ lineHeight: '1em' }}>{titleFirst(status)}</Text> <Text style={{ lineHeight: '1em' }}>{titleFirst(title)}</Text>
</View> </View>
); );
} }

View file

@ -5,12 +5,15 @@ import { send } from 'loot-core/src/platform/client/fetch';
import { useSchedules } from 'loot-core/src/client/data-hooks/schedules'; import { useSchedules } from 'loot-core/src/client/data-hooks/schedules';
import { Page } from '../Page'; import { Page } from '../Page';
import { SchedulesTable, ROW_HEIGHT } from './SchedulesTable'; import { SchedulesTable, ROW_HEIGHT } from './SchedulesTable';
import { useTranslation } from 'react-i18next';
export default function Schedules() { export default function Schedules() {
let history = useHistory(); let history = useHistory();
let scheduleData = useSchedules(); let scheduleData = useSchedules();
const { t } = useTranslation();
if (scheduleData == null) { if (scheduleData == null) {
return null; return null;
} }
@ -50,7 +53,7 @@ export default function Schedules() {
} }
return ( return (
<Page title="Schedules"> <Page title={t('general.schedules')}>
<View <View
style={{ style={{
marginTop: 20, marginTop: 20,
@ -70,7 +73,7 @@ export default function Schedules() {
<View style={{ alignItems: 'flex-end', margin: '20px 0', flexShrink: 0 }}> <View style={{ alignItems: 'flex-end', margin: '20px 0', flexShrink: 0 }}>
<Button primary onClick={onAdd}> <Button primary onClick={onAdd}>
Add new schedule {t('schedules.addNewSchedule')}
</Button> </Button>
</View> </View>
</Page> </Page>

View file

@ -8,8 +8,62 @@
"needAccountMessage": "For Actual to be useful, you need to <strong>add an account</strong>. You can link an account to automatically download transactions, or manage it locally yourself." "needAccountMessage": "For Actual to be useful, you need to <strong>add an account</strong>. You can link an account to automatically download transactions, or manage it locally yourself."
}, },
"bootstrap": { "bootstrap": {
"title": "Bootstrap this Actual instance",
"setPassword": "Set a password for this server instance", "setPassword": "Set a password for this server instance",
"title": "Bootstrap this Actual instance",
"tryDemo": "Try Demo" "tryDemo": "Try Demo"
},
"general": {
"account": "Account",
"add": "Add",
"amount": "Amount",
"approximatelyWithAmount": "Approximately {{amount}}",
"cancel": "Cancel",
"complete": "Complete",
"date": "Date",
"delete": "Delete",
"payee": "Payee",
"recurring": "Recurring",
"repeats": "Repeats",
"restart": "Restart",
"save": "Save",
"schedule": "Schedule",
"schedules": "Schedules",
"status": "Status"
},
"schedules": {
"addNewSchedule": "Add new schedule",
"automaticallyAddTransaction": "Automatically add transaction",
"automaticallyAddTransactionAdvice": "If checked, the schedule will automatically create transactions for you in the specified account",
"editAsRule": "Edit as rule",
"findMatchingTransactions": "Find matching transactions",
"isApproximately": "is approximately",
"isBetween": "is between",
"isExactly": "is exactly",
"linkedTransactions": "Linked transactions",
"linkToSchedule": "Link to schedule",
"nextDate": "Next date",
"none": "(none)",
"noSchedules": "No schedules",
"postTransaction": "Post transaction",
"scheduleNamed": "Schedule: {{name}}",
"selectTransactionsToLinkOnSave": "Select transactions to link on save",
"showCompletedSchedules": "Show completed schedules",
"skipNextDate": "Skip next date",
"theseTransactionsMatchThisSchedule": "These transactions match this schedule:",
"thisScheduleHasCustomConditionsAndActions": "This schedule has custom conditions and actions",
"unlinkFromSchedule": "Unlink from schedule",
"upcomingDates": "Upcoming dates"
},
"status": {
"completed": "completed",
"due": "due",
"missed": "missed",
"paid": "paid",
"pending": "pending",
"scheduled": "scheduled",
"upcoming": "upcoming"
},
"support": {
"anErrorOccuredWhileSaving": "An error occurred while saving. Please contact {{email}} for support."
} }
} }

View file

@ -11,5 +11,59 @@
"setPassword": "Establecer una contraseña para esta instancia de servidor", "setPassword": "Establecer una contraseña para esta instancia de servidor",
"title": "Bootstrap esta instancia de Actual", "title": "Bootstrap esta instancia de Actual",
"tryDemo": "Probar Demo" "tryDemo": "Probar Demo"
},
"general": {
"account": "Cuenta",
"add": "Agregar",
"amount": "Monto",
"approximatelyWithAmount": "Aproximadamente {{amount}}",
"cancel": "Cancelar",
"complete": "Completar",
"date": "Fecha",
"delete": "Borrar",
"payee": "Beneficiario",
"recurring": "Periódico",
"repeats": "Repetir",
"restart": "Reiniciar",
"save": "Guardar",
"schedule": "Agenda",
"schedules": "Agendas",
"status": "Estado"
},
"schedules": {
"addNewSchedule": "Agregar nuevo agenda",
"automaticallyAddTransaction": "Agregar transacción automáticamente",
"automaticallyAddTransactionAdvice": "Si se selecciona, la agenda creará automáticamente una transacción para la cuenta especificada",
"editAsRule": "Editar como regla",
"findMatchingTransactions": "Encontrar transacciones que coincidan",
"isApproximately": "es aproximadamente",
"isBetween": "está entre",
"isExactly": "es exactamente",
"linkedTransactions": "Transacciones vinculadas",
"linkToSchedule": "Vincular a agenda",
"nextDate": "Próxima fecha",
"none": "(ninguno)",
"noSchedules": "Sin agendas",
"postTransaction": "Publicar transacción",
"scheduleNamed": "Agenda: {{name}}",
"selectTransactionsToLinkOnSave": "Seleccionar transacciones para vincular al guardar",
"showCompletedSchedules": "Mostrar agendas completadas",
"skipNextDate": "Saltar próxima fecha",
"theseTransactionsMatchThisSchedule": "Éstas transacciones coinciden con la agenda",
"thisScheduleHasCustomConditionsAndActions": "Ésta agenda tiene condiciones y acciones personalizadas",
"unlinkFromSchedule": "Desvincular de la agenda",
"upcomingDates": "Próximas fechas"
},
"status": {
"completed": "completo",
"due": "adeudado",
"missed": "omitido",
"paid": "pago",
"pending": "pendiente",
"scheduled": "agendado",
"upcoming": "próximo"
},
"support": {
"anErrorOccuredWhileSaving": "Ocurrió un error al guardar. Por favor, póngase en contacto con {{email}} para obtener asistencia."
} }
} }