diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view.tsx index 31ec7ecfdb..939e4e9fe6 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view.tsx @@ -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 = ({ 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 = ({ appId, isInPanel, className }) => { notify({ type, - message: t(`actionMsg.${message}` as any, { ns: 'common' }) as string, + message: t(`actionMsg.${message}`, { ns: 'common' }) as string, }) } diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/chart-view.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/chart-view.tsx index ef6a501f2d..b6e902f456 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/chart-view.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/chart-view.tsx @@ -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' }, diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/long-time-range-picker.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/long-time-range-picker.tsx index 3dfabae800..f7178d7ac2 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/long-time-range-picker.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/long-time-range-picker.tsx @@ -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 = ({ return ( ({ 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} diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/index.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/index.tsx index 469bc97737..10209de97b 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/index.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/index.tsx @@ -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 } diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/range-selector.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/range-selector.tsx index 2d9d3c6c30..986170728f 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/range-selector.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/range-selector.tsx @@ -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 = ({ }, []) return ( ({ ...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} diff --git a/web/app/components/app-sidebar/dataset-info/index.tsx b/web/app/components/app-sidebar/dataset-info/index.tsx index 5589361e2c..2d2eeefbb2 100644 --- a/web/app/components/app-sidebar/dataset-info/index.tsx +++ b/web/app/components/app-sidebar/dataset-info/index.tsx @@ -73,7 +73,7 @@ const DatasetInfo: FC = ({ {isExternalProvider && t('externalTag', { ns: 'dataset' })} {!isExternalProvider && isPipelinePublished && dataset.doc_form && dataset.indexing_technique && (
- {t(`chunkingMode.${DOC_FORM_TEXT[dataset.doc_form]}` as any, { ns: 'dataset' }) as string} + {t(`chunkingMode.${DOC_FORM_TEXT[dataset.doc_form]}`, { ns: 'dataset' })} {formatIndexingTechniqueAndMethod(dataset.indexing_technique, dataset.retrieval_model_dict?.search_method)}
)} diff --git a/web/app/components/app-sidebar/dataset-sidebar-dropdown.tsx b/web/app/components/app-sidebar/dataset-sidebar-dropdown.tsx index 8da48c339d..4ba9814255 100644 --- a/web/app/components/app-sidebar/dataset-sidebar-dropdown.tsx +++ b/web/app/components/app-sidebar/dataset-sidebar-dropdown.tsx @@ -116,7 +116,7 @@ const DatasetSidebarDropdown = ({ {isExternalProvider && t('externalTag', { ns: 'dataset' })} {!isExternalProvider && dataset.doc_form && dataset.indexing_technique && (
- {t(`chunkingMode.${DOC_FORM_TEXT[dataset.doc_form]}` as any, { ns: 'dataset' }) as string} + {t(`chunkingMode.${DOC_FORM_TEXT[dataset.doc_form]}`, { ns: 'dataset' })} {formatIndexingTechniqueAndMethod(dataset.indexing_technique, dataset.retrieval_model_dict?.search_method)}
)} diff --git a/web/app/components/app/app-publisher/index.tsx b/web/app/components/app/app-publisher/index.tsx index 302ced9c60..0a026a680b 100644 --- a/web/app/components/app/app-publisher/index.tsx +++ b/web/app/components/app/app-publisher/index.tsx @@ -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 = { +type AccessModeLabel = I18nKeysByPrefix<'app', 'accessControlDialog.accessItems.'> + +const ACCESS_MODE_MAP: Record = { [AccessMode.ORGANIZATION]: { label: 'organization', icon: RiBuildingLine, @@ -84,7 +87,7 @@ const AccessModeDisplay: React.FC<{ mode?: AccessMode }> = ({ mode }) => { <>
- {t(`accessControlDialog.accessItems.${label}` as any, { ns: 'app' }) as string} + {t(`accessControlDialog.accessItems.${label}`, { ns: 'app' })}
) diff --git a/web/app/components/app/configuration/config-var/config-modal/index.tsx b/web/app/components/app/configuration/config-var/config-modal/index.tsx index f14ba4db88..e5d8df34b8 100644 --- a/web/app/components/app/configuration/config-var/config-modal/index.tsx +++ b/web/app/components/app/configuration/config-var/config-modal/index.tsx @@ -96,7 +96,7 @@ const ConfigModal: FC = ({ 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 = ({ 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 } diff --git a/web/app/components/app/configuration/config-var/select-type-item/index.tsx b/web/app/components/app/configuration/config-var/select-type-item/index.tsx index fd87fc005f..e6ae34664f 100644 --- a/web/app/components/app/configuration/config-var/select-type-item/index.tsx +++ b/web/app/components/app/configuration/config-var/select-type-item/index.tsx @@ -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 = { +type VariableConfigTypeKey = I18nKeysByPrefix<'appDebug', 'variableConfig.'> + +const i18nTypeMap: Partial> = { 'file': 'single-file', 'file-list': 'multi-files', } @@ -23,7 +26,8 @@ const SelectTypeItem: FC = ({ 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 (
= ({ icon: RiGitCommitLine, key: 'GitGud', }, - ] + ] as const // eslint-disable-next-line sonarjs/no-nested-template-literals, sonarjs/no-nested-conditional const [instructionFromSessionStorage, setInstruction] = useSessionStorageState(`improve-instruction-${flowId}${isBasicMode ? '' : `-${nodeId}${editorId ? `-${editorId}` : ''}`}`) const instruction = instructionFromSessionStorage || '' const [ideaOutput, setIdeaOutput] = useState('') + 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 = ({ ))} diff --git a/web/app/components/app/log/filter.tsx b/web/app/components/app/log/filter.tsx index cf4677c835..b5e0e0cf09 100644 --- a/web/app/components/app/log/filter.tsx +++ b/web/app/components/app/log/filter.tsx @@ -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 = ({ 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' }) }))} /> - onToggleResult?: (err: Error | null, message?: string) => void + onToggleResult?: (err: Error | null, message?: I18nKeysByPrefix<'common', 'actionMsg.'>) => void } const getTriggerIcon = (trigger: AppTrigger, triggerPlugins: any[]) => { diff --git a/web/app/components/app/workflow-log/filter.tsx b/web/app/components/app/workflow-log/filter.tsx index 8a1c206794..90c78e6c9d 100644 --- a/web/app/components/app/workflow-log/filter.tsx +++ b/web/app/components/app/workflow-log/filter.tsx @@ -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 = ({ 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' }) }))} /> = ({ 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 } diff --git a/web/app/components/base/date-and-time-picker/hooks.ts b/web/app/components/base/date-and-time-picker/hooks.ts index 2578e4ccb5..caaf6767cd 100644 --- a/web/app/components/base/date-and-time-picker/hooks.ts +++ b/web/app/components/base/date-and-time-picker/hooks.ts @@ -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 = () => { diff --git a/web/app/components/base/encrypted-bottom/index.tsx b/web/app/components/base/encrypted-bottom/index.tsx index 5c821c65c4..5a9bc9b488 100644 --- a/web/app/components/base/encrypted-bottom/index.tsx +++ b/web/app/components/base/encrypted-bottom/index.tsx @@ -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 (
- {t((frontTextKey || 'provider.encrypted.front') as any, { ns: 'common' })} + {t(frontTextKey, { ns: 'common' })} { > PKCS1_OAEP - {t((backTextKey || 'provider.encrypted.back') as any, { ns: 'common' })} + {t(backTextKey, { ns: 'common' })}
) } diff --git a/web/app/components/base/features/new-feature-panel/text-to-speech/param-config-content.tsx b/web/app/components/base/features/new-feature-panel/text-to-speech/param-config-content.tsx index 54d7e695a1..f2485628a6 100644 --- a/web/app/components/base/features/new-feature-panel/text-to-speech/param-config-content.tsx +++ b/web/app/components/base/features/new-feature-panel/text-to-speech/param-config-content.tsx @@ -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" > - {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} - {t(`voice.language.${(item.value).toString().replace('-', '')}` as any, { ns: 'common' }) as string} + {t(`voice.language.${(item.value).toString().replace('-', '')}` as VoiceLanguageKey, { ns: 'common' })} {(selected || item.value === text2speech?.language) && ( = { +type VariableConfigKeySuffix = I18nKeysByPrefix<'appDebug', 'variableConfig.'> + +const i18nFileTypeMap: Partial> = { '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], } diff --git a/web/app/components/base/tag-input/index.tsx b/web/app/components/base/tag-input/index.tsx index 3dc45406d5..e291842a2a 100644 --- a/web/app/components/base/tag-input/index.tsx +++ b/web/app/components/base/tag-input/index.tsx @@ -127,7 +127,7 @@ const TagInput: FC = ({ 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' }))} />
) diff --git a/web/app/components/billing/pricing/plans/cloud-plan-item/index.tsx b/web/app/components/billing/pricing/plans/cloud-plan-item/index.tsx index a3c02878b8..b694dc57e2 100644 --- a/web/app/components/billing/pricing/plans/cloud-plan-item/index.tsx +++ b/web/app/components/billing/pricing/plans/cloud-plan-item/index.tsx @@ -35,7 +35,7 @@ const CloudPlanItem: FC = ({ }) => { 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 = ({ {ICON_MAP[plan]}
-
{t(`${i18nPrefix}.name` as any, { ns: 'billing' }) as string}
+
{t(`${i18nPrefix}.name`, { ns: 'billing' })}
{ isMostPopularPlan && (
@@ -117,7 +117,7 @@ const CloudPlanItem: FC = ({ ) }
-
{t(`${i18nPrefix}.description` as any, { ns: 'billing' }) as string}
+
{t(`${i18nPrefix}.description`, { ns: 'billing' })}
{/* Price */} diff --git a/web/app/components/billing/pricing/plans/self-hosted-plan-item/button.tsx b/web/app/components/billing/pricing/plans/self-hosted-plan-item/button.tsx index 2259eacc1c..9412b87a6a 100644 --- a/web/app/components/billing/pricing/plans/self-hosted-plan-item/button.tsx +++ b/web/app/components/billing/pricing/plans/self-hosted-plan-item/button.tsx @@ -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} >
- {t(`${i18nPrefix}.btnText` as any, { ns: 'billing' }) as string} + {t(`${i18nPrefix}.btnText`, { ns: 'billing' })} {isPremiumPlan && ( diff --git a/web/app/components/billing/pricing/plans/self-hosted-plan-item/index.tsx b/web/app/components/billing/pricing/plans/self-hosted-plan-item/index.tsx index 137a69ac4e..eaee5082ff 100644 --- a/web/app/components/billing/pricing/plans/self-hosted-plan-item/index.tsx +++ b/web/app/components/billing/pricing/plans/self-hosted-plan-item/index.tsx @@ -47,7 +47,7 @@ const SelfHostedPlanItem: FC = ({ 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 = ({
{STYLE_MAP[plan].icon}
-
{t(`${i18nPrefix}.name` as any, { ns: 'billing' }) as string}
-
{t(`${i18nPrefix}.description` as any, { ns: 'billing' }) as string}
+
{t(`${i18nPrefix}.name`, { ns: 'billing' })}
+
{t(`${i18nPrefix}.description`, { ns: 'billing' })}
{/* Price */}
-
{t(`${i18nPrefix}.price` as any, { ns: 'billing' }) as string}
+
{t(`${i18nPrefix}.price`, { ns: 'billing' })}
{!isFreePlan && ( - {t(`${i18nPrefix}.priceTip` as any, { ns: 'billing' }) as string} + {t(`${i18nPrefix}.priceTip`, { ns: 'billing' })} )}
diff --git a/web/app/components/billing/pricing/plans/self-hosted-plan-item/list/index.tsx b/web/app/components/billing/pricing/plans/self-hosted-plan-item/list/index.tsx index 514ed5699a..131a28d07b 100644 --- a/web/app/components/billing/pricing/plans/self-hosted-plan-item/list/index.tsx +++ b/web/app/components/billing/pricing/plans/self-hosted-plan-item/list/index.tsx @@ -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 (
}} />
diff --git a/web/app/components/billing/priority-label/index.tsx b/web/app/components/billing/priority-label/index.tsx index 75191c1c27..1d130c8b32 100644 --- a/web/app/components/billing/priority-label/index.tsx +++ b/web/app/components/billing/priority-label/index.tsx @@ -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 ( -
{`${t('plansCommon.documentProcessingPriority', { ns: 'billing' })}: ${t(`plansCommon.priority.${priority}` as any, { ns: 'billing' }) as string}`}
+
+ {t('plansCommon.documentProcessingPriority', { ns: 'billing' })} + : + {' '} + {t(`plansCommon.priority.${priority}`, { ns: 'billing' })} +
{ priority !== DocumentProcessingPriority.topPriority && (
{t('plansCommon.documentProcessingPriorityTip', { ns: 'billing' })}
@@ -51,7 +58,7 @@ const PriorityLabel = ({ className }: PriorityLabelProps) => { ) } - {t(`plansCommon.priority.${priority}` as any, { ns: 'billing' }) as string} + {t(`plansCommon.priority.${priority}`, { ns: 'billing' })}
) diff --git a/web/app/components/billing/upgrade-btn/index.tsx b/web/app/components/billing/upgrade-btn/index.tsx index b23ab84580..79c9ba52a1 100644 --- a/web/app/components/billing/upgrade-btn/index.tsx +++ b/web/app/components/billing/upgrade-btn/index.tsx @@ -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 = ({ @@ -47,7 +48,7 @@ const UpgradeBtn: FC = ({ } 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 ( diff --git a/web/app/components/datasets/common/retrieval-method-info/index.tsx b/web/app/components/datasets/common/retrieval-method-info/index.tsx index a88eb23c07..398b79975f 100644 --- a/web/app/components/datasets/common/retrieval-method-info/index.tsx +++ b/web/app/components/datasets/common/retrieval-method-info/index.tsx @@ -33,8 +33,8 @@ const EconomicalRetrievalMethodConfig: FC = ({
- {t(`chunkingMode.${DOC_FORM_TEXT[chunkStructure]}` as any, { ns: 'dataset' }) as string} + {t(`chunkingMode.${DOC_FORM_TEXT[chunkStructure]}`, { ns: 'dataset' })}
diff --git a/web/app/components/datasets/create/embedding-process/index.tsx b/web/app/components/datasets/create/embedding-process/index.tsx index fd5fb2ff42..aa1f6cee50 100644 --- a/web/app/components/datasets/create/embedding-process/index.tsx +++ b/web/app/components/datasets/create/embedding-process/index.tsx @@ -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<{ = React.memo(({ /> {icon} -
{t(`retrieval.${retrievalMethod}.title` as any, { ns: 'dataset' }) as string}
+
{t(`retrieval.${retrievalMethod}.title`, { ns: 'dataset' })}
)} diff --git a/web/app/components/datasets/list/dataset-card/index.tsx b/web/app/components/datasets/list/dataset-card/index.tsx index d3017b7f56..99404b0454 100644 --- a/web/app/components/datasets/list/dataset-card/index.tsx +++ b/web/app/components/datasets/list/dataset-card/index.tsx @@ -217,9 +217,9 @@ const DatasetCard = ({ {dataset.doc_form && ( - {t(`chunkingMode.${DOC_FORM_TEXT[dataset.doc_form]}` as any, { ns: 'dataset' }) as string} + {t(`chunkingMode.${DOC_FORM_TEXT[dataset.doc_form]}`, { ns: 'dataset' })} )} {dataset.indexing_technique && ( diff --git a/web/app/components/explore/category.tsx b/web/app/components/explore/category.tsx index 57728def27..97a9ca92b3 100644 --- a/web/app/components/explore/category.tsx +++ b/web/app/components/explore/category.tsx @@ -48,7 +48,7 @@ const Category: FC = ({ 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} ))} diff --git a/web/app/components/header/account-setting/members-page/invite-modal/index.tsx b/web/app/components/header/account-setting/members-page/invite-modal/index.tsx index 01a7e9a048..3c3a1a8eff 100644 --- a/web/app/components/header/account-setting/members-page/invite-modal/index.tsx +++ b/web/app/components/header/account-setting/members-page/invite-modal/index.tsx @@ -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('normal') + const [role, setRole] = useState('normal') const [isSubmitting, { setTrue: setIsSubmitting, diff --git a/web/app/components/header/account-setting/members-page/invite-modal/role-selector.tsx b/web/app/components/header/account-setting/members-page/invite-modal/role-selector.tsx index 3a15715769..912fb339a1 100644 --- a/web/app/components/header/account-setting/members-page/invite-modal/role-selector.tsx +++ b/web/app/components/header/account-setting/members-page/invite-modal/role-selector.tsx @@ -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 ( { className="block" >
-
{t('members.invitedAsRole', { ns: 'common', role: t(`members.${toHump(value)}` as any, { ns: 'common' }) })}
+
{t('members.invitedAsRole', { ns: 'common', role: t(roleI18nKeyMap[value], { ns: 'common' }) })}
diff --git a/web/app/components/header/account-setting/members-page/operation/index.tsx b/web/app/components/header/account-setting/members-page/operation/index.tsx index ccc8502951..88c8e250ea 100644 --- a/web/app/components/header/account-setting/members-page/operation/index.tsx +++ b/web/app/components/header/account-setting/members-page/operation/index.tsx @@ -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 = ({ :
}
-
{t(`members.${toHump(role)}` as any, { ns: 'common' })}
-
{t(`members.${toHump(role)}Tip` as any, { ns: 'common' })}
+
{t(roleI18nKeyMap[role].label, { ns: 'common' })}
+
{t(roleI18nKeyMap[role].tip, { ns: 'common' })}
)) diff --git a/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/presets-parameter.tsx b/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/presets-parameter.tsx index 89c1efb414..35e15a29f4 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/presets-parameter.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/presets-parameter.tsx @@ -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 = ({ text: (
{getToneIcon(tone.id)} - {t(`model.tone.${tone.name}` as any, { ns: 'common' }) as string} + {t(toneI18nKeyMap[tone.name], { ns: 'common' })}
), } diff --git a/web/app/components/plugins/base/deprecation-notice.tsx b/web/app/components/plugins/base/deprecation-notice.tsx index 05117bda90..ef59dc3645 100644 --- a/web/app/components/plugins/base/deprecation-notice.tsx +++ b/web/app/components/plugins/base/deprecation-notice.tsx @@ -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 = ({ status, deprecatedReason, @@ -37,19 +44,15 @@ const DeprecationNotice: FC = ({ 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 = ({ ), }} 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 = ({ { hasValidDeprecatedReason && !alternativePluginId && ( - {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' }) : '' })} ) } diff --git a/web/app/components/workflow/run/loop-result-panel.tsx b/web/app/components/workflow/run/loop-result-panel.tsx index b55867cb06..987222f024 100644 --- a/web/app/components/workflow/run/loop-result-panel.tsx +++ b/web/app/components/workflow/run/loop-result-panel.tsx @@ -43,7 +43,7 @@ const LoopResultPanel: FC = ({
- {t(`${i18nPrefix}.testRunLoop` as any, { ns: 'workflow' }) as string} + {t(`${i18nPrefix}.testRunLoop`, { ns: 'workflow' }) }
diff --git a/web/app/forgot-password/ForgotPasswordForm.tsx b/web/app/forgot-password/ForgotPasswordForm.tsx index 0fc4fa9c5d..7299d24ebc 100644 --- a/web/app/forgot-password/ForgotPasswordForm.tsx +++ b/web/app/forgot-password/ForgotPasswordForm.tsx @@ -108,7 +108,7 @@ const ForgotPasswordForm = () => { {...register('email')} placeholder={t('emailPlaceholder', { ns: 'login' }) || ''} /> - {errors.email && {t(`${errors.email?.message}` as any, { ns: 'login' })}} + {errors.email && {t(`${errors.email?.message}` as 'error.emailInValid', { ns: 'login' })}}
)} diff --git a/web/app/install/installForm.tsx b/web/app/install/installForm.tsx index 29f419e8d0..5288ba3ad2 100644 --- a/web/app/install/installForm.tsx +++ b/web/app/install/installForm.tsx @@ -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 && {t(`${errors.email?.message}` as any, { ns: 'login' })}} + {errors.email && {t(`${errors.email?.message}` as 'error.emailInValid', { ns: 'login' })}}
@@ -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" /> - {errors.name && {t(`${errors.name.message}` as any, { ns: 'login' })}} + {errors.name && {t(`${errors.name.message}` as 'error.nameEmpty', { ns: 'login' })}}
diff --git a/web/config/index.ts b/web/config/index.ts index b225c8f62a..b475087d07 100644 --- a/web/config/index.ts +++ b/web/config/index.ts @@ -132,7 +132,7 @@ export const TONE_LIST = [ id: 4, name: 'Custom', }, -] +] as const export const DEFAULT_CHAT_PROMPT_CONFIG = { prompt: [ diff --git a/web/eslint-rules/index.js b/web/eslint-rules/index.js index 9d01838e35..edb6b96ba4 100644 --- a/web/eslint-rules/index.js +++ b/web/eslint-rules/index.js @@ -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, }, diff --git a/web/eslint-rules/rules/no-as-any-in-t.js b/web/eslint-rules/rules/no-as-any-in-t.js new file mode 100644 index 0000000000..4c37eec782 --- /dev/null +++ b/web/eslint-rules/rules/no-as-any-in-t.js @@ -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', + }) + } + }, + } + }, +} diff --git a/web/eslint.config.mjs b/web/eslint.config.mjs index e0cf64d30f..22ecd651ab 100644 --- a/web/eslint.config.mjs +++ b/web/eslint.config.mjs @@ -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', }, diff --git a/web/hooks/use-knowledge.ts b/web/hooks/use-knowledge.ts index fbd001b729..5b40213a49 100644 --- a/web/hooks/use-knowledge.ts +++ b/web/hooks/use-knowledge.ts @@ -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) diff --git a/web/hooks/use-metadata.ts b/web/hooks/use-metadata.ts index 37ae4049dd..9aa20db06d 100644 --- a/web/hooks/use-metadata.ts +++ b/web/hooks/use-metadata.ts @@ -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' }), } } diff --git a/web/i18n-config/i18next-config.ts b/web/i18n-config/i18next-config.ts index 9bb9f72692..7bd2de5b39 100644 --- a/web/i18n-config/i18next-config.ts +++ b/web/i18n-config/i18next-config.ts @@ -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 `${infer T}-${infer U}` ? `${T}${Capitalize>}` : S -export type NamespaceCamelCase = keyof typeof namespaces +export type Resources = typeof resources +export type NamespaceCamelCase = keyof Resources export type NamespaceKebabCase = KebabCase 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, }) } diff --git a/web/i18n/en-US/billing.json b/web/i18n/en-US/billing.json index e5048942a9..1f10a49966 100644 --- a/web/i18n/en-US/billing.json +++ b/web/i18n/en-US/billing.json @@ -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": [ diff --git a/web/i18n/en-US/dataset-documents.json b/web/i18n/en-US/dataset-documents.json index 425c59290b..382d3f645c 100644 --- a/web/i18n/en-US/dataset-documents.json +++ b/web/i18n/en-US/dataset-documents.json @@ -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", diff --git a/web/i18n/en-US/dataset.json b/web/i18n/en-US/dataset.json index 0406fa162b..d798a53074 100644 --- a/web/i18n/en-US/dataset.json +++ b/web/i18n/en-US/dataset.json @@ -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.", diff --git a/web/i18n/en-US/explore.json b/web/i18n/en-US/explore.json index 7ade9422a5..ba8fd9d448 100644 --- a/web/i18n/en-US/explore.json +++ b/web/i18n/en-US/explore.json @@ -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", diff --git a/web/i18n/en-US/workflow.json b/web/i18n/en-US/workflow.json index d954352354..224be01d1b 100644 --- a/web/i18n/en-US/workflow.json +++ b/web/i18n/en-US/workflow.json @@ -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", diff --git a/web/models/datasets.ts b/web/models/datasets.ts index ba61c95b64..989d700f8f 100644 --- a/web/models/datasets.ts +++ b/web/models/datasets.ts @@ -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 = { +type ChunkingModeText = I18nKeysByPrefix<'dataset', 'chunkingMode.'> + +export const DOC_FORM_TEXT: Record = { [ChunkingMode.text]: 'general', [ChunkingMode.qa]: 'qa', [ChunkingMode.parentChild]: 'parentChild', diff --git a/web/types/i18n.d.ts b/web/types/i18n.d.ts index eedc1465cf..038c694eb9 100644 --- a/web/types/i18n.d.ts +++ b/web/types/i18n.d.ts @@ -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 diff --git a/web/utils/var.ts b/web/utils/var.ts index c72310178d..4f572d7768 100644 --- a/web/utils/var.ts +++ b/web/utils/var.ts @@ -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[]) => {