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)