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:
Jed Fox 2022-09-08 09:37:45 -04:00 committed by GitHub
parent 6fb497dec5
commit 5217835c55
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 376 additions and 169 deletions

View file

@ -15,4 +15,4 @@ jobs:
- name: Set up environment
uses: ./.github/actions/setup
- 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

View file

@ -2,6 +2,7 @@ module.exports = {
input: ['src/**/*.js'],
output: 'src/locales/$LOCALE.json',
locales: ['en-GB', 'es-ES'],
defaultNamespace: 'web',
sort: true,
// Force usage of JsxLexer for .js files as otherwise we can't pick up <Trans> components.
lexers: {

View file

@ -327,7 +327,7 @@ class Budget extends React.PureComponent {
pathname: '/accounts',
state: {
goBack: true,
filterName: `${categoryName} (${monthUtils.format(
filterName: `${categoryName} (${monthUtils.nonLocalizedFormat(
month,
'MMMM yyyy'
)})`,

View file

@ -259,7 +259,7 @@ function ScheduleDescription({ id }) {
</Text>
<Text style={{ margin: '0 5px' }}> </Text>
<Text style={{ flexShrink: 0 }}>
Next: {monthUtils.format(schedule.next_date, dateFormat)}
Next: {monthUtils.nonLocalizedFormat(schedule.next_date, dateFormat)}
</Text>
</View>
<StatusBadge status={status} />

View file

@ -1,4 +1,5 @@
import React, { useState, useEffect, useRef, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { format as formatDate, parseISO } from 'date-fns';
@ -52,6 +53,7 @@ export function Value({
data: dataProp,
describe = x => x.name
}) {
const { i18n } = useTranslation();
let { data, dateFormat } = useSelector(state => {
let data;
if (dataProp) {
@ -95,7 +97,7 @@ export function Value({
} else if (field === 'date') {
if (value) {
if (value.frequency) {
return getRecurringDescription(value);
return getRecurringDescription(value, i18n);
}
return formatDate(parseISO(value), dateFormat);
}

View file

@ -53,7 +53,7 @@ function CashFlow() {
.rangeInclusive(earliestMonth, monthUtils.currentMonth())
.map(month => ({
name: month,
pretty: monthUtils.format(month, 'MMMM, yyyy')
pretty: monthUtils.nonLocalizedFormat(month, 'MMMM, yyyy')
}))
.reverse();

View file

@ -51,7 +51,7 @@ function NetWorth({ accounts }) {
.rangeInclusive(earliestMonth, monthUtils.currentMonth())
.map(month => ({
name: month,
pretty: monthUtils.format(month, 'MMMM, yyyy')
pretty: monthUtils.nonLocalizedFormat(month, 'MMMM, yyyy')
}))
.reverse();

View file

@ -36,12 +36,12 @@ let ROW_HEIGHT = 43;
function DiscoverSchedulesTable({ schedules, loading }) {
let selectedItems = useSelectedItems();
let dispatchSelected = useSelectedDispatch();
const { t } = useTranslation();
let { t, i18n } = useTranslation();
function renderItem({ item }) {
let selected = selectedItems.has(item.id);
let amountOp = item._conditions.find(c => c.field === 'amount').op;
let recurDescription = getRecurringDescription(item.date);
let recurDescription = getRecurringDescription(item.date, i18n);
return (
<Row

View file

@ -549,7 +549,9 @@ export default function ScheduleDetails() {
style={{ marginTop: 10, color: colors.n4 }}
>
{state.upcomingDates.map(date => (
<View>{monthUtils.format(date, `${dateFormat} EEEE`)}</View>
<View>
{monthUtils.nonLocalizedFormat(date, `${dateFormat} EEEE`)}
</View>
))}
</Stack>
</View>

View file

@ -181,7 +181,7 @@ export function SchedulesTable({
</Field>
<Field width={110}>
{item.next_date
? monthUtils.format(item.next_date, dateFormat)
? monthUtils.nonLocalizedFormat(item.next_date, dateFormat)
: null}
</Field>
<Field width={120} style={{ alignItems: 'flex-start' }}>

View file

@ -16,7 +16,7 @@ import Navigation from './Navigation';
function Overspending({ navigationProps, stepTwo }) {
let currentMonth = monthUtils.currentMonth();
let sheetName = monthUtils.sheetForMonth(currentMonth);
let month = monthUtils.format(currentMonth, 'MMM');
let month = monthUtils.nonLocalizedFormat(currentMonth, 'MMM');
let [minimized, toggle] = useMinimized();
return (

View file

@ -2,15 +2,20 @@ import { initReactI18next } from 'react-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 esES from './es-ES.json';
const resources = {
en: {
translation: enUK
web: enUK,
core: enUKCore
},
es: {
translation: esES
web: esES,
core: esESCore
}
};
@ -18,6 +23,7 @@ i18n
.use(initReactI18next) // passes i18n down to react-i18next
.init({
resources,
defaultNS: 'web',
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
// if you're using a language detector, do not define the lng option

View 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']
}
};

View file

@ -12,7 +12,8 @@
"lint": "eslint src",
"test": "npm-run-all -cp 'test:*'",
"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": "",
"license": "ISC",
@ -56,6 +57,8 @@
"fake-indexeddb": "^3.1.3",
"fast-check": "2.13.0",
"fast-glob": "^2.2.0",
"i18next": "^21.9.1",
"i18next-parser": "^6.5.0",
"jest": "^28.1.0",
"jsverify": "^0.8.4",
"lru-cache": "^5.1.1",

View 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}}"
}
}
}

View 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}}"
}
}
}

View file

@ -210,11 +210,11 @@ export function sheetForMonth(month) {
return 'budget' + month.replace('-', '');
}
export function nameForMonth(month) {
return d.format(_parse(month), "MMMM 'yy");
export function format(month, opts, locale) {
return Intl.DateTimeFormat(locale, opts).format(_parse(month));
}
export function format(month, str) {
export function nonLocalizedFormat(month, str) {
return d.format(_parse(month), str);
}

View file

@ -42,39 +42,49 @@ export function getHasTransactionsQuery(schedules) {
.select(['schedule', 'date']);
}
function makeNumberSuffix(num) {
// 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) {
function prettyDayName(day, locale) {
let days = {
SU: 'Sunday',
MO: 'Monday',
TU: 'Tuesday',
WE: 'Wednesday',
TH: 'Thursday',
FR: 'Friday',
SA: 'Saturday'
SU: new Date('2020-01-05T12:00:00.000Z'),
MO: new Date('2020-01-06T12:00:00.000Z'),
TU: new Date('2020-01-07T12:00:00.000Z'),
WE: new Date('2020-01-08T12:00:00.000Z'),
TH: new Date('2020-01-09T12:00:00.000Z'),
FR: new Date('2020-01-10T12:00:00.000Z'),
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;
switch (config.frequency) {
case 'weekly': {
let desc = 'Every ';
desc += interval !== 1 ? `${interval} weeks` : 'week';
desc += ' on ' + monthUtils.format(config.start, 'EEEE');
return desc;
return i18n.t('schedules.recurring.weekly', {
count: interval,
day: monthUtils.format(
config.start,
{ weekday: 'long' },
i18n.resolvedLanguage
),
ns: 'core'
});
}
case 'monthly': {
let desc = 'Every ';
desc += interval !== 1 ? `${interval} months` : 'month';
if (config.patterns && config.patterns.length > 0) {
// Sort the days ascending. We filter out -1 because that
// 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
patterns = patterns.concat(config.patterns.filter(p => p.value === -1));
desc += ' on the ';
let strs = [];
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) {
if (pattern.type === 'day') {
if (pattern.value === -1) {
strs.push('last day');
strs.push(
i18n.t('schedules.recurring.pattern.lastDay', {
ns: 'core'
})
);
} else {
// Example: 15th day
strs.push(makeNumberSuffix(pattern.value));
strs.push(
i18n.t('general.ordinal', {
count: pattern.value,
ordinal: true,
ns: 'core'
})
);
}
} else {
let dayName = isSameDay ? '' : ' ' + prettyDayName(pattern.type);
let dayName = prettyDayName(
pattern.type,
i18n.resolvedLanguage,
i18n.resolvedLanguage
);
if (pattern.value === -1) {
// 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 {
// 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) {
desc += strs.slice(0, strs.length - 1).join(', ');
desc += ', and ';
desc += strs[strs.length - 1];
return i18n.t('schedules.recurring.monthlyPattern', {
context,
count: interval,
day: prettyDayName(patterns[0].type, i18n.resolvedLanguage),
pattern: new Intl.ListFormat(i18n.resolvedLanguage, {
style: 'long',
type: 'conjunction'
}).format(strs),
ns: 'core'
});
} 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': {
let desc = 'Every ';
desc += interval !== 1 ? `${interval} years` : 'year';
desc += ' on ' + monthUtils.format(config.start, 'LLL do');
return desc;
return i18n.t(
'schedules.recurring.yearly',
{
count: interval,
day: formatMonthAndDay(config.start, i18n),
ns: 'core'
},
i18n.resolvedLanguage
);
}
default:
return 'Recurring error';

View file

@ -1,7 +1,18 @@
import i18n from 'i18next';
import MockDate from 'mockdate';
import enUKCore from '../locales/en-GB.json';
import { getRecurringDescription } from './schedules';
i18n.init({
lng: 'en',
resources: {
en: {
core: enUKCore
}
}
});
describe('recurring date description', () => {
beforeEach(() => {
MockDate.set(new Date(2021, 4, 14));
@ -9,88 +20,119 @@ describe('recurring date description', () => {
it('describes weekly interval', () => {
expect(
getRecurringDescription({ start: '2021-05-17', frequency: 'weekly' })
getRecurringDescription(
{ start: '2021-05-17', frequency: 'weekly' },
i18n
)
).toBe('Every week on Monday');
expect(
getRecurringDescription({
getRecurringDescription(
{
start: '2021-05-17',
frequency: 'weekly',
interval: 2
})
},
i18n
)
).toBe('Every 2 weeks on Monday');
});
it('describes monthly interval', () => {
expect(
getRecurringDescription({ start: '2021-04-25', frequency: 'monthly' })
getRecurringDescription(
{ start: '2021-04-25', frequency: 'monthly' },
i18n
)
).toBe('Every month on the 25th');
expect(
getRecurringDescription({
getRecurringDescription(
{
start: '2021-04-25',
frequency: 'monthly',
interval: 2
})
},
i18n
)
).toBe('Every 2 months on the 25th');
expect(
getRecurringDescription({
getRecurringDescription(
{
start: '2021-04-25',
frequency: 'monthly',
patterns: [{ type: 'day', value: 25 }]
})
},
i18n
)
).toBe('Every month on the 25th');
expect(
getRecurringDescription({
getRecurringDescription(
{
start: '2021-04-25',
frequency: 'monthly',
interval: 2,
patterns: [{ type: 'day', value: 25 }]
})
},
i18n
)
).toBe('Every 2 months on the 25th');
// Last day should work
expect(
getRecurringDescription({
getRecurringDescription(
{
start: '2021-04-25',
frequency: 'monthly',
patterns: [{ type: 'day', value: 31 }]
})
},
i18n
)
).toBe('Every month on the 31st');
// -1 should work, representing the last day
expect(
getRecurringDescription({
getRecurringDescription(
{
start: '2021-04-25',
frequency: 'monthly',
patterns: [{ type: 'day', value: -1 }]
})
},
i18n
)
).toBe('Every month on the last day');
// Day names should work
expect(
getRecurringDescription({
getRecurringDescription(
{
start: '2021-04-25',
frequency: 'monthly',
patterns: [{ type: 'FR', value: 2 }]
})
},
i18n
)
).toBe('Every month on the 2nd Friday');
expect(
getRecurringDescription({
getRecurringDescription(
{
start: '2021-04-25',
frequency: 'monthly',
patterns: [{ type: 'FR', value: -1 }]
})
},
i18n
)
).toBe('Every month on the last Friday');
});
it('describes monthly interval with multiple days', () => {
// Note how order doesn't matter - the day should be sorted
expect(
getRecurringDescription({
getRecurringDescription(
{
start: '2021-04-25',
frequency: 'monthly',
patterns: [
@ -98,11 +140,14 @@ describe('recurring date description', () => {
{ type: 'day', value: 3 },
{ type: 'day', value: 20 }
]
})
},
i18n
)
).toBe('Every month on the 3rd, 15th, and 20th');
expect(
getRecurringDescription({
getRecurringDescription(
{
start: '2021-04-25',
frequency: 'monthly',
patterns: [
@ -110,12 +155,15 @@ describe('recurring date description', () => {
{ type: 'day', value: -1 },
{ type: 'day', value: 20 }
]
})
},
i18n
)
).toBe('Every month on the 3rd, 20th, and last day');
// Mix days and day names
expect(
getRecurringDescription({
getRecurringDescription(
{
start: '2021-04-25',
frequency: 'monthly',
patterns: [
@ -123,12 +171,15 @@ describe('recurring date description', () => {
{ type: 'day', value: -1 },
{ type: 'FR', value: 2 }
]
})
},
i18n
)
).toBe('Every month on the 2nd Friday, 3rd, and last day');
// When there is a mixture of types, day names should always come first
expect(
getRecurringDescription({
getRecurringDescription(
{
start: '2021-04-25',
frequency: 'monthly',
patterns: [
@ -137,21 +188,29 @@ describe('recurring date description', () => {
{ type: 'FR', value: 3 },
{ type: 'day', value: 10 }
]
})
},
i18n
)
).toBe('Every month on the 1st Saturday, 3rd Friday, 2nd, and 10th');
});
it('describes yearly interval', () => {
expect(
getRecurringDescription({ start: '2021-05-17', frequency: 'yearly' })
getRecurringDescription(
{ start: '2021-05-17', frequency: 'yearly' },
i18n
)
).toBe('Every year on May 17th');
expect(
getRecurringDescription({
getRecurringDescription(
{
start: '2021-05-17',
frequency: 'yearly',
interval: 2
})
},
i18n
)
).toBe('Every 2 years on May 17th');
});
});

View file

@ -1,4 +1,5 @@
import React, { useEffect, useReducer, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
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 DateSelect from './DateSelect';
const DATE_FORMAT = 'yyyy-MM-dd';
// ex: There is no 6th Friday of the Month
const MAX_DAY_OF_WEEK_INTERVAL = 5;
@ -62,7 +61,7 @@ function unparseConfig(parsed) {
function createMonthlyRecurrence(startDate) {
return {
value: parseInt(monthUtils.format(startDate, 'd')),
value: parseInt(monthUtils.nonLocalizedFormat(startDate, 'd')),
type: 'day'
};
}
@ -152,8 +151,8 @@ function SchedulePreview({ previewDates }) {
<Stack direction="row" spacing={4} style={{ marginTop: 10 }}>
{previewDates.map(d => (
<View>
<Text>{monthUtils.format(d, dateFormat)}</Text>
<Text>{monthUtils.format(d, 'EEEE')}</Text>
<Text>{monthUtils.nonLocalizedFormat(d, dateFormat)}</Text>
<Text>{monthUtils.nonLocalizedFormat(d, 'EEEE')}</Text>
</View>
))}
</Stack>
@ -370,6 +369,7 @@ export default function RecurringSchedulePicker({
onChange
}) {
let { isOpen, close, getOpenEvents } = useTooltip();
let { i18n } = useTranslation();
function onSave(config) {
onChange(config);
@ -379,7 +379,7 @@ export default function RecurringSchedulePicker({
return (
<View>
<Button {...getOpenEvents()} style={[{ textAlign: 'left' }, buttonStyle]}>
{value ? getRecurringDescription(value) : 'No recurring date'}
{value ? getRecurringDescription(value, i18n) : 'No recurring date'}
</Button>
{isOpen && (
<RecurringScheduleTooltip

View file

@ -1256,7 +1256,7 @@ export const MonthPicker = scope(lively => {
function getCurrentMonthName(startMonth, currentMonth) {
return monthUtils.getYear(startMonth) === monthUtils.getYear(currentMonth)
? monthUtils.format(currentMonth, 'MMM')
? monthUtils.nonLocalizedFormat(currentMonth, 'MMM')
: null;
}
@ -1264,7 +1264,7 @@ export const MonthPicker = scope(lively => {
const currentMonth = monthUtils.currentMonth();
const range = getRangeForYear(currentMonth);
const monthNames = range.map(month => {
return monthUtils.format(month, 'MMM');
return monthUtils.nonLocalizedFormat(month, 'MMM');
});
return {
@ -1314,7 +1314,7 @@ export const MonthPicker = scope(lively => {
flex: '0 0 40px'
}}
>
{monthUtils.format(year, 'yyyy')}
{monthUtils.nonLocalizedFormat(year, 'yyyy')}
</View>
<ElementQuery
sizes={[

View file

@ -321,7 +321,7 @@ export default React.memo(function BudgetSummary({ month }) {
currentMonth === month && { textDecoration: 'underline' }
])}
>
{monthUtils.format(month, 'MMMM')}
{monthUtils.nonLocalizedFormat(month, 'MMMM')}
</div>
<View

View file

@ -271,7 +271,10 @@ export default React.memo(function BudgetSummary({ month }) {
setMenuOpen(false);
}
let prevMonthName = monthUtils.format(monthUtils.prevMonth(month), 'MMM');
let prevMonthName = monthUtils.nonLocalizedFormat(
monthUtils.prevMonth(month),
'MMM'
);
let ExpandOrCollapseIcon = collapsed ? ArrowButtonDown1 : ArrowButtonUp1;
@ -337,7 +340,7 @@ export default React.memo(function BudgetSummary({ month }) {
currentMonth === month && { textDecoration: 'underline' }
])}
>
{monthUtils.format(month, 'MMMM')}
{monthUtils.nonLocalizedFormat(month, 'MMMM')}
</div>
<View

View file

@ -1092,7 +1092,7 @@ export function BudgetHeader({
}
]}
>
{monthUtils.format(currentMonth, "MMMM ''yy")}
{monthUtils.nonLocalizedFormat(currentMonth, "MMMM ''yy")}
</Text>
{editMode ? (
<Button

View file

@ -692,7 +692,7 @@ export function DateHeader({ date }) {
}}
>
<Text style={[styles.text, { fontSize: 13, color: colors.n4 }]}>
{monthUtils.format(date, 'MMMM dd, yyyy')}
{monthUtils.nonLocalizedFormat(date, 'MMMM dd, yyyy')}
</Text>
</ListItem>
);

View file

@ -4,7 +4,7 @@ import { connect } from 'react-redux';
import * as d from 'date-fns';
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 {
amountToCurrency,
amountToInteger,

View file

@ -28,7 +28,10 @@ import {
} from 'loot-core/src/shared/categories.js';
function BudgetSummary({ month, onClose }) {
const prevMonthName = monthUtils.format(monthUtils.prevMonth(month), 'MMM');
const prevMonthName = monthUtils.nonLocalizedFormat(
monthUtils.prevMonth(month),
'MMM'
);
return (
<NamespaceContext.Provider value={monthUtils.sheetForMonth(month)}>

View file

@ -15153,6 +15153,8 @@ jest-snapshot@test:
fast-check: 2.13.0
fast-glob: ^2.2.0
google-protobuf: ^3.12.0-rc.1
i18next: ^21.9.1
i18next-parser: ^6.5.0
jest: ^28.1.0
jsverify: ^0.8.4
lru-cache: ^5.1.1