mirror of
https://github.com/Smile-QWQ/SubTracker.git
synced 2026-06-04 00:01:36 +08:00
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:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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:30;On 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'])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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])
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user