diff --git a/packages/desktop-client/src/components/Modals.tsx b/packages/desktop-client/src/components/Modals.tsx
index ef458ef87b4..a52f6fef71d 100644
--- a/packages/desktop-client/src/components/Modals.tsx
+++ b/packages/desktop-client/src/components/Modals.tsx
@@ -1,5 +1,6 @@
// @ts-strict-ignore
import React, { useEffect } from 'react';
+import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { useLocation } from 'react-router-dom';
@@ -81,6 +82,8 @@ export function Modals() {
}
}, [location]);
+ const { t } = useTranslation();
+
const modals = modalStack
.map(({ name, options }) => {
switch (name) {
@@ -287,10 +290,12 @@ export function Modals() {
Header={props => (
}
+ title={
+
+ }
/>
)}
- inputPlaceholder="Category name"
+ inputPlaceholder={t('Category name')}
buttonText="Add"
onValidate={options.onValidate}
onSubmit={options.onSubmit}
@@ -306,12 +311,15 @@ export function Modals() {
+
}
/>
)}
- inputPlaceholder="Category group name"
- buttonText="Add"
+ inputPlaceholder={t('Category group name')}
+ buttonText={t('Add')}
onValidate={options.onValidate}
onSubmit={options.onSubmit}
/>
diff --git a/packages/desktop-client/src/components/modals/BudgetPageMenuModal.tsx b/packages/desktop-client/src/components/modals/BudgetPageMenuModal.tsx
index ad01f64d19f..8e96583490c 100644
--- a/packages/desktop-client/src/components/modals/BudgetPageMenuModal.tsx
+++ b/packages/desktop-client/src/components/modals/BudgetPageMenuModal.tsx
@@ -2,6 +2,7 @@ import React, {
type ComponentPropsWithoutRef,
type CSSProperties,
} from 'react';
+import { useTranslation } from 'react-i18next';
import { useLocalPref } from '../../hooks/useLocalPref';
import { theme, styles } from '../../style';
@@ -77,6 +78,7 @@ function BudgetPageMenu({
throw new Error(`Unrecognized menu item: ${name}`);
}
};
+ const { t } = useTranslation();
return (
diff --git a/packages/desktop-client/src/components/payees/ManagePayeesPage.jsx b/packages/desktop-client/src/components/payees/ManagePayeesPage.jsx
index b27f396cb87..427195f7e72 100644
--- a/packages/desktop-client/src/components/payees/ManagePayeesPage.jsx
+++ b/packages/desktop-client/src/components/payees/ManagePayeesPage.jsx
@@ -1,4 +1,5 @@
import React from 'react';
+import { useTranslation } from 'react-i18next';
import { useLocation } from 'react-router-dom';
import { Page } from '../Page';
@@ -6,9 +7,10 @@ import { Page } from '../Page';
import { ManagePayeesWithData } from './ManagePayeesWithData';
export function ManagePayeesPage() {
+ const { t } = useTranslation();
const location = useLocation();
return (
-
+
();
const [err, setErr] = useState('');
const [value, setValue] = useState('');
+ const { t } = useTranslation();
useEffect(() => {
if (inputRef.current) {
@@ -38,7 +40,9 @@ export function SaveReportChoose({ onApply }: SaveReportChooseProps) {
}}
>
- Choose Report
+
+ {t('Choose Report')}
+
diff --git a/packages/desktop-client/src/components/reports/SaveReportMenu.tsx b/packages/desktop-client/src/components/reports/SaveReportMenu.tsx
index ad1409e8230..719b0d9224f 100644
--- a/packages/desktop-client/src/components/reports/SaveReportMenu.tsx
+++ b/packages/desktop-client/src/components/reports/SaveReportMenu.tsx
@@ -1,4 +1,5 @@
import React from 'react';
+import { useTranslation } from 'react-i18next';
import { Menu, type MenuItem } from '../common/Menu';
@@ -11,11 +12,12 @@ export function SaveReportMenu({
savedStatus: string;
listReports: number;
}) {
+ const { t } = useTranslation();
const savedMenu: MenuItem[] =
savedStatus === 'saved'
? [
- { name: 'rename-report', text: 'Rename' },
- { name: 'delete-report', text: 'Delete' },
+ { name: 'rename-report', text: t('Rename') },
+ { name: 'delete-report', text: t('Delete') },
Menu.line,
]
: [];
@@ -23,16 +25,16 @@ export function SaveReportMenu({
const modifiedMenu: MenuItem[] =
savedStatus === 'modified'
? [
- { name: 'rename-report', text: 'Rename' },
+ { name: 'rename-report', text: t('Rename') },
{
name: 'update-report',
- text: 'Update report',
+ text: t('Update report'),
},
{
name: 'reload-report',
- text: 'Revert changes',
+ text: t('Revert changes'),
},
- { name: 'delete-report', text: 'Delete' },
+ { name: 'delete-report', text: t('Delete') },
Menu.line,
]
: [];
@@ -40,16 +42,16 @@ export function SaveReportMenu({
const unsavedMenu: MenuItem[] = [
{
name: 'save-report',
- text: 'Save new report',
+ text: t('Save new report'),
},
{
name: 'reset-report',
- text: 'Reset to default',
+ text: t('Reset to default'),
},
Menu.line,
{
name: 'choose-report',
- text: 'Choose Report',
+ text: t('Choose Report'),
disabled: listReports > 0 ? false : true,
},
];
diff --git a/packages/desktop-client/src/components/util/DisplayId.tsx b/packages/desktop-client/src/components/util/DisplayId.tsx
index 34c5f593acc..162744a1772 100644
--- a/packages/desktop-client/src/components/util/DisplayId.tsx
+++ b/packages/desktop-client/src/components/util/DisplayId.tsx
@@ -1,5 +1,6 @@
// @ts-strict-ignore
import React from 'react';
+import { useTranslation } from 'react-i18next';
import { useAccount } from '../../hooks/useAccount';
import { usePayee } from '../../hooks/usePayee';
@@ -25,25 +26,27 @@ export function DisplayId({
}
function AccountDisplayId({ id, noneColor }) {
+ const { t } = useTranslation();
const account = useAccount(id);
return (
- {account ? account.name : 'None'}
+ {account ? account.name : t('None')}
);
}
function PayeeDisplayId({ id, noneColor }) {
+ const { t } = useTranslation();
const payee = usePayee(id);
return (
- {payee ? payee.name : 'None'}
+ {payee ? payee.name : t('None')}
);
}
diff --git a/packages/loot-core/src/server/budget/cleanup-template.ts b/packages/loot-core/src/server/budget/cleanup-template.ts
index b284e34a006..9c016fd031c 100644
--- a/packages/loot-core/src/server/budget/cleanup-template.ts
+++ b/packages/loot-core/src/server/budget/cleanup-template.ts
@@ -1,4 +1,6 @@
// @ts-strict-ignore
+import { t } from 'i18next';
+
import { Notification } from '../../client/state-types/notifications';
import * as monthUtils from '../../shared/months';
import * as db from '../db';
@@ -113,7 +115,7 @@ async function applyGroupCleanups(
});
}
} else {
- warnings.push(groupName + ' has no matching sink categories.');
+ warnings.push(groupName + t(' has no matching sink categories.'));
}
sourceGroups = sourceGroups.filter(c => c.group !== groupName);
groupLength = sourceGroups.length;
@@ -218,7 +220,7 @@ async function processCleanup(month: string): Promise {
});
num_sources += 1;
} else {
- warnings.push(category.name + ' does not have available funds.');
+ warnings.push(category.name + t(' does not have available funds.'));
}
const carryover = await db.first(
`SELECT carryover FROM zero_budgets WHERE month = ? and category = ?`,
@@ -285,7 +287,7 @@ async function processCleanup(month: string): Promise {
const budgetAvailable = await getSheetValue(sheetName, `to-budget`);
if (budgetAvailable <= 0) {
- warnings.push('Global: No funds are available to reallocate.');
+ warnings.push(t('Global: No funds are available to reallocate.'));
}
//fill sinking categories
@@ -320,19 +322,19 @@ async function processCleanup(month: string): Promise {
return {
type: 'error',
sticky: true,
- message: `There were errors interpreting some templates:`,
+ message: t('There were errors interpreting some templates:'),
pre: errors.join('\n\n'),
};
} else if (warnings.length) {
return {
type: 'warning',
- message: 'Global: Funds not available:',
+ message: t('Global: Funds not available:'),
pre: warnings.join('\n\n'),
};
} else {
return {
type: 'message',
- message: 'All categories were up to date.',
+ message: t('All categories were up to date.'),
};
}
} else {
@@ -342,13 +344,15 @@ async function processCleanup(month: string): Promise {
if (errors.length) {
return {
sticky: true,
- message: `${applied} There were errors interpreting some templates:`,
+ message: t('{applied} There were errors interpreting some templates:', {
+ applied,
+ }),
pre: errors.join('\n\n'),
};
} else if (warnings.length) {
return {
type: 'warning',
- message: 'Global: Funds not available:',
+ message: t('Global: Funds not available:'),
pre: warnings.join('\n\n'),
};
} else {
diff --git a/packages/loot-core/src/server/budget/goaltemplates.ts b/packages/loot-core/src/server/budget/goaltemplates.ts
index 27fd2aa485b..b1cc0d01ef0 100644
--- a/packages/loot-core/src/server/budget/goaltemplates.ts
+++ b/packages/loot-core/src/server/budget/goaltemplates.ts
@@ -1,4 +1,6 @@
// @ts-strict-ignore
+import { t } from 'i18next';
+
import { Notification } from '../../client/state-types/notifications';
import * as monthUtils from '../../shared/months';
import * as db from '../db';
@@ -188,13 +190,13 @@ async function processTemplate(
if (catObjects.length === 0 && errors.length === 0) {
return {
type: 'message',
- message: 'Everything is up to date',
+ message: t('Everything is up to date'),
};
}
if (errors.length > 0) {
return {
sticky: true,
- message: `There were errors interpreting some templates:`,
+ message: t('There were errors interpreting some templates:'),
pre: errors.join(`\n\n`),
};
}
@@ -245,6 +247,8 @@ async function processTemplate(
return {
type: 'message',
- message: `Successfully applied templates to ${catObjects.length} categories`,
+ message: t('Successfully applied templates to {length} categories', {
+ length: catObjects.length,
+ }),
};
}
diff --git a/packages/loot-core/src/server/main.ts b/packages/loot-core/src/server/main.ts
index a4965372f69..3a69c56886e 100644
--- a/packages/loot-core/src/server/main.ts
+++ b/packages/loot-core/src/server/main.ts
@@ -3,6 +3,7 @@ import './polyfills';
import * as injectAPI from '@actual-app/api/injected';
import * as CRDT from '@actual-app/crdt';
+import { t } from 'i18next';
import { v4 as uuidv4 } from 'uuid';
import { createTestBudget } from '../mocks/budget';
@@ -1087,14 +1088,18 @@ function handleSyncError(err, acct) {
accountId: acct.id,
message: err.reason
? err.reason
- : `Account “${acct.name}” is not linked properly. Please link it again.`,
+ : t(
+ 'Account “{acctName}” is not linked properly. Please link it again.',
+ { acctName: acct.name },
+ ),
};
}
return {
accountId: acct.id,
- message:
+ message: t(
'There was an internal error. Please get in touch https://actualbudget.org/contact for support.',
+ ),
internal: err.stack,
};
}
diff --git a/packages/loot-core/src/server/mutators.ts b/packages/loot-core/src/server/mutators.ts
index f5eac7b1d80..6c8fc834ff8 100644
--- a/packages/loot-core/src/server/mutators.ts
+++ b/packages/loot-core/src/server/mutators.ts
@@ -1,4 +1,6 @@
// @ts-strict-ignore
+import { t } from 'i18next';
+
import { captureException, captureBreadcrumb } from '../platform/exceptions';
import { sequential } from '../shared/async';
import { type HandlerFunctions, type Handlers } from '../types/handlers';
@@ -121,7 +123,7 @@ export function getMutatorContext() {
if (currentContext == null) {
captureBreadcrumb({
category: 'server',
- message: 'Recent methods: ' + _latestHandlerNames.join(', '),
+ message: t('Recent methods: ') + _latestHandlerNames.join(', '),
});
// captureException(new Error('getMutatorContext: mutator not running'));
diff --git a/packages/loot-core/src/server/sheet.ts b/packages/loot-core/src/server/sheet.ts
index b0729ba5cdf..476e62af9e5 100644
--- a/packages/loot-core/src/server/sheet.ts
+++ b/packages/loot-core/src/server/sheet.ts
@@ -1,5 +1,6 @@
// @ts-strict-ignore
import { type Database } from '@jlongster/sql.js';
+import { t } from 'i18next';
import { captureBreadcrumb } from '../platform/exceptions';
import * as sqlite from '../platform/server/sqlite';
@@ -132,7 +133,7 @@ export async function loadSpreadsheet(
}
captureBreadcrumb({
- message: 'loading spreaadsheet',
+ message: t('loading spreaadsheet'),
category: 'server',
});
@@ -162,7 +163,7 @@ export async function loadSpreadsheet(
}
captureBreadcrumb({
- message: 'loaded spreaadsheet',
+ message: t('loaded spreaadsheet'),
category: 'server',
});
diff --git a/packages/loot-core/src/shared/errors.ts b/packages/loot-core/src/shared/errors.ts
index ff8815c81ed..d27d984e2ff 100644
--- a/packages/loot-core/src/shared/errors.ts
+++ b/packages/loot-core/src/shared/errors.ts
@@ -1,4 +1,5 @@
// @ts-strict-ignore
+import { t } from 'i18next';
export function getUploadError({
reason,
meta,
@@ -8,23 +9,34 @@ export function getUploadError({
}) {
switch (reason) {
case 'unauthorized':
- return 'You are not logged in.';
+ return t('You are not logged in.');
case 'encrypt-failure':
if ((meta as { isMissingKey: boolean }).isMissingKey) {
- return 'Encrypting your file failed because you are missing your encryption key. Create your key in the next step.';
+ return t(
+ 'Encrypting your file failed because you are missing your encryption key. Create your key in the next step.',
+ );
}
- return 'Encrypting the file failed. You have the correct key so this is an internal bug. To fix this, generate a new key in the next step.';
+ return t(
+ 'Encrypting the file failed. You have the correct key so this is an internal bug. To fix this, generate a new key in the next step.',
+ );
case 'file-has-reset':
// Something really weird happened - during reset a sanity
// check on the server failed. The user just needs to
// restart the whole process.
- return 'Something went wrong while resetting your file. Please try again.';
+ return t(
+ 'Something went wrong while resetting your file. Please try again.',
+ );
case 'file-has-new-key':
- return 'Unable to encrypt your data because you are missing the key. Create the latest key in the next step.';
+ return t(
+ 'Unable to encrypt your data because you are missing the key. Create the latest key in the next step.',
+ );
case 'network':
- return 'Uploading the file failed. Check your network connection.';
+ return t('Uploading the file failed. Check your network connection.');
default:
- return `An internal error occurred, sorry! Visit https://actualbudget.org/contact/ for support. (ref: ${reason})`;
+ return t(
+ 'An internal error occurred, sorry! Visit https://actualbudget.org/contact/ for support. (ref: {reason})',
+ { reason },
+ );
}
}
@@ -32,11 +44,13 @@ export function getDownloadError({ reason, meta, fileName }) {
switch (reason) {
case 'network':
case 'download-failure':
- return 'Downloading the file failed. Check your network connection.';
+ return t('Downloading the file failed. Check your network connection.');
case 'not-zip-file':
case 'invalid-zip-file':
case 'invalid-meta-file':
- return 'Downloaded file is invalid, sorry! Visit https://actualbudget.org/contact/ for support.';
+ return t(
+ 'Downloaded file is invalid, sorry! Visit https://actualbudget.org/contact/ for support.',
+ );
case 'decrypt-failure':
return (
'Unable to decrypt file ' +
@@ -46,17 +60,15 @@ export function getDownloadError({ reason, meta, fileName }) {
);
case 'out-of-sync-migrations':
- return (
- 'This budget cannot be loaded with this version of the app. ' +
- 'Make sure the app is up-to-date.'
+ return t(
+ 'This budget cannot be loaded with this version of the app. Make sure the app is up-to-date.',
);
default:
const info = meta && meta.fileId ? `, fileId: ${meta.fileId}` : '';
- return (
- 'Something went wrong trying to download that file, sorry! ' +
- 'Visit https://actualbudget.org/contact/ for support. ' +
- `(reason: ${reason}${info})`
+ return t(
+ 'Something went wrong trying to download that file, sorry! Visit https://actualbudget.org/contact/ for support. reason: {reason}{info}',
+ { reason, info },
);
}
}
@@ -68,32 +80,37 @@ export function getCreateKeyError(error) {
export function getTestKeyError({ reason }) {
switch (reason) {
case 'network':
- return 'Unable to connect to the server. We need to access the server to get some information about your keys.';
+ return t(
+ 'Unable to connect to the server. We need to access the server to get some information about your keys.',
+ );
case 'old-key-style':
- return (
- 'This file is encrypted with an old unsupported key style. Recreate the key ' +
- 'on a device where the file is available, or use an older version of Actual to download ' +
- 'it.'
+ return t(
+ 'This file is encrypted with an old unsupported key style. Recreate the key on a device where the file is available, or use an older version of Actual to download it.',
);
case 'decrypt-failure':
- return 'Unable to decrypt file with this password. Please try again.';
+ return t('Unable to decrypt file with this password. Please try again.');
default:
- return 'Something went wrong trying to create a key, sorry! Visit https://actualbudget.org/contact/ for support.';
+ return t(
+ 'Something went wrong trying to create a key, sorry! Visit https://actualbudget.org/contact/ for support.',
+ );
}
}
export function getSyncError(error, id) {
if (error === 'out-of-sync-migrations' || error === 'out-of-sync-data') {
- return 'This budget cannot be loaded with this version of the app.';
+ return t('This budget cannot be loaded with this version of the app.');
} else if (error === 'budget-not-found') {
- return `Budget “${id}” not found. Check the id of your budget in the Advanced section of the settings page.`;
+ return t(
+ 'Budget “{id}” not found. Check the id of your budget in the Advanced section of the settings page.',
+ { id },
+ );
} else {
- return `We had an unknown problem opening “${id}”.`;
+ return t('We had an unknown problem opening “{id}”.', { id });
}
}
export function getBankSyncError(error: { message?: string }) {
- return error.message || 'We had an unknown problem syncing the account.';
+ return error.message || t('We had an unknown problem syncing the account.');
}
export class LazyLoadFailedError extends Error {
diff --git a/packages/loot-core/src/shared/rules.ts b/packages/loot-core/src/shared/rules.ts
index e194266d4aa..a779e79714e 100644
--- a/packages/loot-core/src/shared/rules.ts
+++ b/packages/loot-core/src/shared/rules.ts
@@ -1,4 +1,6 @@
// @ts-strict-ignore
+import { t } from 'i18next';
+
import { FieldValueTypes, RuleConditionOp } from '../types/models';
import { integerToAmount, amountToInteger, currencyToAmount } from './util';
@@ -112,20 +114,20 @@ export function mapField(field, opts?) {
switch (field) {
case 'imported_payee':
- return 'imported payee';
+ return t('imported payee');
case 'payee_name':
- return 'payee (name)';
+ return t('payee (name)');
case 'amount':
if (opts.inflow) {
- return 'amount (inflow)';
+ return t('amount (inflow)');
} else if (opts.outflow) {
- return 'amount (outflow)';
+ return t('amount (outflow)');
}
- return 'amount';
+ return t('amount');
case 'amount-inflow':
- return 'amount (inflow)';
+ return t('amount (inflow)');
case 'amount-outflow':
- return 'amount (outflow)';
+ return t('amount (outflow)');
default:
return field;
}
@@ -134,61 +136,61 @@ export function mapField(field, opts?) {
export function friendlyOp(op, type?) {
switch (op) {
case 'oneOf':
- return 'one of';
+ return t('one of');
case 'notOneOf':
- return 'not one of';
+ return t('not one of');
case 'is':
- return 'is';
+ return t('is');
case 'isNot':
- return 'is not';
+ return t('is not');
case 'isapprox':
- return 'is approx';
+ return t('is approx');
case 'isbetween':
- return 'is between';
+ return t('is between');
case 'contains':
- return 'contains';
+ return t('contains');
case 'hasTags':
- return 'has tag(s)';
+ return t('has tag(s)');
case 'matches':
- return 'matches';
+ return t('matches');
case 'doesNotContain':
- return 'does not contain';
+ return t('does not contain');
case 'gt':
if (type === 'date') {
- return 'is after';
+ return t('is after');
}
- return 'is greater than';
+ return t('is greater than');
case 'gte':
if (type === 'date') {
- return 'is after or equals';
+ return t('is after or equals');
}
- return 'is greater than or equals';
+ return t('is greater than or equals');
case 'lt':
if (type === 'date') {
- return 'is before';
+ return t('is before');
}
- return 'is less than';
+ return t('is less than');
case 'lte':
if (type === 'date') {
- return 'is before or equals';
+ return t('is before or equals');
}
- return 'is less than or equals';
+ return t('is less than or equals');
case 'true':
- return 'is true';
+ return t('is true');
case 'false':
- return 'is false';
+ return t('is false');
case 'set':
- return 'set';
+ return t('set');
case 'set-split-amount':
- return 'allocate';
+ return t('allocate');
case 'link-schedule':
- return 'link schedule';
+ return t('link schedule');
case 'prepend-notes':
- return 'prepend to notes';
+ return t('prepend to notes');
case 'append-notes':
- return 'append to notes';
+ return t('append to notes');
case 'and':
- return 'and';
+ return t('and');
case 'or':
return 'or';
default:
diff --git a/upcoming-release-notes/3827.md b/upcoming-release-notes/3827.md
new file mode 100644
index 00000000000..057317c6822
--- /dev/null
+++ b/upcoming-release-notes/3827.md
@@ -0,0 +1,6 @@
+---
+category: Enhancements
+authors: [awaisalee]
+---
+
+Enhance app with i18n translations