Implement localization for schedule descriptions (#225)
* monthUtils.{format → nonLocalizedFormat} * Implement localization for schedule descriptions * Remove outdated comment * Add general.ordinal in Spanish Co-Authored-By: Manuel Eduardo Cánepa Cihuelo <10290593+manuelcanepa@users.noreply.github.com> * yay time zones? * fix: re-add missing keys * fix: fix broken i18n imports/initialisation * style: linting * fix: re-add english ordinal keys * fix: add remaining english ordinal keys * fix: correct dates in schedules.js * refactor: store translations keys for loot-core in loot-core * fix: add ns to i18n.t calls directly so parser can find them * feat: add spanish translation from manuelcanepa * fix: add comments to help i18n-parser to find contexts * fix: add "many" context to spanish translations Co-authored-by: Manuel Eduardo Cánepa Cihuelo <10290593+manuelcanepa@users.noreply.github.com> Co-authored-by: Tom French <tom@tomfren.ch>
This commit is contained in:
parent
6fb497dec5
commit
5217835c55
28 changed files with 376 additions and 169 deletions
2
.github/workflows/i18n.yml
vendored
2
.github/workflows/i18n.yml
vendored
|
@ -15,4 +15,4 @@ jobs:
|
||||||
- name: Set up environment
|
- name: Set up environment
|
||||||
uses: ./.github/actions/setup
|
uses: ./.github/actions/setup
|
||||||
- name: Check i18n keys
|
- name: Check i18n keys
|
||||||
run: yarn workspace @actual-app/web check-i18n --fail-on-update
|
run: yarn workspaces foreach --verbose run check-i18n --fail-on-update
|
||||||
|
|
|
@ -2,6 +2,7 @@ module.exports = {
|
||||||
input: ['src/**/*.js'],
|
input: ['src/**/*.js'],
|
||||||
output: 'src/locales/$LOCALE.json',
|
output: 'src/locales/$LOCALE.json',
|
||||||
locales: ['en-GB', 'es-ES'],
|
locales: ['en-GB', 'es-ES'],
|
||||||
|
defaultNamespace: 'web',
|
||||||
sort: true,
|
sort: true,
|
||||||
// Force usage of JsxLexer for .js files as otherwise we can't pick up <Trans> components.
|
// Force usage of JsxLexer for .js files as otherwise we can't pick up <Trans> components.
|
||||||
lexers: {
|
lexers: {
|
||||||
|
|
|
@ -327,7 +327,7 @@ class Budget extends React.PureComponent {
|
||||||
pathname: '/accounts',
|
pathname: '/accounts',
|
||||||
state: {
|
state: {
|
||||||
goBack: true,
|
goBack: true,
|
||||||
filterName: `${categoryName} (${monthUtils.format(
|
filterName: `${categoryName} (${monthUtils.nonLocalizedFormat(
|
||||||
month,
|
month,
|
||||||
'MMMM yyyy'
|
'MMMM yyyy'
|
||||||
)})`,
|
)})`,
|
||||||
|
|
|
@ -259,7 +259,7 @@ function ScheduleDescription({ id }) {
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={{ margin: '0 5px' }}> — </Text>
|
<Text style={{ margin: '0 5px' }}> — </Text>
|
||||||
<Text style={{ flexShrink: 0 }}>
|
<Text style={{ flexShrink: 0 }}>
|
||||||
Next: {monthUtils.format(schedule.next_date, dateFormat)}
|
Next: {monthUtils.nonLocalizedFormat(schedule.next_date, dateFormat)}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<StatusBadge status={status} />
|
<StatusBadge status={status} />
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
|
||||||
import { format as formatDate, parseISO } from 'date-fns';
|
import { format as formatDate, parseISO } from 'date-fns';
|
||||||
|
@ -52,6 +53,7 @@ export function Value({
|
||||||
data: dataProp,
|
data: dataProp,
|
||||||
describe = x => x.name
|
describe = x => x.name
|
||||||
}) {
|
}) {
|
||||||
|
const { i18n } = useTranslation();
|
||||||
let { data, dateFormat } = useSelector(state => {
|
let { data, dateFormat } = useSelector(state => {
|
||||||
let data;
|
let data;
|
||||||
if (dataProp) {
|
if (dataProp) {
|
||||||
|
@ -95,7 +97,7 @@ export function Value({
|
||||||
} else if (field === 'date') {
|
} else if (field === 'date') {
|
||||||
if (value) {
|
if (value) {
|
||||||
if (value.frequency) {
|
if (value.frequency) {
|
||||||
return getRecurringDescription(value);
|
return getRecurringDescription(value, i18n);
|
||||||
}
|
}
|
||||||
return formatDate(parseISO(value), dateFormat);
|
return formatDate(parseISO(value), dateFormat);
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,7 +53,7 @@ function CashFlow() {
|
||||||
.rangeInclusive(earliestMonth, monthUtils.currentMonth())
|
.rangeInclusive(earliestMonth, monthUtils.currentMonth())
|
||||||
.map(month => ({
|
.map(month => ({
|
||||||
name: month,
|
name: month,
|
||||||
pretty: monthUtils.format(month, 'MMMM, yyyy')
|
pretty: monthUtils.nonLocalizedFormat(month, 'MMMM, yyyy')
|
||||||
}))
|
}))
|
||||||
.reverse();
|
.reverse();
|
||||||
|
|
||||||
|
|
|
@ -51,7 +51,7 @@ function NetWorth({ accounts }) {
|
||||||
.rangeInclusive(earliestMonth, monthUtils.currentMonth())
|
.rangeInclusive(earliestMonth, monthUtils.currentMonth())
|
||||||
.map(month => ({
|
.map(month => ({
|
||||||
name: month,
|
name: month,
|
||||||
pretty: monthUtils.format(month, 'MMMM, yyyy')
|
pretty: monthUtils.nonLocalizedFormat(month, 'MMMM, yyyy')
|
||||||
}))
|
}))
|
||||||
.reverse();
|
.reverse();
|
||||||
|
|
||||||
|
|
|
@ -36,12 +36,12 @@ let ROW_HEIGHT = 43;
|
||||||
function DiscoverSchedulesTable({ schedules, loading }) {
|
function DiscoverSchedulesTable({ schedules, loading }) {
|
||||||
let selectedItems = useSelectedItems();
|
let selectedItems = useSelectedItems();
|
||||||
let dispatchSelected = useSelectedDispatch();
|
let dispatchSelected = useSelectedDispatch();
|
||||||
const { t } = useTranslation();
|
let { t, i18n } = useTranslation();
|
||||||
|
|
||||||
function renderItem({ item }) {
|
function renderItem({ item }) {
|
||||||
let selected = selectedItems.has(item.id);
|
let selected = selectedItems.has(item.id);
|
||||||
let amountOp = item._conditions.find(c => c.field === 'amount').op;
|
let amountOp = item._conditions.find(c => c.field === 'amount').op;
|
||||||
let recurDescription = getRecurringDescription(item.date);
|
let recurDescription = getRecurringDescription(item.date, i18n);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row
|
<Row
|
||||||
|
|
|
@ -549,7 +549,9 @@ export default function ScheduleDetails() {
|
||||||
style={{ marginTop: 10, color: colors.n4 }}
|
style={{ marginTop: 10, color: colors.n4 }}
|
||||||
>
|
>
|
||||||
{state.upcomingDates.map(date => (
|
{state.upcomingDates.map(date => (
|
||||||
<View>{monthUtils.format(date, `${dateFormat} EEEE`)}</View>
|
<View>
|
||||||
|
{monthUtils.nonLocalizedFormat(date, `${dateFormat} EEEE`)}
|
||||||
|
</View>
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
</View>
|
</View>
|
||||||
|
|
|
@ -181,7 +181,7 @@ export function SchedulesTable({
|
||||||
</Field>
|
</Field>
|
||||||
<Field width={110}>
|
<Field width={110}>
|
||||||
{item.next_date
|
{item.next_date
|
||||||
? monthUtils.format(item.next_date, dateFormat)
|
? monthUtils.nonLocalizedFormat(item.next_date, dateFormat)
|
||||||
: null}
|
: null}
|
||||||
</Field>
|
</Field>
|
||||||
<Field width={120} style={{ alignItems: 'flex-start' }}>
|
<Field width={120} style={{ alignItems: 'flex-start' }}>
|
||||||
|
|
|
@ -16,7 +16,7 @@ import Navigation from './Navigation';
|
||||||
function Overspending({ navigationProps, stepTwo }) {
|
function Overspending({ navigationProps, stepTwo }) {
|
||||||
let currentMonth = monthUtils.currentMonth();
|
let currentMonth = monthUtils.currentMonth();
|
||||||
let sheetName = monthUtils.sheetForMonth(currentMonth);
|
let sheetName = monthUtils.sheetForMonth(currentMonth);
|
||||||
let month = monthUtils.format(currentMonth, 'MMM');
|
let month = monthUtils.nonLocalizedFormat(currentMonth, 'MMM');
|
||||||
let [minimized, toggle] = useMinimized();
|
let [minimized, toggle] = useMinimized();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -2,15 +2,20 @@ import { initReactI18next } from 'react-i18next';
|
||||||
|
|
||||||
import i18n from 'i18next';
|
import i18n from 'i18next';
|
||||||
|
|
||||||
|
import enUKCore from 'loot-core/src/locales/en-GB.json';
|
||||||
|
import esESCore from 'loot-core/src/locales/es-ES.json';
|
||||||
|
|
||||||
import enUK from './en-GB.json';
|
import enUK from './en-GB.json';
|
||||||
import esES from './es-ES.json';
|
import esES from './es-ES.json';
|
||||||
|
|
||||||
const resources = {
|
const resources = {
|
||||||
en: {
|
en: {
|
||||||
translation: enUK
|
web: enUK,
|
||||||
|
core: enUKCore
|
||||||
},
|
},
|
||||||
es: {
|
es: {
|
||||||
translation: esES
|
web: esES,
|
||||||
|
core: esESCore
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -18,6 +23,7 @@ i18n
|
||||||
.use(initReactI18next) // passes i18n down to react-i18next
|
.use(initReactI18next) // passes i18n down to react-i18next
|
||||||
.init({
|
.init({
|
||||||
resources,
|
resources,
|
||||||
|
defaultNS: 'web',
|
||||||
lng: 'es', // language to use, more information here: https://www.i18next.com/overview/configuration-options#languages-namespaces-resources
|
lng: 'es', // language to use, more information here: https://www.i18next.com/overview/configuration-options#languages-namespaces-resources
|
||||||
// you can use the i18n.changeLanguage function to change the language manually: https://www.i18next.com/overview/api#changelanguage
|
// you can use the i18n.changeLanguage function to change the language manually: https://www.i18next.com/overview/api#changelanguage
|
||||||
// if you're using a language detector, do not define the lng option
|
// if you're using a language detector, do not define the lng option
|
||||||
|
|
16
packages/loot-core/i18next-parser.config.js
Normal file
16
packages/loot-core/i18next-parser.config.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
module.exports = {
|
||||||
|
input: ['src/**/*.js'],
|
||||||
|
output: 'src/locales/$LOCALE.json',
|
||||||
|
locales: ['en-GB', 'es-ES'],
|
||||||
|
defaultNamespace: 'core',
|
||||||
|
sort: true,
|
||||||
|
// Force usage of JsxLexer for .js files as otherwise we can't pick up <Trans> components.
|
||||||
|
lexers: {
|
||||||
|
js: ['JsxLexer'],
|
||||||
|
ts: ['JsxLexer'],
|
||||||
|
jsx: ['JsxLexer'],
|
||||||
|
tsx: ['JsxLexer'],
|
||||||
|
|
||||||
|
default: ['JsxLexer']
|
||||||
|
}
|
||||||
|
};
|
|
@ -12,7 +12,8 @@
|
||||||
"lint": "eslint src",
|
"lint": "eslint src",
|
||||||
"test": "npm-run-all -cp 'test:*'",
|
"test": "npm-run-all -cp 'test:*'",
|
||||||
"test:node": "jest -c jest.config.js",
|
"test:node": "jest -c jest.config.js",
|
||||||
"test:web": "jest -c jest.web.config.js"
|
"test:web": "jest -c jest.web.config.js",
|
||||||
|
"check-i18n": "i18next"
|
||||||
},
|
},
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
|
@ -56,6 +57,8 @@
|
||||||
"fake-indexeddb": "^3.1.3",
|
"fake-indexeddb": "^3.1.3",
|
||||||
"fast-check": "2.13.0",
|
"fast-check": "2.13.0",
|
||||||
"fast-glob": "^2.2.0",
|
"fast-glob": "^2.2.0",
|
||||||
|
"i18next": "^21.9.1",
|
||||||
|
"i18next-parser": "^6.5.0",
|
||||||
"jest": "^28.1.0",
|
"jest": "^28.1.0",
|
||||||
"jsverify": "^0.8.4",
|
"jsverify": "^0.8.4",
|
||||||
"lru-cache": "^5.1.1",
|
"lru-cache": "^5.1.1",
|
||||||
|
|
27
packages/loot-core/src/locales/en-GB.json
Normal file
27
packages/loot-core/src/locales/en-GB.json
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"general": {
|
||||||
|
"ordinal_one": "{{count}}st",
|
||||||
|
"ordinal_two": "{{count}}nd",
|
||||||
|
"ordinal_few": "{{count}}rd",
|
||||||
|
"ordinal_other": "{{count}}th"
|
||||||
|
},
|
||||||
|
"schedules": {
|
||||||
|
"recurring": {
|
||||||
|
"monthly_one": "Every month on the {{day}}",
|
||||||
|
"monthly_other": "Every {{count}} months on the {{day}}",
|
||||||
|
"monthlyPattern_one": "Every month on the {{pattern}}",
|
||||||
|
"monthlyPattern_other": "Every {{count}} months on the {{pattern}}",
|
||||||
|
"pattern": {
|
||||||
|
"lastDay": "last day",
|
||||||
|
"lastWeekday": "last {{dayName}}",
|
||||||
|
"lastWeekday_sameDay": "last",
|
||||||
|
"weekAndDay": "{{week}} {{dayName}}",
|
||||||
|
"weekAndDay_sameDay": "{{week}}"
|
||||||
|
},
|
||||||
|
"weekly_one": "Every week on {{day}}",
|
||||||
|
"weekly_other": "Every {{count}} weeks on {{day}}",
|
||||||
|
"yearly_one": "Every year on {{day}}",
|
||||||
|
"yearly_other": "Every {{count}} years on {{day}}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
28
packages/loot-core/src/locales/es-ES.json
Normal file
28
packages/loot-core/src/locales/es-ES.json
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
{
|
||||||
|
"general": {
|
||||||
|
"ordinal_other": "{{count}}º"
|
||||||
|
},
|
||||||
|
"schedules": {
|
||||||
|
"recurring": {
|
||||||
|
"monthly_one": "Cada {{day}} día del mes",
|
||||||
|
"monthly_many": "Cada {{count}} de meses el {{day}} día",
|
||||||
|
"monthly_other": "Cada {{count}} meses el {{day}} día",
|
||||||
|
"monthlyPattern_one": "Cada mes {{pattern}}",
|
||||||
|
"monthlyPattern_many": "Cada {{count}} de meses {{pattern}}",
|
||||||
|
"monthlyPattern_other": "Cada {{count}} meses {{pattern}}",
|
||||||
|
"pattern": {
|
||||||
|
"lastDay": "el último día",
|
||||||
|
"lastWeekday": "el último {{dayName}}",
|
||||||
|
"lastWeekday_sameDay": "el último",
|
||||||
|
"weekAndDay": "{{week}} {{dayName}}",
|
||||||
|
"weekAndDay_sameDay": "{{week}}"
|
||||||
|
},
|
||||||
|
"weekly_one": "Cada semana {{day}}",
|
||||||
|
"weekly_many": "Cada {{count}} de semanas el {{day}}",
|
||||||
|
"weekly_other": "Cada {{count}} semanas el {{day}}",
|
||||||
|
"yearly_one": "Cada año el {{day}}",
|
||||||
|
"yearly_many": "Cada {{count}} de años el {{day}}",
|
||||||
|
"yearly_other": "Cada {{count}} años el {{day}}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -210,11 +210,11 @@ export function sheetForMonth(month) {
|
||||||
return 'budget' + month.replace('-', '');
|
return 'budget' + month.replace('-', '');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function nameForMonth(month) {
|
export function format(month, opts, locale) {
|
||||||
return d.format(_parse(month), "MMMM 'yy");
|
return Intl.DateTimeFormat(locale, opts).format(_parse(month));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function format(month, str) {
|
export function nonLocalizedFormat(month, str) {
|
||||||
return d.format(_parse(month), str);
|
return d.format(_parse(month), str);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -42,39 +42,49 @@ export function getHasTransactionsQuery(schedules) {
|
||||||
.select(['schedule', 'date']);
|
.select(['schedule', 'date']);
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeNumberSuffix(num) {
|
function prettyDayName(day, locale) {
|
||||||
// Slight abuse of date-fns to turn a number like "1" into the full
|
|
||||||
// form "1st" but formatting a date with that number
|
|
||||||
return monthUtils.format(new Date(2020, 0, num, 12), 'do');
|
|
||||||
}
|
|
||||||
|
|
||||||
function prettyDayName(day) {
|
|
||||||
let days = {
|
let days = {
|
||||||
SU: 'Sunday',
|
SU: new Date('2020-01-05T12:00:00.000Z'),
|
||||||
MO: 'Monday',
|
MO: new Date('2020-01-06T12:00:00.000Z'),
|
||||||
TU: 'Tuesday',
|
TU: new Date('2020-01-07T12:00:00.000Z'),
|
||||||
WE: 'Wednesday',
|
WE: new Date('2020-01-08T12:00:00.000Z'),
|
||||||
TH: 'Thursday',
|
TH: new Date('2020-01-09T12:00:00.000Z'),
|
||||||
FR: 'Friday',
|
FR: new Date('2020-01-10T12:00:00.000Z'),
|
||||||
SA: 'Saturday'
|
SA: new Date('2020-01-11T12:00:00.000Z')
|
||||||
};
|
};
|
||||||
return days[day];
|
return Intl.DateTimeFormat(locale, { weekday: 'long' }).format(days[day]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getRecurringDescription(config) {
|
function formatMonthAndDay(month, i18n) {
|
||||||
|
let parts = Intl.DateTimeFormat(i18n.resolvedLanguage, {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric'
|
||||||
|
}).formatToParts(monthUtils.parseDate(month));
|
||||||
|
let dayPart = parts.find(p => p.type === 'day');
|
||||||
|
dayPart.value = i18n.t('general.ordinal', {
|
||||||
|
count: monthUtils.parseDate(month).getDate(),
|
||||||
|
ordinal: true,
|
||||||
|
ns: 'core'
|
||||||
|
});
|
||||||
|
return parts.map(part => part.value).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRecurringDescription(config, i18n) {
|
||||||
let interval = config.interval || 1;
|
let interval = config.interval || 1;
|
||||||
|
|
||||||
switch (config.frequency) {
|
switch (config.frequency) {
|
||||||
case 'weekly': {
|
case 'weekly': {
|
||||||
let desc = 'Every ';
|
return i18n.t('schedules.recurring.weekly', {
|
||||||
desc += interval !== 1 ? `${interval} weeks` : 'week';
|
count: interval,
|
||||||
desc += ' on ' + monthUtils.format(config.start, 'EEEE');
|
day: monthUtils.format(
|
||||||
return desc;
|
config.start,
|
||||||
|
{ weekday: 'long' },
|
||||||
|
i18n.resolvedLanguage
|
||||||
|
),
|
||||||
|
ns: 'core'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
case 'monthly': {
|
case 'monthly': {
|
||||||
let desc = 'Every ';
|
|
||||||
desc += interval !== 1 ? `${interval} months` : 'month';
|
|
||||||
|
|
||||||
if (config.patterns && config.patterns.length > 0) {
|
if (config.patterns && config.patterns.length > 0) {
|
||||||
// Sort the days ascending. We filter out -1 because that
|
// Sort the days ascending. We filter out -1 because that
|
||||||
// represents "last days" and should always be last, but this
|
// represents "last days" and should always be last, but this
|
||||||
|
@ -95,56 +105,101 @@ export function getRecurringDescription(config) {
|
||||||
// Add on all -1 values to the end
|
// Add on all -1 values to the end
|
||||||
patterns = patterns.concat(config.patterns.filter(p => p.value === -1));
|
patterns = patterns.concat(config.patterns.filter(p => p.value === -1));
|
||||||
|
|
||||||
desc += ' on the ';
|
|
||||||
|
|
||||||
let strs = [];
|
let strs = [];
|
||||||
|
|
||||||
let uniqueDays = new Set(patterns.map(p => p.type));
|
let uniqueDays = new Set(patterns.map(p => p.type));
|
||||||
let isSameDay = uniqueDays.length === 1 && !uniqueDays.has('day');
|
let context =
|
||||||
|
uniqueDays.length === 1 && !uniqueDays.has('day')
|
||||||
|
? 'sameDay'
|
||||||
|
: undefined;
|
||||||
|
|
||||||
for (let pattern of patterns) {
|
for (let pattern of patterns) {
|
||||||
if (pattern.type === 'day') {
|
if (pattern.type === 'day') {
|
||||||
if (pattern.value === -1) {
|
if (pattern.value === -1) {
|
||||||
strs.push('last day');
|
strs.push(
|
||||||
|
i18n.t('schedules.recurring.pattern.lastDay', {
|
||||||
|
ns: 'core'
|
||||||
|
})
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
// Example: 15th day
|
strs.push(
|
||||||
strs.push(makeNumberSuffix(pattern.value));
|
i18n.t('general.ordinal', {
|
||||||
|
count: pattern.value,
|
||||||
|
ordinal: true,
|
||||||
|
ns: 'core'
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let dayName = isSameDay ? '' : ' ' + prettyDayName(pattern.type);
|
let dayName = prettyDayName(
|
||||||
|
pattern.type,
|
||||||
|
i18n.resolvedLanguage,
|
||||||
|
i18n.resolvedLanguage
|
||||||
|
);
|
||||||
|
|
||||||
if (pattern.value === -1) {
|
if (pattern.value === -1) {
|
||||||
// Example: last Monday
|
// Example: last Monday
|
||||||
strs.push('last' + dayName);
|
// t('schedules.recurring.pattern.lastWeekday')
|
||||||
|
// t('schedules.recurring.pattern.lastWeekday_sameDay')
|
||||||
|
strs.push(
|
||||||
|
i18n.t('schedules.recurring.pattern.lastWeekday', {
|
||||||
|
context,
|
||||||
|
dayName,
|
||||||
|
ns: 'core'
|
||||||
|
})
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
// Example: 3rd Monday
|
// Example: 3rd Monday
|
||||||
strs.push(makeNumberSuffix(pattern.value) + dayName);
|
// t('schedules.recurring.pattern.weekAndDay')
|
||||||
|
// t('schedules.recurring.pattern.weekAndDay_sameDay')
|
||||||
|
strs.push(
|
||||||
|
i18n.t('schedules.recurring.pattern.weekAndDay', {
|
||||||
|
context,
|
||||||
|
week: i18n.t('general.ordinal', {
|
||||||
|
count: pattern.value,
|
||||||
|
ordinal: true,
|
||||||
|
ns: 'core'
|
||||||
|
}),
|
||||||
|
dayName,
|
||||||
|
ns: 'core'
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (strs.length > 2) {
|
return i18n.t('schedules.recurring.monthlyPattern', {
|
||||||
desc += strs.slice(0, strs.length - 1).join(', ');
|
context,
|
||||||
desc += ', and ';
|
count: interval,
|
||||||
desc += strs[strs.length - 1];
|
day: prettyDayName(patterns[0].type, i18n.resolvedLanguage),
|
||||||
|
pattern: new Intl.ListFormat(i18n.resolvedLanguage, {
|
||||||
|
style: 'long',
|
||||||
|
type: 'conjunction'
|
||||||
|
}).format(strs),
|
||||||
|
ns: 'core'
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
desc += strs.join(' and ');
|
return i18n.t('schedules.recurring.monthly', {
|
||||||
|
count: interval,
|
||||||
|
day: i18n.t('general.ordinal', {
|
||||||
|
count: monthUtils.parseDate(config.start).getDate(),
|
||||||
|
ordinal: true,
|
||||||
|
ns: 'core'
|
||||||
|
}),
|
||||||
|
ns: 'core'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isSameDay) {
|
|
||||||
desc += ' ' + prettyDayName(patterns[0].type);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
desc += ' on the ' + monthUtils.format(config.start, 'do');
|
|
||||||
}
|
|
||||||
|
|
||||||
return desc;
|
|
||||||
}
|
}
|
||||||
case 'yearly': {
|
case 'yearly': {
|
||||||
let desc = 'Every ';
|
return i18n.t(
|
||||||
desc += interval !== 1 ? `${interval} years` : 'year';
|
'schedules.recurring.yearly',
|
||||||
desc += ' on ' + monthUtils.format(config.start, 'LLL do');
|
{
|
||||||
return desc;
|
count: interval,
|
||||||
|
day: formatMonthAndDay(config.start, i18n),
|
||||||
|
ns: 'core'
|
||||||
|
},
|
||||||
|
i18n.resolvedLanguage
|
||||||
|
);
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return 'Recurring error';
|
return 'Recurring error';
|
||||||
|
|
|
@ -1,7 +1,18 @@
|
||||||
|
import i18n from 'i18next';
|
||||||
import MockDate from 'mockdate';
|
import MockDate from 'mockdate';
|
||||||
|
|
||||||
|
import enUKCore from '../locales/en-GB.json';
|
||||||
import { getRecurringDescription } from './schedules';
|
import { getRecurringDescription } from './schedules';
|
||||||
|
|
||||||
|
i18n.init({
|
||||||
|
lng: 'en',
|
||||||
|
resources: {
|
||||||
|
en: {
|
||||||
|
core: enUKCore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
describe('recurring date description', () => {
|
describe('recurring date description', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
MockDate.set(new Date(2021, 4, 14));
|
MockDate.set(new Date(2021, 4, 14));
|
||||||
|
@ -9,88 +20,119 @@ describe('recurring date description', () => {
|
||||||
|
|
||||||
it('describes weekly interval', () => {
|
it('describes weekly interval', () => {
|
||||||
expect(
|
expect(
|
||||||
getRecurringDescription({ start: '2021-05-17', frequency: 'weekly' })
|
getRecurringDescription(
|
||||||
|
{ start: '2021-05-17', frequency: 'weekly' },
|
||||||
|
i18n
|
||||||
|
)
|
||||||
).toBe('Every week on Monday');
|
).toBe('Every week on Monday');
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
getRecurringDescription({
|
getRecurringDescription(
|
||||||
|
{
|
||||||
start: '2021-05-17',
|
start: '2021-05-17',
|
||||||
frequency: 'weekly',
|
frequency: 'weekly',
|
||||||
interval: 2
|
interval: 2
|
||||||
})
|
},
|
||||||
|
i18n
|
||||||
|
)
|
||||||
).toBe('Every 2 weeks on Monday');
|
).toBe('Every 2 weeks on Monday');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('describes monthly interval', () => {
|
it('describes monthly interval', () => {
|
||||||
expect(
|
expect(
|
||||||
getRecurringDescription({ start: '2021-04-25', frequency: 'monthly' })
|
getRecurringDescription(
|
||||||
|
{ start: '2021-04-25', frequency: 'monthly' },
|
||||||
|
i18n
|
||||||
|
)
|
||||||
).toBe('Every month on the 25th');
|
).toBe('Every month on the 25th');
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
getRecurringDescription({
|
getRecurringDescription(
|
||||||
|
{
|
||||||
start: '2021-04-25',
|
start: '2021-04-25',
|
||||||
frequency: 'monthly',
|
frequency: 'monthly',
|
||||||
interval: 2
|
interval: 2
|
||||||
})
|
},
|
||||||
|
i18n
|
||||||
|
)
|
||||||
).toBe('Every 2 months on the 25th');
|
).toBe('Every 2 months on the 25th');
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
getRecurringDescription({
|
getRecurringDescription(
|
||||||
|
{
|
||||||
start: '2021-04-25',
|
start: '2021-04-25',
|
||||||
frequency: 'monthly',
|
frequency: 'monthly',
|
||||||
patterns: [{ type: 'day', value: 25 }]
|
patterns: [{ type: 'day', value: 25 }]
|
||||||
})
|
},
|
||||||
|
i18n
|
||||||
|
)
|
||||||
).toBe('Every month on the 25th');
|
).toBe('Every month on the 25th');
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
getRecurringDescription({
|
getRecurringDescription(
|
||||||
|
{
|
||||||
start: '2021-04-25',
|
start: '2021-04-25',
|
||||||
frequency: 'monthly',
|
frequency: 'monthly',
|
||||||
interval: 2,
|
interval: 2,
|
||||||
patterns: [{ type: 'day', value: 25 }]
|
patterns: [{ type: 'day', value: 25 }]
|
||||||
})
|
},
|
||||||
|
i18n
|
||||||
|
)
|
||||||
).toBe('Every 2 months on the 25th');
|
).toBe('Every 2 months on the 25th');
|
||||||
|
|
||||||
// Last day should work
|
// Last day should work
|
||||||
expect(
|
expect(
|
||||||
getRecurringDescription({
|
getRecurringDescription(
|
||||||
|
{
|
||||||
start: '2021-04-25',
|
start: '2021-04-25',
|
||||||
frequency: 'monthly',
|
frequency: 'monthly',
|
||||||
patterns: [{ type: 'day', value: 31 }]
|
patterns: [{ type: 'day', value: 31 }]
|
||||||
})
|
},
|
||||||
|
i18n
|
||||||
|
)
|
||||||
).toBe('Every month on the 31st');
|
).toBe('Every month on the 31st');
|
||||||
|
|
||||||
// -1 should work, representing the last day
|
// -1 should work, representing the last day
|
||||||
expect(
|
expect(
|
||||||
getRecurringDescription({
|
getRecurringDescription(
|
||||||
|
{
|
||||||
start: '2021-04-25',
|
start: '2021-04-25',
|
||||||
frequency: 'monthly',
|
frequency: 'monthly',
|
||||||
patterns: [{ type: 'day', value: -1 }]
|
patterns: [{ type: 'day', value: -1 }]
|
||||||
})
|
},
|
||||||
|
i18n
|
||||||
|
)
|
||||||
).toBe('Every month on the last day');
|
).toBe('Every month on the last day');
|
||||||
|
|
||||||
// Day names should work
|
// Day names should work
|
||||||
expect(
|
expect(
|
||||||
getRecurringDescription({
|
getRecurringDescription(
|
||||||
|
{
|
||||||
start: '2021-04-25',
|
start: '2021-04-25',
|
||||||
frequency: 'monthly',
|
frequency: 'monthly',
|
||||||
patterns: [{ type: 'FR', value: 2 }]
|
patterns: [{ type: 'FR', value: 2 }]
|
||||||
})
|
},
|
||||||
|
i18n
|
||||||
|
)
|
||||||
).toBe('Every month on the 2nd Friday');
|
).toBe('Every month on the 2nd Friday');
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
getRecurringDescription({
|
getRecurringDescription(
|
||||||
|
{
|
||||||
start: '2021-04-25',
|
start: '2021-04-25',
|
||||||
frequency: 'monthly',
|
frequency: 'monthly',
|
||||||
patterns: [{ type: 'FR', value: -1 }]
|
patterns: [{ type: 'FR', value: -1 }]
|
||||||
})
|
},
|
||||||
|
i18n
|
||||||
|
)
|
||||||
).toBe('Every month on the last Friday');
|
).toBe('Every month on the last Friday');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('describes monthly interval with multiple days', () => {
|
it('describes monthly interval with multiple days', () => {
|
||||||
// Note how order doesn't matter - the day should be sorted
|
// Note how order doesn't matter - the day should be sorted
|
||||||
expect(
|
expect(
|
||||||
getRecurringDescription({
|
getRecurringDescription(
|
||||||
|
{
|
||||||
start: '2021-04-25',
|
start: '2021-04-25',
|
||||||
frequency: 'monthly',
|
frequency: 'monthly',
|
||||||
patterns: [
|
patterns: [
|
||||||
|
@ -98,11 +140,14 @@ describe('recurring date description', () => {
|
||||||
{ type: 'day', value: 3 },
|
{ type: 'day', value: 3 },
|
||||||
{ type: 'day', value: 20 }
|
{ type: 'day', value: 20 }
|
||||||
]
|
]
|
||||||
})
|
},
|
||||||
|
i18n
|
||||||
|
)
|
||||||
).toBe('Every month on the 3rd, 15th, and 20th');
|
).toBe('Every month on the 3rd, 15th, and 20th');
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
getRecurringDescription({
|
getRecurringDescription(
|
||||||
|
{
|
||||||
start: '2021-04-25',
|
start: '2021-04-25',
|
||||||
frequency: 'monthly',
|
frequency: 'monthly',
|
||||||
patterns: [
|
patterns: [
|
||||||
|
@ -110,12 +155,15 @@ describe('recurring date description', () => {
|
||||||
{ type: 'day', value: -1 },
|
{ type: 'day', value: -1 },
|
||||||
{ type: 'day', value: 20 }
|
{ type: 'day', value: 20 }
|
||||||
]
|
]
|
||||||
})
|
},
|
||||||
|
i18n
|
||||||
|
)
|
||||||
).toBe('Every month on the 3rd, 20th, and last day');
|
).toBe('Every month on the 3rd, 20th, and last day');
|
||||||
|
|
||||||
// Mix days and day names
|
// Mix days and day names
|
||||||
expect(
|
expect(
|
||||||
getRecurringDescription({
|
getRecurringDescription(
|
||||||
|
{
|
||||||
start: '2021-04-25',
|
start: '2021-04-25',
|
||||||
frequency: 'monthly',
|
frequency: 'monthly',
|
||||||
patterns: [
|
patterns: [
|
||||||
|
@ -123,12 +171,15 @@ describe('recurring date description', () => {
|
||||||
{ type: 'day', value: -1 },
|
{ type: 'day', value: -1 },
|
||||||
{ type: 'FR', value: 2 }
|
{ type: 'FR', value: 2 }
|
||||||
]
|
]
|
||||||
})
|
},
|
||||||
|
i18n
|
||||||
|
)
|
||||||
).toBe('Every month on the 2nd Friday, 3rd, and last day');
|
).toBe('Every month on the 2nd Friday, 3rd, and last day');
|
||||||
|
|
||||||
// When there is a mixture of types, day names should always come first
|
// When there is a mixture of types, day names should always come first
|
||||||
expect(
|
expect(
|
||||||
getRecurringDescription({
|
getRecurringDescription(
|
||||||
|
{
|
||||||
start: '2021-04-25',
|
start: '2021-04-25',
|
||||||
frequency: 'monthly',
|
frequency: 'monthly',
|
||||||
patterns: [
|
patterns: [
|
||||||
|
@ -137,21 +188,29 @@ describe('recurring date description', () => {
|
||||||
{ type: 'FR', value: 3 },
|
{ type: 'FR', value: 3 },
|
||||||
{ type: 'day', value: 10 }
|
{ type: 'day', value: 10 }
|
||||||
]
|
]
|
||||||
})
|
},
|
||||||
|
i18n
|
||||||
|
)
|
||||||
).toBe('Every month on the 1st Saturday, 3rd Friday, 2nd, and 10th');
|
).toBe('Every month on the 1st Saturday, 3rd Friday, 2nd, and 10th');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('describes yearly interval', () => {
|
it('describes yearly interval', () => {
|
||||||
expect(
|
expect(
|
||||||
getRecurringDescription({ start: '2021-05-17', frequency: 'yearly' })
|
getRecurringDescription(
|
||||||
|
{ start: '2021-05-17', frequency: 'yearly' },
|
||||||
|
i18n
|
||||||
|
)
|
||||||
).toBe('Every year on May 17th');
|
).toBe('Every year on May 17th');
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
getRecurringDescription({
|
getRecurringDescription(
|
||||||
|
{
|
||||||
start: '2021-05-17',
|
start: '2021-05-17',
|
||||||
frequency: 'yearly',
|
frequency: 'yearly',
|
||||||
interval: 2
|
interval: 2
|
||||||
})
|
},
|
||||||
|
i18n
|
||||||
|
)
|
||||||
).toBe('Every 2 years on May 17th');
|
).toBe('Every 2 years on May 17th');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import React, { useEffect, useReducer, useState } from 'react';
|
import React, { useEffect, useReducer, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
|
|
||||||
import { sendCatch } from 'loot-core/src/platform/client/fetch';
|
import { sendCatch } from 'loot-core/src/platform/client/fetch';
|
||||||
|
@ -12,8 +13,6 @@ import SubtractIcon from 'loot-design/src/svg/Subtract';
|
||||||
import { Button, Select, Input, Tooltip, View, Text, Stack } from './common';
|
import { Button, Select, Input, Tooltip, View, Text, Stack } from './common';
|
||||||
import DateSelect from './DateSelect';
|
import DateSelect from './DateSelect';
|
||||||
|
|
||||||
const DATE_FORMAT = 'yyyy-MM-dd';
|
|
||||||
|
|
||||||
// ex: There is no 6th Friday of the Month
|
// ex: There is no 6th Friday of the Month
|
||||||
const MAX_DAY_OF_WEEK_INTERVAL = 5;
|
const MAX_DAY_OF_WEEK_INTERVAL = 5;
|
||||||
|
|
||||||
|
@ -62,7 +61,7 @@ function unparseConfig(parsed) {
|
||||||
|
|
||||||
function createMonthlyRecurrence(startDate) {
|
function createMonthlyRecurrence(startDate) {
|
||||||
return {
|
return {
|
||||||
value: parseInt(monthUtils.format(startDate, 'd')),
|
value: parseInt(monthUtils.nonLocalizedFormat(startDate, 'd')),
|
||||||
type: 'day'
|
type: 'day'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -152,8 +151,8 @@ function SchedulePreview({ previewDates }) {
|
||||||
<Stack direction="row" spacing={4} style={{ marginTop: 10 }}>
|
<Stack direction="row" spacing={4} style={{ marginTop: 10 }}>
|
||||||
{previewDates.map(d => (
|
{previewDates.map(d => (
|
||||||
<View>
|
<View>
|
||||||
<Text>{monthUtils.format(d, dateFormat)}</Text>
|
<Text>{monthUtils.nonLocalizedFormat(d, dateFormat)}</Text>
|
||||||
<Text>{monthUtils.format(d, 'EEEE')}</Text>
|
<Text>{monthUtils.nonLocalizedFormat(d, 'EEEE')}</Text>
|
||||||
</View>
|
</View>
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
@ -370,6 +369,7 @@ export default function RecurringSchedulePicker({
|
||||||
onChange
|
onChange
|
||||||
}) {
|
}) {
|
||||||
let { isOpen, close, getOpenEvents } = useTooltip();
|
let { isOpen, close, getOpenEvents } = useTooltip();
|
||||||
|
let { i18n } = useTranslation();
|
||||||
|
|
||||||
function onSave(config) {
|
function onSave(config) {
|
||||||
onChange(config);
|
onChange(config);
|
||||||
|
@ -379,7 +379,7 @@ export default function RecurringSchedulePicker({
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<Button {...getOpenEvents()} style={[{ textAlign: 'left' }, buttonStyle]}>
|
<Button {...getOpenEvents()} style={[{ textAlign: 'left' }, buttonStyle]}>
|
||||||
{value ? getRecurringDescription(value) : 'No recurring date'}
|
{value ? getRecurringDescription(value, i18n) : 'No recurring date'}
|
||||||
</Button>
|
</Button>
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<RecurringScheduleTooltip
|
<RecurringScheduleTooltip
|
||||||
|
|
|
@ -1256,7 +1256,7 @@ export const MonthPicker = scope(lively => {
|
||||||
|
|
||||||
function getCurrentMonthName(startMonth, currentMonth) {
|
function getCurrentMonthName(startMonth, currentMonth) {
|
||||||
return monthUtils.getYear(startMonth) === monthUtils.getYear(currentMonth)
|
return monthUtils.getYear(startMonth) === monthUtils.getYear(currentMonth)
|
||||||
? monthUtils.format(currentMonth, 'MMM')
|
? monthUtils.nonLocalizedFormat(currentMonth, 'MMM')
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1264,7 +1264,7 @@ export const MonthPicker = scope(lively => {
|
||||||
const currentMonth = monthUtils.currentMonth();
|
const currentMonth = monthUtils.currentMonth();
|
||||||
const range = getRangeForYear(currentMonth);
|
const range = getRangeForYear(currentMonth);
|
||||||
const monthNames = range.map(month => {
|
const monthNames = range.map(month => {
|
||||||
return monthUtils.format(month, 'MMM');
|
return monthUtils.nonLocalizedFormat(month, 'MMM');
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -1314,7 +1314,7 @@ export const MonthPicker = scope(lively => {
|
||||||
flex: '0 0 40px'
|
flex: '0 0 40px'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{monthUtils.format(year, 'yyyy')}
|
{monthUtils.nonLocalizedFormat(year, 'yyyy')}
|
||||||
</View>
|
</View>
|
||||||
<ElementQuery
|
<ElementQuery
|
||||||
sizes={[
|
sizes={[
|
||||||
|
|
|
@ -321,7 +321,7 @@ export default React.memo(function BudgetSummary({ month }) {
|
||||||
currentMonth === month && { textDecoration: 'underline' }
|
currentMonth === month && { textDecoration: 'underline' }
|
||||||
])}
|
])}
|
||||||
>
|
>
|
||||||
{monthUtils.format(month, 'MMMM')}
|
{monthUtils.nonLocalizedFormat(month, 'MMMM')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<View
|
<View
|
||||||
|
|
|
@ -271,7 +271,10 @@ export default React.memo(function BudgetSummary({ month }) {
|
||||||
setMenuOpen(false);
|
setMenuOpen(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
let prevMonthName = monthUtils.format(monthUtils.prevMonth(month), 'MMM');
|
let prevMonthName = monthUtils.nonLocalizedFormat(
|
||||||
|
monthUtils.prevMonth(month),
|
||||||
|
'MMM'
|
||||||
|
);
|
||||||
|
|
||||||
let ExpandOrCollapseIcon = collapsed ? ArrowButtonDown1 : ArrowButtonUp1;
|
let ExpandOrCollapseIcon = collapsed ? ArrowButtonDown1 : ArrowButtonUp1;
|
||||||
|
|
||||||
|
@ -337,7 +340,7 @@ export default React.memo(function BudgetSummary({ month }) {
|
||||||
currentMonth === month && { textDecoration: 'underline' }
|
currentMonth === month && { textDecoration: 'underline' }
|
||||||
])}
|
])}
|
||||||
>
|
>
|
||||||
{monthUtils.format(month, 'MMMM')}
|
{monthUtils.nonLocalizedFormat(month, 'MMMM')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<View
|
<View
|
||||||
|
|
|
@ -1092,7 +1092,7 @@ export function BudgetHeader({
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
{monthUtils.format(currentMonth, "MMMM ''yy")}
|
{monthUtils.nonLocalizedFormat(currentMonth, "MMMM ''yy")}
|
||||||
</Text>
|
</Text>
|
||||||
{editMode ? (
|
{editMode ? (
|
||||||
<Button
|
<Button
|
||||||
|
|
|
@ -692,7 +692,7 @@ export function DateHeader({ date }) {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text style={[styles.text, { fontSize: 13, color: colors.n4 }]}>
|
<Text style={[styles.text, { fontSize: 13, color: colors.n4 }]}>
|
||||||
{monthUtils.format(date, 'MMMM dd, yyyy')}
|
{monthUtils.nonLocalizedFormat(date, 'MMMM dd, yyyy')}
|
||||||
</Text>
|
</Text>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
);
|
);
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { connect } from 'react-redux';
|
||||||
import * as d from 'date-fns';
|
import * as d from 'date-fns';
|
||||||
|
|
||||||
import * as actions from 'loot-core/src/client/actions';
|
import * as actions from 'loot-core/src/client/actions';
|
||||||
import { format as formatDate_ } from 'loot-core/src/shared/months';
|
import { nonLocalizedFormat as formatDate_ } from 'loot-core/src/shared/months';
|
||||||
import {
|
import {
|
||||||
amountToCurrency,
|
amountToCurrency,
|
||||||
amountToInteger,
|
amountToInteger,
|
||||||
|
|
|
@ -28,7 +28,10 @@ import {
|
||||||
} from 'loot-core/src/shared/categories.js';
|
} from 'loot-core/src/shared/categories.js';
|
||||||
|
|
||||||
function BudgetSummary({ month, onClose }) {
|
function BudgetSummary({ month, onClose }) {
|
||||||
const prevMonthName = monthUtils.format(monthUtils.prevMonth(month), 'MMM');
|
const prevMonthName = monthUtils.nonLocalizedFormat(
|
||||||
|
monthUtils.prevMonth(month),
|
||||||
|
'MMM'
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NamespaceContext.Provider value={monthUtils.sheetForMonth(month)}>
|
<NamespaceContext.Provider value={monthUtils.sheetForMonth(month)}>
|
||||||
|
|
|
@ -15153,6 +15153,8 @@ jest-snapshot@test:
|
||||||
fast-check: 2.13.0
|
fast-check: 2.13.0
|
||||||
fast-glob: ^2.2.0
|
fast-glob: ^2.2.0
|
||||||
google-protobuf: ^3.12.0-rc.1
|
google-protobuf: ^3.12.0-rc.1
|
||||||
|
i18next: ^21.9.1
|
||||||
|
i18next-parser: ^6.5.0
|
||||||
jest: ^28.1.0
|
jest: ^28.1.0
|
||||||
jsverify: ^0.8.4
|
jsverify: ^0.8.4
|
||||||
lru-cache: ^5.1.1
|
lru-cache: ^5.1.1
|
||||||
|
|
Loading…
Reference in a new issue