refactor(i18n): use JSON with flattened key and namespace (#30114)

Co-authored-by: yyh <yuanyouhuilyz@gmail.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Stephen Zhou
2025-12-29 14:52:32 +08:00
committed by GitHub
parent 09be869f58
commit 6d0e36479b
2552 changed files with 111159 additions and 142972 deletions

View File

@ -18,7 +18,12 @@ vi.mock('react-i18next', async (importOriginal) => {
return {
...actual,
useTranslation: () => ({
t: (key: string) => mockTranslations[key] ?? key,
t: (key: string, options?: { ns?: string }) => {
if (mockTranslations[key])
return mockTranslations[key]
const prefix = options?.ns ? `${options.ns}.` : ''
return `${prefix}${key}`
},
}),
}
})

View File

@ -22,8 +22,8 @@ const Footer = ({
<div className={cn('flex max-w-[1680px] grow border-x border-divider-accent p-6', currentCategory === CategoryEnum.CLOUD ? 'justify-between' : 'justify-end')}>
{currentCategory === CategoryEnum.CLOUD && (
<div className="flex flex-col text-text-tertiary">
<span className="system-xs-regular">{t('billing.plansCommon.taxTip')}</span>
<span className="system-xs-regular">{t('billing.plansCommon.taxTipSecond')}</span>
<span className="system-xs-regular">{t('plansCommon.taxTip', { ns: 'billing' })}</span>
<span className="system-xs-regular">{t('plansCommon.taxTipSecond', { ns: 'billing' })}</span>
</div>
)}
<span className="flex h-fit items-center gap-x-1 text-saas-dify-blue-accessible">
@ -32,7 +32,7 @@ const Footer = ({
className="system-md-regular"
target="_blank"
>
{t('billing.plansCommon.comparePlanAndFeatures')}
{t('plansCommon.comparePlanAndFeatures', { ns: 'billing' })}
</Link>
<RiArrowRightUpLine className="size-4" />
</span>

View File

@ -9,7 +9,12 @@ vi.mock('react-i18next', async (importOriginal) => {
return {
...actual,
useTranslation: () => ({
t: (key: string) => mockTranslations[key] ?? key,
t: (key: string, options?: { ns?: string }) => {
if (mockTranslations[key])
return mockTranslations[key]
const prefix = options?.ns ? `${options.ns}.` : ''
return `${prefix}${key}`
},
}),
}
})

View File

@ -21,11 +21,11 @@ const Header = ({
<DifyLogo className="h-[27px] w-[60px]" />
</div>
<span className="bg-billing-plan-title-bg bg-clip-text px-1.5 font-instrument text-[37px] italic leading-[1.2] text-transparent">
{t('billing.plansCommon.title.plans')}
{t('plansCommon.title.plans', { ns: 'billing' })}
</span>
</div>
<p className="system-sm-regular text-text-tertiary">
{t('billing.plansCommon.title.description')}
{t('plansCommon.title.description', { ns: 'billing' })}
</p>
<Button
variant="secondary"

View File

@ -41,10 +41,13 @@ vi.mock('react-i18next', async (importOriginal) => {
return {
...actual,
useTranslation: () => ({
t: (key: string, options?: { returnObjects?: boolean }) => {
t: (key: string, options?: { returnObjects?: boolean, ns?: string }) => {
if (options?.returnObjects)
return mockTranslations[key] ?? []
return mockTranslations[key] ?? key
if (mockTranslations[key])
return mockTranslations[key]
const prefix = options?.ns ? `${options.ns}.` : ''
return `${prefix}${key}`
},
}),
Trans: ({ i18nKey }: { i18nKey: string }) => <span>{i18nKey}</span>,

View File

@ -11,7 +11,12 @@ vi.mock('react-i18next', async (importOriginal) => {
return {
...actual,
useTranslation: () => ({
t: (key: string) => mockTranslations[key] ?? key,
t: (key: string, options?: { ns?: string }) => {
if (key in mockTranslations)
return mockTranslations[key]
const prefix = options?.ns ? `${options.ns}.` : ''
return `${prefix}${key}`
},
}),
}
})
@ -85,8 +90,8 @@ describe('PlanSwitcher', () => {
it('should render tabs when translation strings are empty', () => {
// Arrange
mockTranslations = {
'billing.plansCommon.cloud': '',
'billing.plansCommon.self': '',
'plansCommon.cloud': '',
'plansCommon.self': '',
}
// Act

View File

@ -27,12 +27,12 @@ const PlanSwitcher: FC<PlanSwitcherProps> = ({
const tabs = {
cloud: {
value: 'cloud' as Category,
label: t('billing.plansCommon.cloud'),
label: t('plansCommon.cloud', { ns: 'billing' }),
Icon: Cloud,
},
self: {
value: 'self' as Category,
label: t('billing.plansCommon.self'),
label: t('plansCommon.self', { ns: 'billing' }),
Icon: SelfHosted,
},
}

View File

@ -9,7 +9,12 @@ vi.mock('react-i18next', async (importOriginal) => {
return {
...actual,
useTranslation: () => ({
t: (key: string) => mockTranslations[key] ?? key,
t: (key: string, options?: { ns?: string }) => {
if (mockTranslations[key])
return mockTranslations[key]
const prefix = options?.ns ? `${options.ns}.` : ''
return `${prefix}${key}`
},
}),
}
})

View File

@ -30,7 +30,7 @@ const PlanRangeSwitcher: FC<PlanRangeSwitcherProps> = ({
}}
/>
<span className="system-md-regular text-text-tertiary">
{t('billing.plansCommon.annualBilling', { percent: 17 })}
{t('plansCommon.annualBilling', { ns: 'billing', percent: 17 })}
</span>
</div>
)

View File

@ -35,7 +35,7 @@ const CloudPlanItem: FC<CloudPlanItemProps> = ({
}) => {
const { t } = useTranslation()
const [loading, setLoading] = React.useState(false)
const i18nPrefix = `billing.plans.${plan}`
const i18nPrefix = `plans.${plan}` as const
const isFreePlan = plan === Plan.sandbox
const isMostPopularPlan = plan === Plan.professional
const planInfo = ALL_PLANS[plan]
@ -48,12 +48,12 @@ const CloudPlanItem: FC<CloudPlanItemProps> = ({
const btnText = useMemo(() => {
if (isCurrent)
return t('billing.plansCommon.currentPlan')
return t('plansCommon.currentPlan', { ns: 'billing' })
return ({
[Plan.sandbox]: t('billing.plansCommon.startForFree'),
[Plan.professional]: t('billing.plansCommon.startBuilding'),
[Plan.team]: t('billing.plansCommon.getStarted'),
[Plan.sandbox]: t('plansCommon.startForFree', { ns: 'billing' }),
[Plan.professional]: t('plansCommon.startBuilding', { ns: 'billing' }),
[Plan.team]: t('plansCommon.getStarted', { ns: 'billing' }),
})[plan]
}, [isCurrent, plan, t])
@ -67,7 +67,7 @@ const CloudPlanItem: FC<CloudPlanItemProps> = ({
if (!isCurrentWorkspaceManager) {
Toast.notify({
type: 'error',
message: t('billing.buyPermissionDeniedTip'),
message: t('buyPermissionDeniedTip', { ns: 'billing' }),
className: 'z-[1001]',
})
return
@ -106,24 +106,24 @@ const CloudPlanItem: FC<CloudPlanItemProps> = ({
{ICON_MAP[plan]}
<div className="flex min-h-[104px] flex-col gap-y-2">
<div className="flex items-center gap-x-2.5">
<div className="text-[30px] font-medium leading-[1.2] text-text-primary">{t(`${i18nPrefix}.name` as any) as string}</div>
<div className="text-[30px] font-medium leading-[1.2] text-text-primary">{t(`${i18nPrefix}.name`, { ns: 'billing' })}</div>
{
isMostPopularPlan && (
<div className="flex items-center justify-center bg-saas-dify-blue-static px-1.5 py-1">
<span className="system-2xs-semibold-uppercase text-text-primary-on-surface">
{t('billing.plansCommon.mostPopular')}
{t('plansCommon.mostPopular', { ns: 'billing' })}
</span>
</div>
)
}
</div>
<div className="system-sm-regular text-text-secondary">{t(`${i18nPrefix}.description` as any) as string}</div>
<div className="system-sm-regular text-text-secondary">{t(`${i18nPrefix}.description`, { ns: 'billing' })}</div>
</div>
</div>
{/* Price */}
<div className="flex items-end gap-x-2 px-1 pb-8 pt-4">
{isFreePlan && (
<span className="title-4xl-semi-bold text-text-primary">{t('billing.plansCommon.free')}</span>
<span className="title-4xl-semi-bold text-text-primary">{t('plansCommon.free', { ns: 'billing' })}</span>
)}
{!isFreePlan && (
<>
@ -138,8 +138,8 @@ const CloudPlanItem: FC<CloudPlanItemProps> = ({
{isYear ? planInfo.price * 10 : planInfo.price}
</span>
<span className="system-md-regular pb-0.5 text-text-tertiary">
{t('billing.plansCommon.priceTip')}
{t(`billing.plansCommon.${!isYear ? 'month' : 'year'}`)}
{t('plansCommon.priceTip', { ns: 'billing' })}
{t(`plansCommon.${!isYear ? 'month' : 'year'}`, { ns: 'billing' })}
</span>
</>
)}

View File

@ -21,82 +21,82 @@ const List = ({
<div className="flex flex-col gap-y-2.5 p-6">
<Item
label={isFreePlan
? t('billing.plansCommon.messageRequest.title', { count: planInfo.messageRequest })
: t('billing.plansCommon.messageRequest.titlePerMonth', { count: planInfo.messageRequest })}
tooltip={t('billing.plansCommon.messageRequest.tooltip') as string}
? t('plansCommon.messageRequest.title', { ns: 'billing', count: planInfo.messageRequest })
: t('plansCommon.messageRequest.titlePerMonth', { ns: 'billing', count: planInfo.messageRequest })}
tooltip={t('plansCommon.messageRequest.tooltip', { ns: 'billing' }) as string}
/>
<Item
label={t('billing.plansCommon.teamWorkspace', { count: planInfo.teamWorkspace })}
label={t('plansCommon.teamWorkspace', { ns: 'billing', count: planInfo.teamWorkspace })}
/>
<Item
label={t('billing.plansCommon.teamMember', { count: planInfo.teamMembers })}
label={t('plansCommon.teamMember', { ns: 'billing', count: planInfo.teamMembers })}
/>
<Item
label={t('billing.plansCommon.buildApps', { count: planInfo.buildApps })}
label={t('plansCommon.buildApps', { ns: 'billing', count: planInfo.buildApps })}
/>
<Divider bgStyle="gradient" />
<Item
label={t('billing.plansCommon.documents', { count: planInfo.documents })}
tooltip={t('billing.plansCommon.documentsTooltip') as string}
label={t('plansCommon.documents', { ns: 'billing', count: planInfo.documents })}
tooltip={t('plansCommon.documentsTooltip', { ns: 'billing' }) as string}
/>
<Item
label={t('billing.plansCommon.vectorSpace', { size: planInfo.vectorSpace })}
tooltip={t('billing.plansCommon.vectorSpaceTooltip') as string}
label={t('plansCommon.vectorSpace', { ns: 'billing', size: planInfo.vectorSpace })}
tooltip={t('plansCommon.vectorSpaceTooltip', { ns: 'billing' }) as string}
/>
<Item
label={t('billing.plansCommon.documentsRequestQuota', { count: planInfo.documentsRequestQuota })}
tooltip={t('billing.plansCommon.documentsRequestQuotaTooltip')}
label={t('plansCommon.documentsRequestQuota', { ns: 'billing', count: planInfo.documentsRequestQuota })}
tooltip={t('plansCommon.documentsRequestQuotaTooltip', { ns: 'billing' })}
/>
<Item
label={[t(`billing.plansCommon.priority.${planInfo.documentProcessingPriority}`), t('billing.plansCommon.documentProcessingPriority')].join('')}
label={[t(`plansCommon.priority.${planInfo.documentProcessingPriority}`, { ns: 'billing' }), t('plansCommon.documentProcessingPriority', { ns: 'billing' })].join('')}
/>
<Divider bgStyle="gradient" />
<Item
label={
planInfo.triggerEvents === NUM_INFINITE
? t('billing.plansCommon.triggerEvents.unlimited')
? t('plansCommon.triggerEvents.unlimited', { ns: 'billing' })
: plan === Plan.sandbox
? t('billing.plansCommon.triggerEvents.sandbox', { count: planInfo.triggerEvents })
: t('billing.plansCommon.triggerEvents.professional', { count: planInfo.triggerEvents })
? t('plansCommon.triggerEvents.sandbox', { ns: 'billing', count: planInfo.triggerEvents })
: t('plansCommon.triggerEvents.professional', { ns: 'billing', count: planInfo.triggerEvents })
}
tooltip={t('billing.plansCommon.triggerEvents.tooltip') as string}
tooltip={t('plansCommon.triggerEvents.tooltip', { ns: 'billing' }) as string}
/>
<Item
label={
plan === Plan.sandbox
? t('billing.plansCommon.startNodes.limited', { count: 2 })
: t('billing.plansCommon.startNodes.unlimited')
? t('plansCommon.startNodes.limited', { ns: 'billing', count: 2 })
: t('plansCommon.startNodes.unlimited', { ns: 'billing' })
}
/>
<Item
label={
plan === Plan.sandbox
? t('billing.plansCommon.workflowExecution.standard')
? t('plansCommon.workflowExecution.standard', { ns: 'billing' })
: plan === Plan.professional
? t('billing.plansCommon.workflowExecution.faster')
: t('billing.plansCommon.workflowExecution.priority')
? t('plansCommon.workflowExecution.faster', { ns: 'billing' })
: t('plansCommon.workflowExecution.priority', { ns: 'billing' })
}
tooltip={t('billing.plansCommon.workflowExecution.tooltip') as string}
tooltip={t('plansCommon.workflowExecution.tooltip', { ns: 'billing' }) as string}
/>
<Divider bgStyle="gradient" />
<Item
label={t('billing.plansCommon.annotatedResponse.title', { count: planInfo.annotatedResponse })}
tooltip={t('billing.plansCommon.annotatedResponse.tooltip') as string}
label={t('plansCommon.annotatedResponse.title', { ns: 'billing', count: planInfo.annotatedResponse })}
tooltip={t('plansCommon.annotatedResponse.tooltip', { ns: 'billing' }) as string}
/>
<Item
label={t('billing.plansCommon.logsHistory', { days: planInfo.logHistory === NUM_INFINITE ? t('billing.plansCommon.unlimited') as string : `${planInfo.logHistory} ${t('billing.plansCommon.days')}` })}
label={t('plansCommon.logsHistory', { ns: 'billing', days: planInfo.logHistory === NUM_INFINITE ? t('plansCommon.unlimited', { ns: 'billing' }) as string : `${planInfo.logHistory} ${t('plansCommon.days', { ns: 'billing' })}` })}
/>
<Item
label={
planInfo.apiRateLimit === NUM_INFINITE
? t('billing.plansCommon.unlimitedApiRate')
: `${t('billing.plansCommon.apiRateLimitUnit', { count: planInfo.apiRateLimit })} ${t('billing.plansCommon.apiRateLimit')}/${t('billing.plansCommon.month')}`
? t('plansCommon.unlimitedApiRate', { ns: 'billing' })
: `${t('plansCommon.apiRateLimitUnit', { ns: 'billing', count: planInfo.apiRateLimit })} ${t('plansCommon.apiRateLimit', { ns: 'billing' })}/${t('plansCommon.month', { ns: 'billing' })}`
}
tooltip={planInfo.apiRateLimit === NUM_INFINITE ? undefined : t('billing.plansCommon.apiRateLimitTooltip') as string}
tooltip={planInfo.apiRateLimit === NUM_INFINITE ? undefined : t('plansCommon.apiRateLimitTooltip', { ns: 'billing' }) as string}
/>
<Divider bgStyle="gradient" />
<Item
label={t('billing.plansCommon.modelProviders')}
label={t('plansCommon.modelProviders', { ns: 'billing' })}
/>
</div>
)

View File

@ -25,7 +25,7 @@ const Button = ({
}: ButtonProps) => {
const { t } = useTranslation()
const { theme } = useTheme()
const i18nPrefix = `billing.plans.${plan}`
const i18nPrefix = `plans.${plan}` as const
const isPremiumPlan = plan === SelfHostedPlan.premium
const AwsMarketplace = useMemo(() => {
return theme === Theme.light ? AwsMarketplaceLight : AwsMarketplaceDark
@ -42,7 +42,7 @@ const Button = ({
onClick={handleGetPayUrl}
>
<div className="flex grow items-center gap-x-2">
<span>{t(`${i18nPrefix}.btnText` as any) as string}</span>
<span>{t(`${i18nPrefix}.btnText`, { ns: 'billing' })}</span>
{isPremiumPlan && (
<span className="pb-px pt-[7px]">
<AwsMarketplace className="h-6" />

View File

@ -16,12 +16,13 @@ const featuresTranslations: Record<string, string[]> = {
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string, options?: Record<string, unknown>) => {
const prefix = options?.ns ? `${options.ns}.` : ''
if (options?.returnObjects)
return featuresTranslations[key] || []
return key
return featuresTranslations[`${prefix}${key}`] || []
return `${prefix}${key}`
},
}),
Trans: ({ i18nKey }: { i18nKey: string }) => <span>{i18nKey}</span>,
Trans: ({ i18nKey, ns }: { i18nKey: string, ns?: string }) => <span>{ns ? `${ns}.${i18nKey}` : i18nKey}</span>,
}))
vi.mock('../../../../base/toast', () => ({

View File

@ -47,7 +47,7 @@ const SelfHostedPlanItem: FC<SelfHostedPlanItemProps> = ({
plan,
}) => {
const { t } = useTranslation()
const i18nPrefix = `billing.plans.${plan}`
const i18nPrefix = `plans.${plan}` as const
const isFreePlan = plan === SelfHostedPlan.community
const isPremiumPlan = plan === SelfHostedPlan.premium
const isEnterprisePlan = plan === SelfHostedPlan.enterprise
@ -58,7 +58,7 @@ const SelfHostedPlanItem: FC<SelfHostedPlanItemProps> = ({
if (!isCurrentWorkspaceManager) {
Toast.notify({
type: 'error',
message: t('billing.buyPermissionDeniedTip'),
message: t('buyPermissionDeniedTip', { ns: 'billing' }),
className: 'z-[1001]',
})
return
@ -85,16 +85,16 @@ const SelfHostedPlanItem: FC<SelfHostedPlanItemProps> = ({
<div className=" flex flex-col gap-y-6 px-1 pt-10">
{STYLE_MAP[plan].icon}
<div className="flex min-h-[104px] flex-col gap-y-2">
<div className="text-[30px] font-medium leading-[1.2] text-text-primary">{t(`${i18nPrefix}.name` as any) as string}</div>
<div className="system-md-regular line-clamp-2 text-text-secondary">{t(`${i18nPrefix}.description` as any) as string}</div>
<div className="text-[30px] font-medium leading-[1.2] text-text-primary">{t(`${i18nPrefix}.name`, { ns: 'billing' })}</div>
<div className="system-md-regular line-clamp-2 text-text-secondary">{t(`${i18nPrefix}.description`, { ns: 'billing' })}</div>
</div>
</div>
{/* Price */}
<div className="flex items-end gap-x-2 px-1 pb-8 pt-4">
<div className="title-4xl-semi-bold shrink-0 text-text-primary">{t(`${i18nPrefix}.price` as any) as string}</div>
<div className="title-4xl-semi-bold shrink-0 text-text-primary">{t(`${i18nPrefix}.price`, { ns: 'billing' })}</div>
{!isFreePlan && (
<span className="system-md-regular pb-0.5 text-text-tertiary">
{t(`${i18nPrefix}.priceTip` as any) as string}
{t(`${i18nPrefix}.priceTip`, { ns: 'billing' })}
</span>
)}
</div>
@ -115,7 +115,7 @@ const SelfHostedPlanItem: FC<SelfHostedPlanItemProps> = ({
</div>
</div>
<span className="system-xs-regular text-text-tertiary">
{t('billing.plans.premium.comingSoon')}
{t('plans.premium.comingSoon', { ns: 'billing' })}
</span>
</div>
)}

View File

@ -8,10 +8,11 @@ vi.mock('react-i18next', () => ({
t: (key: string, options?: Record<string, unknown>) => {
if (options?.returnObjects)
return ['Feature A', 'Feature B']
return key
const prefix = options?.ns ? `${options.ns}.` : ''
return `${prefix}${key}`
},
}),
Trans: ({ i18nKey }: { i18nKey: string }) => <span>{i18nKey}</span>,
Trans: ({ i18nKey, ns }: { i18nKey: string, ns?: string }) => <span>{ns ? `${ns}.${i18nKey}` : i18nKey}</span>,
}))
describe('SelfHostedPlanItem/List', () => {

View File

@ -11,14 +11,15 @@ const List = ({
plan,
}: ListProps) => {
const { t } = useTranslation()
const i18nPrefix = `billing.plans.${plan}`
const features = t(`${i18nPrefix}.features` as any, { returnObjects: true }) as unknown as string[]
const i18nPrefix = `plans.${plan}` as const
const features = t(`${i18nPrefix}.features`, { ns: 'billing', returnObjects: true }) as string[]
return (
<div className="flex flex-col gap-y-[10px] p-6">
<div className="system-md-semibold text-text-secondary">
<Trans
i18nKey={t(`${i18nPrefix}.includesTitle` as any) as string}
i18nKey={`${i18nPrefix}.includesTitle`}
ns="billing"
components={{ highlight: <span className="text-text-warning"></span> }}
/>
</div>