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 f2485628a6..41715b3c6b 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 @@ -10,6 +10,7 @@ import { usePathname } from 'next/navigation' import * as React from 'react' import { Fragment } from 'react' import { useTranslation } from 'react-i18next' +import { replace } from 'string-ts' import AudioBtn from '@/app/components/base/audio-btn' import { useFeatures, useFeaturesStore } from '@/app/components/base/features/hooks' import Switch from '@/app/components/base/switch' @@ -100,7 +101,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 VoiceLanguageKey, { ns: 'common' }) : localLanguagePlaceholder} + {languageItem?.name ? t(`voice.language.${replace(languageItem?.value, '-', '')}`, { ns: 'common' }) : localLanguagePlaceholder} - {languages.map((item: Item) => ( + {languages.map(item => ( - {t(`voice.language.${(item.value).toString().replace('-', '')}` as VoiceLanguageKey, { ns: 'common' })} + {t(`voice.language.${replace((item.value), '-', '')}`, { ns: 'common' })} {(selected || item.value === text2speech?.language) && ( -const i18nFileTypeMap: Partial> = { +const i18nFileTypeMap = { + 'text-input': 'text-input', + 'paragraph': 'paragraph', 'number': 'number', + 'select': 'select', + 'checkbox': 'checkbox', 'file': 'single-file', 'file-list': 'multi-files', -} +} satisfies Record const INPUT_TYPE_ICON = { [PipelineInputVarType.textInput]: RiTextSnippet, @@ -48,7 +52,7 @@ export const useInputTypeOptions = (supportFile: boolean) => { return options.map((value) => { return { value, - label: t(`variableConfig.${i18nFileTypeMap[value] || value}` as `variableConfig.${VariableConfigKeySuffix}`, { ns: 'appDebug' }), + label: t(`variableConfig.${i18nFileTypeMap[value]}`, { ns: 'appDebug' }), Icon: INPUT_TYPE_ICON[value], type: DATA_TYPE[value], } diff --git a/web/app/components/workflow-app/hooks/use-available-nodes-meta-data.ts b/web/app/components/workflow-app/hooks/use-available-nodes-meta-data.ts index 032cff54be..60f0bf3b28 100644 --- a/web/app/components/workflow-app/hooks/use-available-nodes-meta-data.ts +++ b/web/app/components/workflow-app/hooks/use-available-nodes-meta-data.ts @@ -1,5 +1,4 @@ import type { AvailableNodesMetaData } from '@/app/components/workflow/hooks-store/store' -import type { I18nKeysWithPrefix } from '@/types/i18n' import { useMemo } from 'react' import { useTranslation } from 'react-i18next' import { WORKFLOW_COMMON_NODES } from '@/app/components/workflow/constants/node' @@ -43,8 +42,8 @@ export const useAvailableNodesMetaData = () => { const availableNodesMetaData = useMemo(() => mergedNodesMetaData.map((node) => { const { metaData } = node - const title = t(`blocks.${metaData.type}` as I18nKeysWithPrefix<'workflow', 'blocks.'>, { ns: 'workflow' }) - const description = t(`blocksAbout.${metaData.type}` as I18nKeysWithPrefix<'workflow', 'blocksAbout.'>, { ns: 'workflow' }) + const title = t(`blocks.${metaData.type}`, { ns: 'workflow' }) + const description = t(`blocksAbout.${metaData.type}`, { ns: 'workflow' }) const helpLinkPath = `guides/workflow/node/${metaData.helpLinkUri}` return { ...node, diff --git a/web/app/components/workflow/hooks/use-checklist.ts b/web/app/components/workflow/hooks/use-checklist.ts index 90d85e610b..5a9e4dacb7 100644 --- a/web/app/components/workflow/hooks/use-checklist.ts +++ b/web/app/components/workflow/hooks/use-checklist.ts @@ -238,7 +238,10 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => { list.push({ id: `${type}-need-added`, type, + // We don't have enough type info for t() here + title: t(`blocks.${type}` as I18nKeysWithPrefix<'workflow', 'blocks.'>, { ns: 'workflow' }), + errorMessage: t('common.needAdd', { ns: 'workflow', node: t(`blocks.${type}` as I18nKeysWithPrefix<'workflow', 'blocks.'>, { ns: 'workflow' }) }), canNavigate: false, }) diff --git a/web/eslint-rules/rules/no-as-any-in-t.js b/web/eslint-rules/rules/no-as-any-in-t.js index 4c37eec782..0eb134a3cf 100644 --- a/web/eslint-rules/rules/no-as-any-in-t.js +++ b/web/eslint-rules/rules/no-as-any-in-t.js @@ -3,15 +3,32 @@ export default { meta: { type: 'problem', docs: { - description: 'Disallow using "as any" type assertion in t() function calls', + description: 'Disallow using type assertions in t() function calls', }, - schema: [], + schema: [ + { + type: 'object', + properties: { + mode: { + type: 'string', + enum: ['any', 'all'], + default: 'any', + }, + }, + additionalProperties: false, + }, + ], messages: { noAsAnyInT: 'Avoid using "as any" in t() function calls. Use proper i18n key types instead.', + noAsInT: + 'Avoid using type assertions in t() function calls. Use proper i18n key types instead.', }, }, create(context) { + const options = context.options[0] || {} + const mode = options.mode || 'any' + /** * Check if this is a t() function call * @param {import('estree').CallExpression} node @@ -45,6 +62,23 @@ export default { ) } + /** + * Check if a node is a TSAsExpression (excluding "as const") + * @param {object} node + * @returns {boolean} + */ + function isAsExpression(node) { + if (node.type !== 'TSAsExpression') + return false + // Ignore "as const" + if (node.typeAnnotation && node.typeAnnotation.type === 'TSTypeReference') { + const typeName = node.typeAnnotation.typeName + if (typeName && typeName.type === 'Identifier' && typeName.name === 'const') + return false + } + return true + } + return { CallExpression(node) { if (!isTCall(node) || node.arguments.length === 0) @@ -52,12 +86,23 @@ export default { const firstArg = node.arguments[0] - // Check if the first argument uses "as any" - if (isAsAny(firstArg)) { - context.report({ - node: firstArg, - messageId: 'noAsAnyInT', - }) + if (mode === 'all') { + // Check for any type assertion + if (isAsExpression(firstArg)) { + context.report({ + node: firstArg, + messageId: 'noAsInT', + }) + } + } + else { + // Check only for "as any" + if (isAsAny(firstArg)) { + context.report({ + node: firstArg, + messageId: 'noAsAnyInT', + }) + } } }, } diff --git a/web/eslint.config.mjs b/web/eslint.config.mjs index 22ecd651ab..89f6d292cd 100644 --- a/web/eslint.config.mjs +++ b/web/eslint.config.mjs @@ -165,8 +165,9 @@ export default antfu( 'dify-i18n': difyI18n, }, rules: { + // 'dify-i18n/no-as-any-in-t': ['error', { mode: 'all' }], 'dify-i18n/no-as-any-in-t': 'error', - 'dify-i18n/no-legacy-namespace-prefix': 'error', + // 'dify-i18n/no-legacy-namespace-prefix': 'error', 'dify-i18n/require-ns-option': 'error', }, }, diff --git a/web/package.json b/web/package.json index da0d1a25db..cad21b1b17 100644 --- a/web/package.json +++ b/web/package.json @@ -138,6 +138,7 @@ "semver": "^7.7.3", "sharp": "^0.33.5", "sortablejs": "^1.15.6", + "string-ts": "^2.3.1", "swr": "^2.3.6", "tailwind-merge": "^2.6.0", "tldts": "^7.0.17", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index ff0ef5e6fa..7971d62958 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -330,6 +330,9 @@ importers: sortablejs: specifier: ^1.15.6 version: 1.15.6 + string-ts: + specifier: ^2.3.1 + version: 2.3.1 swr: specifier: ^2.3.6 version: 2.3.7(react@19.2.3)