type safe

This commit is contained in:
Stephen Zhou 2025-12-26 22:48:25 +08:00
parent caf3e58f61
commit d9a439d774
No known key found for this signature in database
58 changed files with 324 additions and 141 deletions

View File

@ -4,6 +4,7 @@ import type { IAppCardProps } from '@/app/components/app/overview/app-card'
import type { BlockEnum } from '@/app/components/workflow/types'
import type { UpdateAppSiteCodeResponse } from '@/models/app'
import type { App } from '@/types/app'
import type { I18nKeysByPrefix } from '@/types/i18n'
import * as React from 'react'
import { useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
@ -94,7 +95,7 @@ const CardView: FC<ICardViewProps> = ({ appId, isInPanel, className }) => {
catch (error) { console.error(error) }
}
const handleCallbackResult = (err: Error | null, message?: string) => {
const handleCallbackResult = (err: Error | null, message?: I18nKeysByPrefix<'common', 'actionMsg.'>) => {
const type = err ? 'error' : 'success'
message ||= (type === 'success' ? 'modifiedSuccessfully' : 'modifiedUnsuccessfully')
@ -104,7 +105,7 @@ const CardView: FC<ICardViewProps> = ({ appId, isInPanel, className }) => {
notify({
type,
message: t(`actionMsg.${message}` as any, { ns: 'common' }) as string,
message: t(`actionMsg.${message}`, { ns: 'common' }) as string,
})
}

View File

@ -1,5 +1,6 @@
'use client'
import type { PeriodParams } from '@/app/components/app/overview/app-chart'
import type { I18nKeysByPrefix } from '@/types/i18n'
import dayjs from 'dayjs'
import quarterOfYear from 'dayjs/plugin/quarterOfYear'
import * as React from 'react'
@ -16,7 +17,9 @@ dayjs.extend(quarterOfYear)
const today = dayjs()
const TIME_PERIOD_MAPPING = [
type TimePeriodName = I18nKeysByPrefix<'appLog', 'filter.period.'>
const TIME_PERIOD_MAPPING: { value: number, name: TimePeriodName }[] = [
{ value: 0, name: 'today' },
{ value: 7, name: 'last7days' },
{ value: 30, name: 'last30days' },

View File

@ -2,13 +2,16 @@
import type { FC } from 'react'
import type { PeriodParams } from '@/app/components/app/overview/app-chart'
import type { Item } from '@/app/components/base/select'
import type { I18nKeysByPrefix } from '@/types/i18n'
import dayjs from 'dayjs'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import { SimpleSelect } from '@/app/components/base/select'
type TimePeriodName = I18nKeysByPrefix<'appLog', 'filter.period.'>
type Props = {
periodMapping: { [key: string]: { value: number, name: string } }
periodMapping: { [key: string]: { value: number, name: TimePeriodName } }
onSelect: (payload: PeriodParams) => void
queryDateFormat: string
}
@ -53,7 +56,7 @@ const LongTimeRangePicker: FC<Props> = ({
return (
<SimpleSelect
items={Object.entries(periodMapping).map(([k, v]) => ({ value: k, name: t(`filter.period.${v.name}` as any, { ns: 'appLog' }) as string }))}
items={Object.entries(periodMapping).map(([k, v]) => ({ value: k, name: t(`filter.period.${v.name}`, { ns: 'appLog' }) }))}
className="mt-0 !w-40"
notClearable={true}
onSelect={handleSelect}

View File

@ -2,6 +2,7 @@
import type { Dayjs } from 'dayjs'
import type { FC } from 'react'
import type { PeriodParams, PeriodParamsWithTimeRange } from '@/app/components/app/overview/app-chart'
import type { I18nKeysByPrefix } from '@/types/i18n'
import dayjs from 'dayjs'
import * as React from 'react'
import { useCallback, useState } from 'react'
@ -13,8 +14,10 @@ import RangeSelector from './range-selector'
const today = dayjs()
type TimePeriodName = I18nKeysByPrefix<'appLog', 'filter.period.'>
type Props = {
ranges: { value: number, name: string }[]
ranges: { value: number, name: TimePeriodName }[]
onSelect: (payload: PeriodParams) => void
queryDateFormat: string
}

View File

@ -2,6 +2,7 @@
import type { FC } from 'react'
import type { PeriodParamsWithTimeRange, TimeRange } from '@/app/components/app/overview/app-chart'
import type { Item } from '@/app/components/base/select'
import type { I18nKeysByPrefix } from '@/types/i18n'
import { RiArrowDownSLine, RiCheckLine } from '@remixicon/react'
import dayjs from 'dayjs'
import * as React from 'react'
@ -12,9 +13,11 @@ import { cn } from '@/utils/classnames'
const today = dayjs()
type TimePeriodName = I18nKeysByPrefix<'appLog', 'filter.period.'>
type Props = {
isCustomRange: boolean
ranges: { value: number, name: string }[]
ranges: { value: number, name: TimePeriodName }[]
onSelect: (payload: PeriodParamsWithTimeRange) => void
}
@ -66,7 +69,7 @@ const RangeSelector: FC<Props> = ({
}, [])
return (
<SimpleSelect
items={ranges.map(v => ({ ...v, name: t(`filter.period.${v.name}` as any, { ns: 'appLog' }) as string }))}
items={ranges.map(v => ({ ...v, name: t(`filter.period.${v.name}`, { ns: 'appLog' }) }))}
className="mt-0 !w-40"
notClearable={true}
onSelect={handleSelectRange}

View File

@ -73,7 +73,7 @@ const DatasetInfo: FC<DatasetInfoProps> = ({
{isExternalProvider && t('externalTag', { ns: 'dataset' })}
{!isExternalProvider && isPipelinePublished && dataset.doc_form && dataset.indexing_technique && (
<div className="flex items-center gap-x-2">
<span>{t(`chunkingMode.${DOC_FORM_TEXT[dataset.doc_form]}` as any, { ns: 'dataset' }) as string}</span>
<span>{t(`chunkingMode.${DOC_FORM_TEXT[dataset.doc_form]}`, { ns: 'dataset' })}</span>
<span>{formatIndexingTechniqueAndMethod(dataset.indexing_technique, dataset.retrieval_model_dict?.search_method)}</span>
</div>
)}

View File

@ -116,7 +116,7 @@ const DatasetSidebarDropdown = ({
{isExternalProvider && t('externalTag', { ns: 'dataset' })}
{!isExternalProvider && dataset.doc_form && dataset.indexing_technique && (
<div className="flex items-center gap-x-2">
<span>{t(`chunkingMode.${DOC_FORM_TEXT[dataset.doc_form]}` as any, { ns: 'dataset' }) as string}</span>
<span>{t(`chunkingMode.${DOC_FORM_TEXT[dataset.doc_form]}`, { ns: 'dataset' })}</span>
<span>{formatIndexingTechniqueAndMethod(dataset.indexing_technique, dataset.retrieval_model_dict?.search_method)}</span>
</div>
)}

View File

@ -1,5 +1,6 @@
import type { ModelAndParameter } from '../configuration/debug/types'
import type { InputVar, Variable } from '@/app/components/workflow/types'
import type { I18nKeysByPrefix } from '@/types/i18n'
import type { PublishWorkflowParams } from '@/types/workflow'
import {
RiArrowDownSLine,
@ -53,7 +54,9 @@ import AccessControl from '../app-access-control'
import PublishWithMultipleModel from './publish-with-multiple-model'
import SuggestedAction from './suggested-action'
const ACCESS_MODE_MAP: Record<AccessMode, { label: string, icon: React.ElementType }> = {
type AccessModeLabel = I18nKeysByPrefix<'app', 'accessControlDialog.accessItems.'>
const ACCESS_MODE_MAP: Record<AccessMode, { label: AccessModeLabel, icon: React.ElementType }> = {
[AccessMode.ORGANIZATION]: {
label: 'organization',
icon: RiBuildingLine,
@ -84,7 +87,7 @@ const AccessModeDisplay: React.FC<{ mode?: AccessMode }> = ({ mode }) => {
<>
<Icon className="h-4 w-4 shrink-0 text-text-secondary" />
<div className="grow truncate">
<span className="system-sm-medium text-text-secondary">{t(`accessControlDialog.accessItems.${label}` as any, { ns: 'app' }) as string}</span>
<span className="system-sm-medium text-text-secondary">{t(`accessControlDialog.accessItems.${label}`, { ns: 'app' })}</span>
</div>
</>
)

View File

@ -96,7 +96,7 @@ const ConfigModal: FC<IConfigModalProps> = ({
if (!isValid) {
Toast.notify({
type: 'error',
message: t(`varKeyError.${errorMessageKey}` as any, { ns: 'appDebug', key: t('variableConfig.varName', { ns: 'appDebug' }) }) as string,
message: t(`varKeyError.${errorMessageKey}`, { ns: 'appDebug', key: t('variableConfig.varName', { ns: 'appDebug' }) }),
})
return false
}
@ -216,7 +216,7 @@ const ConfigModal: FC<IConfigModalProps> = ({
if (!isValid) {
Toast.notify({
type: 'error',
message: t(`varKeyError.${errorMessageKey}` as any, { ns: 'appDebug', key: errorKey }) as string,
message: t(`varKeyError.${errorMessageKey}`, { ns: 'appDebug', key: errorKey }),
})
return
}

View File

@ -1,6 +1,7 @@
'use client'
import type { FC } from 'react'
import type { InputVarType } from '@/app/components/workflow/types'
import type { I18nKeysByPrefix } from '@/types/i18n'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import InputVarTypeIcon from '@/app/components/workflow/nodes/_base/components/input-var-type-icon'
@ -12,7 +13,9 @@ export type ISelectTypeItemProps = {
onClick: () => void
}
const i18nFileTypeMap: Record<string, string> = {
type VariableConfigTypeKey = I18nKeysByPrefix<'appDebug', 'variableConfig.'>
const i18nTypeMap: Partial<Record<InputVarType, VariableConfigTypeKey>> = {
'file': 'single-file',
'file-list': 'multi-files',
}
@ -23,7 +26,8 @@ const SelectTypeItem: FC<ISelectTypeItemProps> = ({
onClick,
}) => {
const { t } = useTranslation()
const typeName = t(`variableConfig.${i18nFileTypeMap[type] || type}` as any, { ns: 'appDebug' }) as string
const typeKey = i18nTypeMap[type] ?? type as VariableConfigTypeKey
const typeName = t(`variableConfig.${typeKey}`, { ns: 'appDebug' })
return (
<div

View File

@ -42,6 +42,7 @@ import { GeneratorType } from './types'
import useGenData from './use-gen-data'
const i18nPrefix = 'generate'
export type IGetAutomaticResProps = {
mode: AppModeEnum
isShow: boolean
@ -131,17 +132,19 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
icon: RiGitCommitLine,
key: 'GitGud',
},
]
] as const
// eslint-disable-next-line sonarjs/no-nested-template-literals, sonarjs/no-nested-conditional
const [instructionFromSessionStorage, setInstruction] = useSessionStorageState<string>(`improve-instruction-${flowId}${isBasicMode ? '' : `-${nodeId}${editorId ? `-${editorId}` : ''}`}`)
const instruction = instructionFromSessionStorage || ''
const [ideaOutput, setIdeaOutput] = useState<string>('')
type TemplateKey = typeof tryList[number]['key']
const [editorKey, setEditorKey] = useState(`${flowId}-0`)
const handleChooseTemplate = useCallback((key: string) => {
const handleChooseTemplate = useCallback((key: TemplateKey) => {
return () => {
const template = t(`generate.template.${key}.instruction` as any, { ns: 'appDebug' }) as string
const template = t(`generate.template.${key}.instruction` as const, { ns: 'appDebug' })
setInstruction(template)
setEditorKey(`${flowId}-${Date.now()}`)
}
@ -323,7 +326,7 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
<TryLabel
key={item.key}
Icon={item.icon}
text={t(`generate.template.${item.key}.name` as any, { ns: 'appDebug' }) as string}
text={t(`generate.template.${item.key}.name`, { ns: 'appDebug' })}
onClick={handleChooseTemplate(item.key)}
/>
))}

View File

@ -1,6 +1,7 @@
'use client'
import type { FC } from 'react'
import type { QueryParam } from './index'
import type { I18nKeysByPrefix } from '@/types/i18n'
import { RiCalendarLine } from '@remixicon/react'
import dayjs from 'dayjs'
import quarterOfYear from 'dayjs/plugin/quarterOfYear'
@ -15,7 +16,9 @@ dayjs.extend(quarterOfYear)
const today = dayjs()
export const TIME_PERIOD_MAPPING: { [key: string]: { value: number, name: string } } = {
type TimePeriodName = I18nKeysByPrefix<'appLog', 'filter.period.'>
export const TIME_PERIOD_MAPPING: { [key: string]: { value: number, name: TimePeriodName } } = {
1: { value: 0, name: 'today' },
2: { value: 7, name: 'last7days' },
3: { value: 28, name: 'last4weeks' },
@ -50,7 +53,7 @@ const Filter: FC<IFilterProps> = ({ isChatMode, appId, queryParams, setQueryPara
setQueryParams({ ...queryParams, period: item.value })
}}
onClear={() => setQueryParams({ ...queryParams, period: '9' })}
items={Object.entries(TIME_PERIOD_MAPPING).map(([k, v]) => ({ value: k, name: t(`filter.period.${v.name}` as any, { ns: 'appLog' }) as string }))}
items={Object.entries(TIME_PERIOD_MAPPING).map(([k, v]) => ({ value: k, name: t(`filter.period.${v.name}`, { ns: 'appLog' }) }))}
/>
<Chip
className="min-w-[150px]"

View File

@ -2,6 +2,7 @@
import type { AppDetailResponse } from '@/models/app'
import type { AppTrigger } from '@/service/use-tools'
import type { AppSSO } from '@/types/app'
import type { I18nKeysByPrefix } from '@/types/i18n'
import Link from 'next/link'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
@ -23,7 +24,7 @@ import { canFindTool } from '@/utils'
export type ITriggerCardProps = {
appInfo: AppDetailResponse & Partial<AppSSO>
onToggleResult?: (err: Error | null, message?: string) => void
onToggleResult?: (err: Error | null, message?: I18nKeysByPrefix<'common', 'actionMsg.'>) => void
}
const getTriggerIcon = (trigger: AppTrigger, triggerPlugins: any[]) => {

View File

@ -1,6 +1,7 @@
'use client'
import type { FC } from 'react'
import type { QueryParam } from './index'
import type { I18nKeysByPrefix } from '@/types/i18n'
import { RiCalendarLine } from '@remixicon/react'
import dayjs from 'dayjs'
import quarterOfYear from 'dayjs/plugin/quarterOfYear'
@ -14,7 +15,9 @@ dayjs.extend(quarterOfYear)
const today = dayjs()
export const TIME_PERIOD_MAPPING: { [key: string]: { value: number, name: string } } = {
type TimePeriodName = I18nKeysByPrefix<'appLog', 'filter.period.'>
export const TIME_PERIOD_MAPPING: { [key: string]: { value: number, name: TimePeriodName } } = {
1: { value: 0, name: 'today' },
2: { value: 7, name: 'last7days' },
3: { value: 28, name: 'last4weeks' },
@ -55,7 +58,7 @@ const Filter: FC<IFilterProps> = ({ queryParams, setQueryParams }: IFilterProps)
setQueryParams({ ...queryParams, period: item.value })
}}
onClear={() => setQueryParams({ ...queryParams, period: '9' })}
items={Object.entries(TIME_PERIOD_MAPPING).map(([k, v]) => ({ value: k, name: t(`filter.period.${v.name}` as any, { ns: 'appLog' }) as string }))}
items={Object.entries(TIME_PERIOD_MAPPING).map(([k, v]) => ({ value: k, name: t(`filter.period.${v.name}`, { ns: 'appLog' }) }))}
/>
<Input
wrapperClassName="w-[200px]"

View File

@ -90,11 +90,11 @@ const BlockInput: FC<IBlockInputProps> = ({
const handleSubmit = (value: string) => {
if (onConfirm) {
const keys = getInputKeys(value)
const { isValid, errorKey, errorMessageKey } = checkKeys(keys)
if (!isValid) {
const result = checkKeys(keys)
if (!result.isValid) {
Toast.notify({
type: 'error',
message: t(`varKeyError.${errorMessageKey}` as any, { ns: 'appDebug', key: errorKey }) as string,
message: t(`varKeyError.${result.errorMessageKey}`, { ns: 'appDebug', key: result.errorKey }),
})
return
}

View File

@ -4,31 +4,31 @@ import dayjs from './utils/dayjs'
const YEAR_RANGE = 100
const daysInWeek = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] as const
export const useDaysOfWeek = () => {
const { t } = useTranslation()
const daysOfWeek = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map(day => t(`daysInWeek.${day}` as any, { ns: 'time' }) as string)
return daysOfWeek
return daysInWeek.map(day => t(`daysInWeek.${day}`, { ns: 'time' }))
}
const monthNames = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
] as const
export const useMonths = () => {
const { t } = useTranslation()
const months = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
].map(month => t(`months.${month}` as any, { ns: 'time' }) as string)
return months
return monthNames.map(month => t(`months.${month}`, { ns: 'time' }))
}
export const useYearOptions = () => {

View File

@ -1,22 +1,28 @@
import type { I18nKeysWithPrefix } from '@/types/i18n'
import { RiLock2Fill } from '@remixicon/react'
import Link from 'next/link'
import { useTranslation } from 'react-i18next'
import { cn } from '@/utils/classnames'
type EncryptedKey = I18nKeysWithPrefix<'common', 'provider.encrypted.'>
type Props = {
className?: string
frontTextKey?: string
backTextKey?: string
frontTextKey?: EncryptedKey
backTextKey?: EncryptedKey
}
const DEFAULT_FRONT_KEY: EncryptedKey = 'provider.encrypted.front'
const DEFAULT_BACK_KEY: EncryptedKey = 'provider.encrypted.back'
export const EncryptedBottom = (props: Props) => {
const { t } = useTranslation()
const { frontTextKey, backTextKey, className } = props
const { frontTextKey = DEFAULT_FRONT_KEY, backTextKey = DEFAULT_BACK_KEY, className } = props
return (
<div className={cn('system-xs-regular flex items-center justify-center rounded-b-2xl border-t-[0.5px] border-divider-subtle bg-background-soft px-2 py-3 text-text-tertiary', className)}>
<RiLock2Fill className="mx-1 h-3 w-3 text-text-quaternary" />
{t((frontTextKey || 'provider.encrypted.front') as any, { ns: 'common' })}
{t(frontTextKey, { ns: 'common' })}
<Link
className="mx-1 text-text-accent"
target="_blank"
@ -25,7 +31,7 @@ export const EncryptedBottom = (props: Props) => {
>
PKCS1_OAEP
</Link>
{t((backTextKey || 'provider.encrypted.back') as any, { ns: 'common' })}
{t(backTextKey, { ns: 'common' })}
</div>
)
}

View File

@ -1,6 +1,7 @@
'use client'
import type { OnFeaturesChange } from '@/app/components/base/features/types'
import type { Item } from '@/app/components/base/select'
import type { I18nKeysWithPrefix } from '@/types/i18n'
import { Listbox, ListboxButton, ListboxOption, ListboxOptions, Transition } from '@headlessui/react'
import { CheckIcon, ChevronDownIcon } from '@heroicons/react/20/solid'
import { RiCloseLine } from '@remixicon/react'
@ -18,6 +19,8 @@ import { useAppVoices } from '@/service/use-apps'
import { TtsAutoPlay } from '@/types/app'
import { cn } from '@/utils/classnames'
type VoiceLanguageKey = I18nKeysWithPrefix<'common', 'voice.language.'>
type VoiceParamConfigProps = {
onClose: () => void
onChange?: OnFeaturesChange
@ -97,7 +100,7 @@ const VoiceParamConfig = ({
className="h-full w-full cursor-pointer rounded-lg border-0 bg-components-input-bg-normal py-1.5 pl-3 pr-10 focus-visible:bg-state-base-hover focus-visible:outline-none group-hover:bg-state-base-hover sm:text-sm sm:leading-6"
>
<span className={cn('block truncate text-left text-text-secondary', !languageItem?.name && 'text-text-tertiary')}>
{languageItem?.name ? t(`voice.language.${languageItem?.value.replace('-', '')}` as any, { ns: 'common' }) as string : localLanguagePlaceholder}
{languageItem?.name ? t(`voice.language.${languageItem?.value.replace('-', '')}` as VoiceLanguageKey, { ns: 'common' }) : localLanguagePlaceholder}
</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronDownIcon
@ -128,7 +131,7 @@ const VoiceParamConfig = ({
<span
className={cn('block', selected && 'font-normal')}
>
{t(`voice.language.${(item.value).toString().replace('-', '')}` as any, { ns: 'common' }) as string}
{t(`voice.language.${(item.value).toString().replace('-', '')}` as VoiceLanguageKey, { ns: 'common' })}
</span>
{(selected || item.value === text2speech?.language) && (
<span

View File

@ -1,3 +1,5 @@
import type { InputType } from './types'
import type { I18nKeysByPrefix } from '@/types/i18n'
import {
RiAlignLeft,
RiCheckboxLine,
@ -11,7 +13,9 @@ import { useTranslation } from 'react-i18next'
import { PipelineInputVarType } from '@/models/pipeline'
import { InputTypeEnum } from './types'
const i18nFileTypeMap: Record<string, string> = {
type VariableConfigKeySuffix = I18nKeysByPrefix<'appDebug', 'variableConfig.'>
const i18nFileTypeMap: Partial<Record<InputType, VariableConfigKeySuffix>> = {
'number': 'number',
'file': 'single-file',
'file-list': 'multi-files',
@ -44,7 +48,7 @@ export const useInputTypeOptions = (supportFile: boolean) => {
return options.map((value) => {
return {
value,
label: t(`variableConfig.${i18nFileTypeMap[value] || value}` as any, { ns: 'appDebug' }),
label: t(`variableConfig.${i18nFileTypeMap[value] || value}` as `variableConfig.${VariableConfigKeySuffix}`, { ns: 'appDebug' }),
Icon: INPUT_TYPE_ICON[value],
type: DATA_TYPE[value],
}

View File

@ -127,7 +127,7 @@ const TagInput: FC<TagInputProps> = ({
setValue(e.target.value)
}}
onKeyDown={handleKeyDown}
placeholder={t((placeholder || (isSpecialMode ? 'model.params.stop_sequencesPlaceholder' : 'segment.addKeyWord')) as any, { ns: isSpecialMode ? 'common' : 'datasetDocuments' })}
placeholder={placeholder || (isSpecialMode ? t('model.params.stop_sequencesPlaceholder', { ns: 'common' }) : t('segment.addKeyWord', { ns: 'datasetDocuments' }))}
/>
</div>
)

View File

@ -35,7 +35,7 @@ const CloudPlanItem: FC<CloudPlanItemProps> = ({
}) => {
const { t } = useTranslation()
const [loading, setLoading] = React.useState(false)
const i18nPrefix = `plans.${plan}`
const i18nPrefix = `plans.${plan}` as const
const isFreePlan = plan === Plan.sandbox
const isMostPopularPlan = plan === Plan.professional
const planInfo = ALL_PLANS[plan]
@ -106,7 +106,7 @@ 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, { ns: 'billing' }) 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">
@ -117,7 +117,7 @@ const CloudPlanItem: FC<CloudPlanItemProps> = ({
)
}
</div>
<div className="system-sm-regular text-text-secondary">{t(`${i18nPrefix}.description` as any, { ns: 'billing' }) as string}</div>
<div className="system-sm-regular text-text-secondary">{t(`${i18nPrefix}.description`, { ns: 'billing' })}</div>
</div>
</div>
{/* Price */}

View File

@ -25,7 +25,7 @@ const Button = ({
}: ButtonProps) => {
const { t } = useTranslation()
const { theme } = useTheme()
const i18nPrefix = `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, { ns: 'billing' }) as string}</span>
<span>{t(`${i18nPrefix}.btnText`, { ns: 'billing' })}</span>
{isPremiumPlan && (
<span className="pb-px pt-[7px]">
<AwsMarketplace className="h-6" />

View File

@ -47,7 +47,7 @@ const SelfHostedPlanItem: FC<SelfHostedPlanItemProps> = ({
plan,
}) => {
const { t } = useTranslation()
const i18nPrefix = `plans.${plan}`
const i18nPrefix = `plans.${plan}` as const
const isFreePlan = plan === SelfHostedPlan.community
const isPremiumPlan = plan === SelfHostedPlan.premium
const isEnterprisePlan = plan === SelfHostedPlan.enterprise
@ -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, { ns: 'billing' }) as string}</div>
<div className="system-md-regular line-clamp-2 text-text-secondary">{t(`${i18nPrefix}.description` as any, { ns: 'billing' }) 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, { ns: 'billing' }) 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, { ns: 'billing' }) as string}
{t(`${i18nPrefix}.priceTip`, { ns: 'billing' })}
</span>
)}
</div>

View File

@ -11,14 +11,15 @@ const List = ({
plan,
}: ListProps) => {
const { t } = useTranslation()
const i18nPrefix = `plans.${plan}`
const features = t(`${i18nPrefix}.features` as any, { ns: 'billing', 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, { ns: 'billing' }) as string}
i18nKey={`${i18nPrefix}.includesTitle`}
ns="billing"
components={{ highlight: <span className="text-text-warning"></span> }}
/>
</div>

View File

@ -26,12 +26,19 @@ const PriorityLabel = ({ className }: PriorityLabelProps) => {
if (plan.type === Plan.team || plan.type === Plan.enterprise)
return DocumentProcessingPriority.topPriority
return DocumentProcessingPriority.standard
}, [plan])
return (
<Tooltip popupContent={(
<div>
<div className="mb-1 text-xs font-semibold text-text-primary">{`${t('plansCommon.documentProcessingPriority', { ns: 'billing' })}: ${t(`plansCommon.priority.${priority}` as any, { ns: 'billing' }) as string}`}</div>
<div className="mb-1 text-xs font-semibold text-text-primary">
{t('plansCommon.documentProcessingPriority', { ns: 'billing' })}
:
{' '}
{t(`plansCommon.priority.${priority}`, { ns: 'billing' })}
</div>
{
priority !== DocumentProcessingPriority.topPriority && (
<div className="text-xs text-text-secondary">{t('plansCommon.documentProcessingPriorityTip', { ns: 'billing' })}</div>
@ -51,7 +58,7 @@ const PriorityLabel = ({ className }: PriorityLabelProps) => {
<RiAedFill className="mr-0.5 size-3" />
)
}
<span>{t(`plansCommon.priority.${priority}` as any, { ns: 'billing' }) as string}</span>
<span>{t(`plansCommon.priority.${priority}`, { ns: 'billing' })}</span>
</div>
</Tooltip>
)

View File

@ -1,5 +1,6 @@
'use client'
import type { CSSProperties, FC } from 'react'
import type { I18nKeysWithPrefix } from '@/types/i18n'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
@ -16,7 +17,7 @@ type Props = {
isShort?: boolean
onClick?: () => void
loc?: string
labelKey?: string
labelKey?: I18nKeysWithPrefix<'billing', 'upgradeBtn.'>
}
const UpgradeBtn: FC<Props> = ({
@ -47,7 +48,7 @@ const UpgradeBtn: FC<Props> = ({
}
const defaultBadgeLabel = t(isShort ? 'upgradeBtn.encourageShort' : 'upgradeBtn.encourage', { ns: 'billing' })
const label = labelKey ? t(labelKey as any, { ns: 'billing' }) : defaultBadgeLabel
const label = labelKey ? t(labelKey, { ns: 'billing' }) : defaultBadgeLabel
if (isPlain) {
return (

View File

@ -33,8 +33,8 @@ const EconomicalRetrievalMethodConfig: FC<Props> = ({
<div className="space-y-2">
<RadioCard
icon={icon}
title={t(`retrieval.${type}.title` as any, { ns: 'dataset' }) as string}
description={t(`retrieval.${type}.description` as any, { ns: 'dataset' }) as string}
title={t(`retrieval.${type}.title`, { ns: 'dataset' })}
description={t(`retrieval.${type}.description`, { ns: 'dataset' })}
noRadio
chosenConfigWrapClassName="!pb-3"
chosenConfig={(

View File

@ -44,7 +44,7 @@ const Content = ({
{name}
</div>
<div className="system-2xs-medium-uppercase text-text-tertiary">
{t(`chunkingMode.${DOC_FORM_TEXT[chunkStructure]}` as any, { ns: 'dataset' }) as string}
{t(`chunkingMode.${DOC_FORM_TEXT[chunkStructure]}`, { ns: 'dataset' })}
</div>
</div>
</div>

View File

@ -46,13 +46,13 @@ type Props = {
batchId: string
documents?: FullDocumentDetail[]
indexingType?: string
retrievalMethod?: string
retrievalMethod?: RETRIEVE_METHOD
}
const RuleDetail: FC<{
sourceData?: ProcessRuleResponse
indexingType?: string
retrievalMethod?: string
retrievalMethod?: RETRIEVE_METHOD
}> = ({ sourceData, indexingType, retrievalMethod }) => {
const { t } = useTranslation()
@ -141,7 +141,7 @@ const RuleDetail: FC<{
<FieldInfo
label={t('form.retrievalSetting.title', { ns: 'datasetSettings' })}
// displayedValue={t(`datasetSettings.form.retrievalSetting.${retrievalMethod}`) as string}
displayedValue={t(`retrieval.${indexingType === IndexingType.ECONOMICAL ? 'keyword_search' : retrievalMethod}.title` as any, { ns: 'dataset' }) as string}
displayedValue={t(`retrieval.${indexingType === IndexingType.ECONOMICAL ? 'keyword_search' : retrievalMethod ?? 'semantic_search'}.title`, { ns: 'dataset' })}
valueIcon={(
<Image
className="size-4"

View File

@ -1,9 +1,10 @@
'use client'
import type { createDocumentResponse, FullDocumentDetail } from '@/models/datasets'
import type { RETRIEVE_METHOD } from '@/types/app'
import { RiBookOpenLine } from '@remixicon/react'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import { useTranslation } from 'react-i18next'
import AppIcon from '@/app/components/base/app-icon'
import Divider from '@/app/components/base/divider'
import { useDocLink } from '@/context/i18n'
@ -14,7 +15,7 @@ type StepThreeProps = {
datasetId?: string
datasetName?: string
indexingType?: string
retrievalMethod?: string
retrievalMethod?: RETRIEVE_METHOD
creationCache?: createDocumentResponse
}

View File

@ -63,7 +63,7 @@ const RuleDetail = ({
/>
<FieldInfo
label={t('form.retrievalSetting.title', { ns: 'datasetSettings' })}
displayedValue={t(`retrieval.${indexingType === IndexingType.ECONOMICAL ? 'keyword_search' : retrievalMethod}.title` as any, { ns: 'dataset' }) as string}
displayedValue={t(`retrieval.${indexingType === IndexingType.ECONOMICAL ? 'keyword_search' : retrievalMethod ?? 'semantic_search'}.title`, { ns: 'dataset' })}
valueIcon={(
<Image
className="size-4"

View File

@ -133,7 +133,7 @@ const RuleDetail: FC<IRuleDetailProps> = React.memo(({
/>
<FieldInfo
label={t('form.retrievalSetting.title', { ns: 'datasetSettings' })}
displayedValue={t(`retrieval.${indexingType === IndexingType.ECONOMICAL ? 'keyword_search' : retrievalMethod}.title` as any, { ns: 'dataset' }) as string}
displayedValue={t(`retrieval.${indexingType === IndexingType.ECONOMICAL ? 'keyword_search' : retrievalMethod ?? 'semantic_search'}.title`, { ns: 'dataset' })}
valueIcon={(
<Image
className="size-4"

View File

@ -228,7 +228,7 @@ const QueryInput = ({
className="flex h-7 cursor-pointer items-center space-x-0.5 rounded-lg border-[0.5px] border-components-button-secondary-bg bg-components-button-secondary-bg px-1.5 shadow-xs backdrop-blur-[5px] hover:bg-components-button-secondary-bg-hover"
>
{icon}
<div className="text-xs font-medium uppercase text-text-secondary">{t(`retrieval.${retrievalMethod}.title` as any, { ns: 'dataset' }) as string}</div>
<div className="text-xs font-medium uppercase text-text-secondary">{t(`retrieval.${retrievalMethod}.title`, { ns: 'dataset' })}</div>
<RiEqualizer2Line className="size-4 text-components-menu-item-text"></RiEqualizer2Line>
</div>
)}

View File

@ -217,9 +217,9 @@ const DatasetCard = ({
{dataset.doc_form && (
<span
className="min-w-0 max-w-full truncate"
title={t(`chunkingMode.${DOC_FORM_TEXT[dataset.doc_form]}` as any, { ns: 'dataset' }) as string}
title={t(`chunkingMode.${DOC_FORM_TEXT[dataset.doc_form]}`, { ns: 'dataset' })}
>
{t(`chunkingMode.${DOC_FORM_TEXT[dataset.doc_form]}` as any, { ns: 'dataset' }) as string}
{t(`chunkingMode.${DOC_FORM_TEXT[dataset.doc_form]}`, { ns: 'dataset' })}
</span>
)}
{dataset.indexing_technique && (

View File

@ -48,7 +48,7 @@ const Category: FC<ICategoryProps> = ({
className={itemClassName(name === value)}
onClick={() => onChange(name)}
>
{`category.${name}` in exploreI18n ? t(`category.${name}` as any, { ns: 'explore' }) as string : name}
{`category.${name}` in exploreI18n ? t(`category.${name}`, { ns: 'explore' }) : name}
</div>
))}
</div>

View File

@ -1,4 +1,5 @@
'use client'
import type { RoleKey } from './role-selector'
import type { InvitationResult } from '@/models/common'
import { RiCloseLine, RiErrorWarningFill } from '@remixicon/react'
import { useBoolean } from 'ahooks'
@ -47,7 +48,7 @@ const InviteModal = ({
}, [licenseLimit, emails])
const { locale } = useContext(I18n)
const [role, setRole] = useState<string>('normal')
const [role, setRole] = useState<RoleKey>('normal')
const [isSubmitting, {
setTrue: setIsSubmitting,

View File

@ -11,9 +11,18 @@ import {
import { useProviderContext } from '@/context/provider-context'
import { cn } from '@/utils/classnames'
const roleI18nKeyMap = {
normal: 'members.normal',
editor: 'members.editor',
admin: 'members.admin',
dataset_operator: 'members.datasetOperator',
} as const
export type RoleKey = keyof typeof roleI18nKeyMap
export type RoleSelectorProps = {
value: string
onChange: (role: string) => void
value: RoleKey
onChange: (role: RoleKey) => void
}
const RoleSelector = ({ value, onChange }: RoleSelectorProps) => {
@ -21,8 +30,6 @@ const RoleSelector = ({ value, onChange }: RoleSelectorProps) => {
const [open, setOpen] = useState(false)
const { datasetOperatorEnabled } = useProviderContext()
const toHump = (name: string) => name.replace(/_(\w)/g, (all, letter) => letter.toUpperCase())
return (
<PortalToFollowElem
open={open}
@ -36,7 +43,7 @@ const RoleSelector = ({ value, onChange }: RoleSelectorProps) => {
className="block"
>
<div className={cn('flex cursor-pointer items-center rounded-lg bg-components-input-bg-normal px-3 py-2 hover:bg-state-base-hover', open && 'bg-state-base-hover')}>
<div className="mr-2 grow text-sm leading-5 text-text-primary">{t('members.invitedAsRole', { ns: 'common', role: t(`members.${toHump(value)}` as any, { ns: 'common' }) })}</div>
<div className="mr-2 grow text-sm leading-5 text-text-primary">{t('members.invitedAsRole', { ns: 'common', role: t(roleI18nKeyMap[value], { ns: 'common' }) })}</div>
<RiArrowDownSLine className="h-4 w-4 shrink-0 text-text-secondary" />
</div>
</PortalToFollowElemTrigger>

View File

@ -20,6 +20,15 @@ type IOperationProps = {
onOperate: () => void
}
const roleI18nKeyMap = {
admin: { label: 'members.admin', tip: 'members.adminTip' },
editor: { label: 'members.editor', tip: 'members.editorTip' },
normal: { label: 'members.normal', tip: 'members.normalTip' },
dataset_operator: { label: 'members.datasetOperator', tip: 'members.datasetOperatorTip' },
} as const
type OperationRoleKey = keyof typeof roleI18nKeyMap
const Operation = ({
member,
operatorRole,
@ -35,26 +44,25 @@ const Operation = ({
normal: t('members.normal', { ns: 'common' }),
dataset_operator: t('members.datasetOperator', { ns: 'common' }),
}
const roleList = useMemo(() => {
const roleList = useMemo((): OperationRoleKey[] => {
if (operatorRole === 'owner') {
return [
'admin',
'editor',
'normal',
...(datasetOperatorEnabled ? ['dataset_operator'] : []),
...(datasetOperatorEnabled ? ['dataset_operator'] as const : []),
]
}
if (operatorRole === 'admin') {
return [
'editor',
'normal',
...(datasetOperatorEnabled ? ['dataset_operator'] : []),
...(datasetOperatorEnabled ? ['dataset_operator'] as const : []),
]
}
return []
}, [operatorRole, datasetOperatorEnabled])
const { notify } = useContext(ToastContext)
const toHump = (name: string) => name.replace(/_(\w)/g, (all, letter) => letter.toUpperCase())
const handleDeleteMemberOrCancelInvitation = async () => {
setOpen(false)
try {
@ -106,8 +114,8 @@ const Operation = ({
: <div className="mr-1 mt-[2px] h-4 w-4 text-text-accent" />
}
<div>
<div className="system-sm-semibold whitespace-nowrap text-text-secondary">{t(`members.${toHump(role)}` as any, { ns: 'common' })}</div>
<div className="system-xs-regular whitespace-nowrap text-text-tertiary">{t(`members.${toHump(role)}Tip` as any, { ns: 'common' })}</div>
<div className="system-sm-semibold whitespace-nowrap text-text-secondary">{t(roleI18nKeyMap[role].label, { ns: 'common' })}</div>
<div className="system-xs-regular whitespace-nowrap text-text-tertiary">{t(roleI18nKeyMap[role].tip, { ns: 'common' })}</div>
</div>
</div>
))

View File

@ -10,6 +10,13 @@ import { Target04 } from '@/app/components/base/icons/src/vender/solid/general'
import { TONE_LIST } from '@/config'
import { cn } from '@/utils/classnames'
const toneI18nKeyMap = {
Creative: 'model.tone.Creative',
Balanced: 'model.tone.Balanced',
Precise: 'model.tone.Precise',
Custom: 'model.tone.Custom',
} as const
type PresetsParameterProps = {
onSelect: (toneId: number) => void
}
@ -44,7 +51,7 @@ const PresetsParameter: FC<PresetsParameterProps> = ({
text: (
<div className="flex h-full items-center">
{getToneIcon(tone.id)}
{t(`model.tone.${tone.name}` as any, { ns: 'common' }) as string}
{t(toneI18nKeyMap[tone.name], { ns: 'common' })}
</div>
),
}

View File

@ -22,6 +22,13 @@ type DeprecationNoticeProps = {
const i18nPrefix = 'detailPanel.deprecation'
type DeprecatedReasonKey = 'businessAdjustments' | 'ownershipTransferred' | 'noMaintainer'
const validReasonKeys: DeprecatedReasonKey[] = ['businessAdjustments', 'ownershipTransferred', 'noMaintainer']
function isValidReasonKey(key: string): key is DeprecatedReasonKey {
return (validReasonKeys as string[]).includes(key)
}
const DeprecationNotice: FC<DeprecationNoticeProps> = ({
status,
deprecatedReason,
@ -37,19 +44,15 @@ const DeprecationNotice: FC<DeprecationNoticeProps> = ({
const deprecatedReasonKey = useMemo(() => {
if (!deprecatedReason)
return ''
return camelCase(deprecatedReason)
return null
const key = camelCase(deprecatedReason)
if (isValidReasonKey(key))
return key
return null
}, [deprecatedReason])
// Check if the deprecatedReasonKey exists in i18n
const hasValidDeprecatedReason = useMemo(() => {
if (!deprecatedReason || !deprecatedReasonKey)
return false
// Define valid reason keys that exist in i18n
const validReasonKeys = ['businessAdjustments', 'ownershipTransferred', 'noMaintainer']
return validReasonKeys.includes(deprecatedReasonKey)
}, [deprecatedReason, deprecatedReasonKey])
const hasValidDeprecatedReason = deprecatedReasonKey !== null
if (status !== 'deleted')
return null
@ -82,7 +85,7 @@ const DeprecationNotice: FC<DeprecationNoticeProps> = ({
),
}}
values={{
deprecatedReason: t(`${i18nPrefix}.reason.${deprecatedReasonKey}` as any, { ns: 'plugin' }) as string,
deprecatedReason: deprecatedReasonKey ? t(`${i18nPrefix}.reason.${deprecatedReasonKey}`, { ns: 'plugin' }) : '',
alternativePluginId,
}}
/>
@ -91,7 +94,7 @@ const DeprecationNotice: FC<DeprecationNoticeProps> = ({
{
hasValidDeprecatedReason && !alternativePluginId && (
<span>
{t(`${i18nPrefix}.onlyReason` as any, { ns: 'plugin', deprecatedReason: t(`${i18nPrefix}.reason.${deprecatedReasonKey}` as any, { ns: 'plugin' }) as string }) as string}
{t(`${i18nPrefix}.onlyReason`, { ns: 'plugin', deprecatedReason: deprecatedReasonKey ? t(`${i18nPrefix}.reason.${deprecatedReasonKey}`, { ns: 'plugin' }) : '' })}
</span>
)
}

View File

@ -43,7 +43,7 @@ const LoopResultPanel: FC<Props> = ({
<div className={cn(!noWrap && 'shrink-0 ', 'px-4 pt-3')}>
<div className="flex h-8 shrink-0 items-center justify-between">
<div className="system-xl-semibold truncate text-text-primary">
{t(`${i18nPrefix}.testRunLoop` as any, { ns: 'workflow' }) as string}
{t(`${i18nPrefix}.testRunLoop`, { ns: 'workflow' }) }
</div>
<div className="ml-2 shrink-0 cursor-pointer p-1" onClick={onHide}>
<RiCloseLine className="h-4 w-4 text-text-tertiary" />

View File

@ -108,7 +108,7 @@ const ForgotPasswordForm = () => {
{...register('email')}
placeholder={t('emailPlaceholder', { ns: 'login' }) || ''}
/>
{errors.email && <span className="text-sm text-red-400">{t(`${errors.email?.message}` as any, { ns: 'login' })}</span>}
{errors.email && <span className="text-sm text-red-400">{t(`${errors.email?.message}` as 'error.emailInValid', { ns: 'login' })}</span>}
</div>
</div>
)}

View File

@ -138,7 +138,7 @@ const InstallForm = () => {
placeholder={t('emailPlaceholder', { ns: 'login' }) || ''}
className="system-sm-regular w-full appearance-none rounded-md border border-transparent bg-components-input-bg-normal px-3 py-[7px] text-components-input-text-filled caret-primary-600 outline-none placeholder:text-components-input-text-placeholder hover:border-components-input-border-hover hover:bg-components-input-bg-hover focus:border-components-input-border-active focus:bg-components-input-bg-active focus:shadow-xs"
/>
{errors.email && <span className="text-sm text-red-400">{t(`${errors.email?.message}` as any, { ns: 'login' })}</span>}
{errors.email && <span className="text-sm text-red-400">{t(`${errors.email?.message}` as 'error.emailInValid', { ns: 'login' })}</span>}
</div>
</div>
@ -154,7 +154,7 @@ const InstallForm = () => {
className="system-sm-regular w-full appearance-none rounded-md border border-transparent bg-components-input-bg-normal px-3 py-[7px] text-components-input-text-filled caret-primary-600 outline-none placeholder:text-components-input-text-placeholder hover:border-components-input-border-hover hover:bg-components-input-bg-hover focus:border-components-input-border-active focus:bg-components-input-bg-active focus:shadow-xs"
/>
</div>
{errors.name && <span className="text-sm text-red-400">{t(`${errors.name.message}` as any, { ns: 'login' })}</span>}
{errors.name && <span className="text-sm text-red-400">{t(`${errors.name.message}` as 'error.nameEmpty', { ns: 'login' })}</span>}
</div>
<div className="mb-5">

View File

@ -132,7 +132,7 @@ export const TONE_LIST = [
id: 4,
name: 'Custom',
},
]
] as const
export const DEFAULT_CHAT_PROMPT_CONFIG = {
prompt: [

View File

@ -1,3 +1,4 @@
import noAsAnyInT from './rules/no-as-any-in-t.js'
import noLegacyNamespacePrefix from './rules/no-legacy-namespace-prefix.js'
import requireNsOption from './rules/require-ns-option.js'
@ -8,6 +9,7 @@ const plugin = {
version: '1.0.0',
},
rules: {
'no-as-any-in-t': noAsAnyInT,
'no-legacy-namespace-prefix': noLegacyNamespacePrefix,
'require-ns-option': requireNsOption,
},

View File

@ -0,0 +1,65 @@
/** @type {import('eslint').Rule.RuleModule} */
export default {
meta: {
type: 'problem',
docs: {
description: 'Disallow using "as any" type assertion in t() function calls',
},
schema: [],
messages: {
noAsAnyInT:
'Avoid using "as any" in t() function calls. Use proper i18n key types instead.',
},
},
create(context) {
/**
* Check if this is a t() function call
* @param {import('estree').CallExpression} node
* @returns {boolean}
*/
function isTCall(node) {
// Direct t() call
if (node.callee.type === 'Identifier' && node.callee.name === 't')
return true
// i18n.t() or similar member expression
if (
node.callee.type === 'MemberExpression'
&& node.callee.property.type === 'Identifier'
&& node.callee.property.name === 't'
) {
return true
}
return false
}
/**
* Check if a node is a TSAsExpression with "any" type
* @param {object} node
* @returns {boolean}
*/
function isAsAny(node) {
return (
node.type === 'TSAsExpression'
&& node.typeAnnotation
&& node.typeAnnotation.type === 'TSAnyKeyword'
)
}
return {
CallExpression(node) {
if (!isTCall(node) || node.arguments.length === 0)
return
const firstArg = node.arguments[0]
// Check if the first argument uses "as any"
if (isAsAny(firstArg)) {
context.report({
node: firstArg,
messageId: 'noAsAnyInT',
})
}
},
}
},
}

View File

@ -165,6 +165,7 @@ export default antfu(
'dify-i18n': difyI18n,
},
rules: {
'dify-i18n/no-as-any-in-t': 'error',
'dify-i18n/no-legacy-namespace-prefix': 'error',
'dify-i18n/require-ns-option': 'error',
},

View File

@ -1,21 +1,25 @@
import type { I18nKeysByPrefix } from '@/types/i18n'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
type IndexingTechnique = I18nKeysByPrefix<'dataset', 'indexingTechnique.'>
type IndexingMethod = I18nKeysByPrefix<'dataset', 'indexingMethod.'>
export const useKnowledge = () => {
const { t } = useTranslation()
const formatIndexingTechnique = useCallback((indexingTechnique: string) => {
return t(`indexingTechnique.${indexingTechnique}` as any, { ns: 'dataset' }) as string
const formatIndexingTechnique = useCallback((indexingTechnique: IndexingTechnique) => {
return t(`indexingTechnique.${indexingTechnique}`, { ns: 'dataset' }) as string
}, [t])
const formatIndexingMethod = useCallback((indexingMethod: string, isEco?: boolean) => {
const formatIndexingMethod = useCallback((indexingMethod: IndexingMethod, isEco?: boolean) => {
if (isEco)
return t('indexingMethod.invertedIndex', { ns: 'dataset' })
return t(`indexingMethod.${indexingMethod}` as any, { ns: 'dataset' }) as string
return t(`indexingMethod.${indexingMethod}`, { ns: 'dataset' }) as string
}, [t])
const formatIndexingTechniqueAndMethod = useCallback((indexingTechnique: string, indexingMethod: string) => {
const formatIndexingTechniqueAndMethod = useCallback((indexingTechnique: IndexingTechnique, indexingMethod: IndexingMethod) => {
let result = formatIndexingTechnique(indexingTechnique)
if (indexingMethod)

View File

@ -1,5 +1,6 @@
'use client'
import type { DocType } from '@/models/datasets'
import type { I18nKeysByPrefix } from '@/types/i18n'
import { useTranslation } from 'react-i18next'
import useTimestamp from '@/hooks/use-timestamp'
import { ChunkingMode } from '@/models/datasets'
@ -86,7 +87,7 @@ export const useMetadataMap = (): MetadataMap => {
},
'volume/issue/page_numbers': { label: t(`${fieldPrefix}.paper.volumeIssuePage`, { ns: 'datasetDocuments' }) },
'doi': { label: t(`${fieldPrefix}.paper.DOI`, { ns: 'datasetDocuments' }) },
'topic/keywords': { label: t(`${fieldPrefix}.paper.topicKeywords` as any, { ns: 'datasetDocuments' }) as string },
'topic/keywords': { label: t(`${fieldPrefix}.paper.topicsKeywords`, { ns: 'datasetDocuments' }) },
'abstract': {
label: t(`${fieldPrefix}.paper.abstract`, { ns: 'datasetDocuments' }),
inputType: 'textarea',
@ -160,7 +161,7 @@ export const useMetadataMap = (): MetadataMap => {
'end_date': { label: t(`${fieldPrefix}.IMChat.endDate`, { ns: 'datasetDocuments' }) },
'participants': { label: t(`${fieldPrefix}.IMChat.participants`, { ns: 'datasetDocuments' }) },
'topicKeywords': {
label: t(`${fieldPrefix}.IMChat.topicKeywords` as any, { ns: 'datasetDocuments' }) as string,
label: t(`${fieldPrefix}.IMChat.topicsKeywords`, { ns: 'datasetDocuments' }),
inputType: 'textarea',
},
'fileType': { label: t(`${fieldPrefix}.IMChat.fileType`, { ns: 'datasetDocuments' }) },
@ -193,7 +194,7 @@ export const useMetadataMap = (): MetadataMap => {
allowEdit: false,
subFieldsMap: {
'title': { label: t(`${fieldPrefix}.notion.title`, { ns: 'datasetDocuments' }) },
'language': { label: t(`${fieldPrefix}.notion.lang` as any, { ns: 'datasetDocuments' }) as string, inputType: 'select' },
'language': { label: t(`${fieldPrefix}.notion.language`, { ns: 'datasetDocuments' }), inputType: 'select' },
'author/creator': { label: t(`${fieldPrefix}.notion.author`, { ns: 'datasetDocuments' }) },
'creation_date': { label: t(`${fieldPrefix}.notion.createdTime`, { ns: 'datasetDocuments' }) },
'last_modified_date': {
@ -201,7 +202,7 @@ export const useMetadataMap = (): MetadataMap => {
},
'notion_page_link': { label: t(`${fieldPrefix}.notion.url`, { ns: 'datasetDocuments' }) },
'category/tags': { label: t(`${fieldPrefix}.notion.tag`, { ns: 'datasetDocuments' }) },
'description': { label: t(`${fieldPrefix}.notion.desc` as any, { ns: 'datasetDocuments' }) as string },
'description': { label: t(`${fieldPrefix}.notion.description`, { ns: 'datasetDocuments' }) },
},
},
synced_from_github: {
@ -241,7 +242,7 @@ export const useMetadataMap = (): MetadataMap => {
},
'data_source_type': {
label: t(`${fieldPrefix}.originInfo.source`, { ns: 'datasetDocuments' }),
render: value => t(`metadata.source.${value === 'notion_import' ? 'notion' : value}` as any, { ns: 'datasetDocuments' }) as string,
render: (value: I18nKeysByPrefix<'datasetDocuments', 'metadata.source.'> | 'notion_import') => t(`metadata.source.${value === 'notion_import' ? 'notion' : value}`, { ns: 'datasetDocuments' }),
},
},
},
@ -323,7 +324,7 @@ export const useLanguages = () => {
cs: t(`${langPrefix}cs`, { ns: 'datasetDocuments' }),
th: t(`${langPrefix}th`, { ns: 'datasetDocuments' }),
id: t(`${langPrefix}id`, { ns: 'datasetDocuments' }),
ro: t(`${langPrefix}ro` as any, { ns: 'datasetDocuments' }) as string,
ro: t(`${langPrefix}ro`, { ns: 'datasetDocuments' }),
}
}

View File

@ -36,7 +36,7 @@ import tools from '../i18n/en-US/tools.json'
import workflow from '../i18n/en-US/workflow.json'
// @keep-sorted
export const namespaces = {
export const resources = {
app,
appAnnotation,
appApi,
@ -79,7 +79,8 @@ export type CamelCase<S extends string> = S extends `${infer T}-${infer U}`
? `${T}${Capitalize<CamelCase<U>>}`
: S
export type NamespaceCamelCase = keyof typeof namespaces
export type Resources = typeof resources
export type NamespaceCamelCase = keyof Resources
export type NamespaceKebabCase = KebabCase<NamespaceCamelCase>
const requireSilent = async (lang: Locale, namespace: NamespaceKebabCase) => {
@ -94,7 +95,7 @@ const requireSilent = async (lang: Locale, namespace: NamespaceKebabCase) => {
return res
}
const NAMESPACES = Object.keys(namespaces).map(kebabCase) as NamespaceKebabCase[]
const NAMESPACES = Object.keys(resources).map(kebabCase) as NamespaceKebabCase[]
// Load a single namespace for a language
export const loadNamespace = async (lang: Locale, ns: NamespaceKebabCase) => {
@ -116,7 +117,7 @@ export const loadLangResources = async (lang: Locale) => {
// Initial resources: load en-US namespaces for fallback/default locale
const getInitialTranslations = () => {
return {
'en-US': namespaces,
'en-US': resources,
}
}
@ -126,7 +127,7 @@ if (!i18n.isInitialized) {
fallbackLng: 'en-US',
resources: getInitialTranslations(),
defaultNS: 'common',
ns: Object.keys(namespaces),
ns: Object.keys(resources),
keySeparator: false,
})
}

View File

@ -20,6 +20,7 @@
"plans.community.includesTitle": "Free Features:",
"plans.community.name": "Community",
"plans.community.price": "Free",
"plans.community.priceTip": "",
"plans.enterprise.btnText": "Contact Sales",
"plans.enterprise.description": "For enterprise requiring organization-grade security, compliance, scalability, control and custom solutions",
"plans.enterprise.features": [

View File

@ -247,6 +247,7 @@
"metadata.languageMap.no": "Norwegian",
"metadata.languageMap.pl": "Polish",
"metadata.languageMap.pt": "Portuguese",
"metadata.languageMap.ro": "Romanian",
"metadata.languageMap.ru": "Russian",
"metadata.languageMap.sv": "Swedish",
"metadata.languageMap.th": "Thai",

View File

@ -154,6 +154,8 @@
"retrieval.hybrid_search.description": "Execute full-text search and vector searches simultaneously, re-rank to select the best match for the user's query. Users can choose to set weights or configure to a Rerank model.",
"retrieval.hybrid_search.recommend": "Recommend",
"retrieval.hybrid_search.title": "Hybrid Search",
"retrieval.invertedIndex.description": "Inverted Index is a structure used for efficient retrieval. Organized by terms, each term points to documents or web pages containing it.",
"retrieval.invertedIndex.title": "Inverted Index",
"retrieval.keyword_search.description": "Inverted Index is a structure used for efficient retrieval. Organized by terms, each term points to documents or web pages containing it.",
"retrieval.keyword_search.title": "Inverted Index",
"retrieval.semantic_search.description": "Generate query embeddings and search for the text chunk most similar to its vector representation.",

View File

@ -12,6 +12,7 @@
"category.Entertainment": "Entertainment",
"category.HR": "HR",
"category.Programming": "Programming",
"category.Recommended": "Recommended",
"category.Translate": "Translate",
"category.Workflow": "Workflow",
"category.Writing": "Writing",

View File

@ -971,6 +971,7 @@
"singleRun.startRun": "Start Run",
"singleRun.testRun": "Test Run",
"singleRun.testRunIteration": "Test Run Iteration",
"singleRun.testRunLoop": "Test Run Loop",
"tabs.addAll": "Add all",
"tabs.agent": "Agent Strategy",
"tabs.allAdded": "All added",

View File

@ -5,6 +5,7 @@ import type { IndexingType } from '@/app/components/datasets/create/step-two'
import type { MetadataItemWithValue } from '@/app/components/datasets/metadata/types'
import type { MetadataFilteringVariableType } from '@/app/components/workflow/nodes/knowledge-retrieval/types'
import type { AppIconType, AppModeEnum, RetrievalConfig, TransferMethod } from '@/types/app'
import type { I18nKeysByPrefix } from '@/types/i18n'
import { ExternalKnowledgeBase, General, ParentChild, Qa } from '@/app/components/base/icons/src/public/knowledge/dataset-card'
import { GeneralChunk, ParentChildChunk, QuestionAndAnswer } from '@/app/components/base/icons/src/vender/knowledge'
@ -804,7 +805,9 @@ export const DOC_FORM_ICON: Record<ChunkingMode.text | ChunkingMode.qa | Chunkin
[ChunkingMode.parentChild]: ParentChildChunk,
}
export const DOC_FORM_TEXT: Record<ChunkingMode, string> = {
type ChunkingModeText = I18nKeysByPrefix<'dataset', 'chunkingMode.'>
export const DOC_FORM_TEXT: Record<ChunkingMode, ChunkingModeText> = {
[ChunkingMode.text]: 'general',
[ChunkingMode.qa]: 'qa',
[ChunkingMode.parentChild]: 'parentChild',

18
web/types/i18n.d.ts vendored
View File

@ -1,11 +1,25 @@
import type { namespaces } from '../i18n-config/i18next-config'
import type { NamespaceCamelCase, Resources } from '../i18n-config/i18next-config'
import 'i18next'
declare module 'i18next' {
// eslint-disable-next-line ts/consistent-type-definitions
interface CustomTypeOptions {
defaultNS: 'common'
resources: typeof namespaces
resources: Resources
keySeparator: false
}
}
export type I18nKeysByPrefix<
NS extends NamespaceCamelCase,
Prefix extends string = '',
> = keyof Resources[NS] extends infer K
? K extends `${Prefix}${infer Rest}`
? Rest
: never
: never
export type I18nKeysWithPrefix<
NS extends NamespaceCamelCase,
Prefix extends string = '',
> = Extract<keyof Resources[NS], `${Prefix}${string}`>

View File

@ -1,4 +1,5 @@
import type { InputVar } from '@/app/components/workflow/types'
import type { I18nKeysByPrefix } from '@/types/i18n'
import {
CONTEXT_PLACEHOLDER_TEXT,
HISTORY_PLACEHOLDER_TEXT,
@ -49,7 +50,9 @@ export const getNewVarInWorkflow = (key: string, type = InputVarType.textInput):
}
}
export const checkKey = (key: string, canBeEmpty?: boolean, _keys?: string[]) => {
export type VarKeyErrorMessageKey = I18nKeysByPrefix<'appDebug', 'varKeyError.'>
export const checkKey = (key: string, canBeEmpty?: boolean, _keys?: string[]): true | VarKeyErrorMessageKey => {
if (key.length === 0 && !canBeEmpty)
return 'canNoBeEmpty'
@ -68,10 +71,14 @@ export const checkKey = (key: string, canBeEmpty?: boolean, _keys?: string[]) =>
return 'notValid'
}
export const checkKeys = (keys: string[], canBeEmpty?: boolean) => {
type CheckKeysResult
= | { isValid: true, errorKey: '', errorMessageKey: '' }
| { isValid: false, errorKey: string, errorMessageKey: VarKeyErrorMessageKey }
export const checkKeys = (keys: string[], canBeEmpty?: boolean): CheckKeysResult => {
let isValid = true
let errorKey = ''
let errorMessageKey = ''
let errorMessageKey: VarKeyErrorMessageKey | '' = ''
keys.forEach((key) => {
if (!isValid)
return
@ -83,7 +90,7 @@ export const checkKeys = (keys: string[], canBeEmpty?: boolean) => {
errorMessageKey = res
}
})
return { isValid, errorKey, errorMessageKey }
return { isValid, errorKey, errorMessageKey } as CheckKeysResult
}
export const hasDuplicateStr = (strArr: string[]) => {