fix: finish web locale utilities and ignore local scratch dirs

- remove the last frontend locale branches from reminder, currency and timezone helpers and keep the rendered copy on shared messages only

- align settings and statistics web tests with the new locale-aware helper behavior and reminder preview wording

- ignore .tmp and docs workspace scratch directories so the i18n branch stays clean while local analysis notes remain untracked
This commit is contained in:
SmileQWQ
2026-05-17 22:45:16 +08:00
parent c726df08d3
commit afe7fcdeb7
9 changed files with 90 additions and 41 deletions

2
.gitignore vendored
View File

@@ -5,6 +5,8 @@ coverage
.env
.env.local
*.log
/.tmp/
/docs/
apps/api/prisma/dev.db
apps/api/prisma/dev.db-journal
apps/api/prisma/test.db

View File

@@ -568,7 +568,7 @@
/>
</div>
</template>
<span>需先启用至少一个直达通知渠道</span>
<span>{{ t('settings.helps.forgotPasswordChannelRequired') }}</span>
</n-tooltip>
<n-switch
v-else
@@ -1400,10 +1400,10 @@ async function handleForgotPasswordToggleChange(value: boolean) {
forgotPasswordEnabled: value
})
applySavedSettings(result)
message.success(value ? '找回密码已开启' : '找回密码已关闭')
message.success(value ? t('settings.messages.forgotPasswordEnabled') : t('settings.messages.forgotPasswordDisabled'))
} catch (error) {
settingsForm.forgotPasswordEnabled = previousValue
message.error(error instanceof Error ? error.message : '找回密码设置保存失败')
message.error(error instanceof Error ? error.message : t('settings.messages.forgotPasswordSaveFailed'))
} finally {
savingForgotPasswordToggle.value = false
}

View File

@@ -3,10 +3,13 @@ import { getAppLocale } from '@/locales'
export function getCurrencyLabel(code: string) {
const upper = code.toUpperCase()
if (getAppLocale() === 'en-US' && typeof Intl !== 'undefined' && typeof Intl.DisplayNames === 'function') {
const displayNames = new Intl.DisplayNames(['en-US'], { type: 'currency' })
const englishName = displayNames.of(upper)
return englishName ? `${englishName} (${upper})` : upper
const locale = getAppLocale()
if (typeof Intl !== 'undefined' && typeof Intl.DisplayNames === 'function') {
const displayNames = new Intl.DisplayNames([locale], { type: 'currency' })
const localizedName = displayNames.of(upper)
if (localizedName && localizedName !== upper) {
return `${localizedName} (${upper})`
}
}
const name = (currencyNameMap as Record<string, string>)[upper]

View File

@@ -1,3 +1,5 @@
import { DEFAULT_APP_LOCALE, getMessage, type AppLocale } from '@subtracker/shared'
export type ReminderRulesKind = 'advance' | 'overdue'
export type ReminderRuleEntry = {
@@ -51,31 +53,31 @@ function formatI18n(template: string, params: Record<string, string | number>) {
return template.replace(/\{(\w+)\}/g, (_match, key) => String(params[key] ?? `{${key}}`))
}
function getDefaultI18n(): ReminderRulesI18n {
export function getDefaultReminderRulesI18n(locale: AppLocale = DEFAULT_APP_LOCALE): ReminderRulesI18n {
return {
fallback: '沿用系统默认',
emptyTitle: '请先输入规则后再演算',
resultTitle: '演算结果',
invalidTitle: '规则格式有误',
defaultRulesLabel: '系统默认规则',
defaultAdvanceRulesLabel: '系统默认到期前规则',
defaultOverdueRulesLabel: '系统默认过期规则',
fallbackPreviewTitle: '当前未填写,以下按{label}演算',
fallbackInvalidTitle: '{label}格式有误',
noAdvance: '暂无到期前提醒规则',
noOverdue: '暂无过期提醒规则',
parseFailed: '规则解析失败',
invalidSegmentFormat: '规则 "{segment}" 格式无效,应为 天数&HH:mm',
invalidDaysInteger: '规则 "{segment}" 中的天数必须为整数',
invalidOverdueDays: '规则 "{segment}" 中的天数必须大于等于 1',
invalidAdvanceDays: '规则 "{segment}" 中的天数不能小于 0',
invalidTime: '规则 "{segment}" 中的时间必须为 HH:mm',
inlineAdvanceSameDay: '当天 {time}',
inlineAdvanceBefore: '提前 {days} 天 {time}',
inlineOverdue: '过期 {days} 天 {time}',
evalAdvanceSameDay: '到期当天 {time} 提醒',
evalAdvanceBefore: '提前 {days} 天 {time} 提醒',
evalOverdue: '过期 {days} 天 {time} 提醒'
fallback: getMessage(locale, 'validation.reminderRules.fallback'),
emptyTitle: getMessage(locale, 'validation.reminderRules.emptyTitle'),
resultTitle: getMessage(locale, 'validation.reminderRules.resultTitle'),
invalidTitle: getMessage(locale, 'validation.reminderRules.invalidTitle'),
defaultRulesLabel: getMessage(locale, 'validation.reminderRules.defaultRulesLabel'),
defaultAdvanceRulesLabel: getMessage(locale, 'validation.reminderRules.defaultAdvanceRulesLabel'),
defaultOverdueRulesLabel: getMessage(locale, 'validation.reminderRules.defaultOverdueRulesLabel'),
fallbackPreviewTitle: getMessage(locale, 'validation.reminderRules.fallbackPreviewTitle', { label: '{label}' }),
fallbackInvalidTitle: getMessage(locale, 'validation.reminderRules.fallbackInvalidTitle', { label: '{label}' }),
noAdvance: getMessage(locale, 'validation.reminderRules.noAdvance'),
noOverdue: getMessage(locale, 'validation.reminderRules.noOverdue'),
parseFailed: getMessage(locale, 'validation.reminderRules.parseFailed'),
invalidSegmentFormat: getMessage(locale, 'validation.reminderRules.invalidSegmentFormat', { segment: '{segment}' }),
invalidDaysInteger: getMessage(locale, 'validation.reminderRules.invalidDaysInteger', { segment: '{segment}' }),
invalidOverdueDays: getMessage(locale, 'validation.reminderRules.invalidOverdueDays', { segment: '{segment}' }),
invalidAdvanceDays: getMessage(locale, 'validation.reminderRules.invalidAdvanceDays', { segment: '{segment}' }),
invalidTime: getMessage(locale, 'validation.reminderRules.invalidTime', { segment: '{segment}' }),
inlineAdvanceSameDay: getMessage(locale, 'validation.reminderRules.inlineAdvanceSameDay', { time: '{time}' }),
inlineAdvanceBefore: getMessage(locale, 'validation.reminderRules.inlineAdvanceBefore', { days: '{days}', time: '{time}' }),
inlineOverdue: getMessage(locale, 'validation.reminderRules.inlineOverdue', { days: '{days}', time: '{time}' }),
evalAdvanceSameDay: getMessage(locale, 'validation.reminderRules.evalAdvanceSameDay', { time: '{time}' }),
evalAdvanceBefore: getMessage(locale, 'validation.reminderRules.evalAdvanceBefore', { days: '{days}', time: '{time}' }),
evalOverdue: getMessage(locale, 'validation.reminderRules.evalOverdue', { days: '{days}', time: '{time}' })
}
}
@@ -172,20 +174,21 @@ function toEvaluationDescription(rule: ParsedReminderRule, kind: ReminderRulesKi
export function formatReminderRulesText(
value: string | null | undefined,
kind: ReminderRulesKind,
fallback = getDefaultI18n().fallback,
fallback?: string,
options?: {
i18n?: Partial<ReminderRulesI18n>
}
) {
const copy = { ...getDefaultI18n(), ...options?.i18n }
if (!value?.trim()) return fallback
const copy = { ...getDefaultReminderRulesI18n(), ...options?.i18n }
const fallbackText = fallback ?? copy.fallback
if (!value?.trim()) return fallbackText
try {
const parts = parseReminderRulesStrict(value, kind, copy)
if (!parts.length) return fallback
if (!parts.length) return fallbackText
return parts.map((item) => toInlineDescription(item, kind, copy)).join('')
} catch {
return fallback
return fallbackText
}
}
@@ -197,7 +200,7 @@ export function listReminderRuleDescriptions(
i18n?: Partial<ReminderRulesI18n>
}
) {
const copy = { ...getDefaultI18n(), ...options?.i18n }
const copy = { ...getDefaultReminderRulesI18n(), ...options?.i18n }
const currentValue = value?.trim() ?? ''
try {
@@ -223,7 +226,7 @@ export function evaluateReminderRules(
i18n?: Partial<ReminderRulesI18n>
}
): ReminderRulesEvaluation {
const copy = { ...getDefaultI18n(), ...options?.i18n }
const copy = { ...getDefaultReminderRulesI18n(), ...options?.i18n }
const fallbackLabel =
options?.fallbackLabel ??
(kind === 'advance' ? copy.defaultAdvanceRulesLabel : kind === 'overdue' ? copy.defaultOverdueRulesLabel : copy.defaultRulesLabel)

View File

@@ -1,3 +1,5 @@
import { getMessage } from '@subtracker/shared'
import { getAppLocale } from '@/locales'
import type { StatisticsOverview } from '@/types/api'
interface TopSubscriptionsThemeTokens {
@@ -15,6 +17,7 @@ export function buildTopSubscriptionsOption(
) {
const data = (items ?? []).slice(0, 10)
if (!data.length) return null
const locale = getAppLocale()
return {
tooltip: {
@@ -29,7 +32,7 @@ export function buildTopSubscriptionsOption(
grid: { left: 160, right: 24, top: 20, bottom: 20 },
xAxis: {
type: 'value',
name: `金额(${baseCurrency})`,
name: getMessage(locale, 'statistics.labels.amountAxis', { currency: baseCurrency }),
nameTextStyle: {
color: theme?.secondaryTextColor
},

View File

@@ -2,6 +2,7 @@ import dayjs from 'dayjs'
import utc from 'dayjs/plugin/utc.js'
import timezone from 'dayjs/plugin/timezone.js'
import customParseFormat from 'dayjs/plugin/customParseFormat.js'
import { getMessage } from '@subtracker/shared'
import { getAppLocale } from '@/locales'
dayjs.extend(utc)
@@ -154,7 +155,7 @@ export function addIntervalToPickerTs(
export function formatMonthLabelInTimezone(value: Date | string | number, timezoneValue = DEFAULT_APP_TIMEZONE) {
const locale = getAppLocale()
const date = toTimezonedDayjs(typeof value === 'number' ? new Date(value) : value, timezoneValue)
return locale === 'en-US' ? date.format('MMMM YYYY') : date.format('YYYY 年 M 月')
return date.locale(locale).format(getMessage(locale, 'formatting.monthLabel.long'))
}
export function addIntervalToBusinessDateString(

View File

@@ -76,7 +76,7 @@ describe('settings import export section', () => {
expect(source).toContain('forgotPasswordToggleUnlocked')
expect(source).toContain('savingForgotPasswordToggle')
expect(source).toContain('handleForgotPasswordToggleChange')
expect(source).toContain('需先启用至少一个直达通知渠道')
expect(source).toContain("t('settings.helps.forgotPasswordChannelRequired')")
expect(source).toContain("@update:value=\"handleForgotPasswordToggleChange\"")
expect(source).toContain('switch-group switch-group--single')
expect(source).toContain('switch-inline-label')

View File

@@ -76,4 +76,40 @@ describe('reminder rules helpers', () => {
{ key: '2&09:30', description: '过期 2 天 09:30' }
])
})
it('supports localized reminder rule copy overrides', () => {
const copy = {
fallback: 'Use the system default',
resultTitle: 'Preview result',
invalidTitle: 'Invalid rule format',
defaultRulesLabel: 'system default rules',
defaultAdvanceRulesLabel: 'system default pre-renewal rules',
defaultOverdueRulesLabel: 'system default overdue rules',
fallbackPreviewTitle: 'No value entered. Previewing with {label}',
fallbackInvalidTitle: '{label} is invalid',
noAdvance: 'No pre-renewal reminder rules',
noOverdue: 'No overdue reminder rules',
parseFailed: 'Failed to parse rules',
invalidSegmentFormat: 'Rule "{segment}" is invalid. Expected format: days&HH:mm',
invalidDaysInteger: 'The days value in rule "{segment}" must be an integer',
invalidOverdueDays: 'The days value in rule "{segment}" must be greater than or equal to 1',
invalidAdvanceDays: 'The days value in rule "{segment}" cannot be less than 0',
invalidTime: 'The time value in rule "{segment}" must use HH:mm',
inlineAdvanceSameDay: 'On the due date at {time}',
inlineAdvanceBefore: '{days} day(s) before at {time}',
inlineOverdue: 'On overdue day {days} at {time}',
evalAdvanceSameDay: 'Remind on the due date at {time}',
evalAdvanceBefore: 'Remind {days} day(s) before at {time}',
evalOverdue: 'Remind on overdue day {days} at {time}'
}
expect(formatReminderRulesText('', 'advance', undefined, { i18n: copy })).toBe('Use the system default')
expect(formatReminderRulesText('3&09:30;0&09:30;', 'advance', undefined, { i18n: copy })).toBe(
'3 day(s) before at 09:30On the due date at 09:30'
)
const result = evaluateReminderRules('1&09:30;', 'overdue', { i18n: copy })
expect(result.title).toBe('Preview result')
expect(result.entries.map((item) => item.description)).toEqual(['Remind on overdue day 1 at 09:30'])
})
})

View File

@@ -23,6 +23,7 @@ describe('buildTopSubscriptionsOption', () => {
expect(option?.yAxis.data).toEqual(['Netflix'])
expect(option?.yAxis.inverse).toBe(true)
expect(option?.xAxis.name).toBe('金额CNY')
expect(option?.series[0].data).toEqual([88])
})