diff --git a/web/eslint-rules/index.js b/web/eslint-rules/index.js deleted file mode 100644 index 75bf7cd092..0000000000 --- a/web/eslint-rules/index.js +++ /dev/null @@ -1,14 +0,0 @@ -import noLegacyNamespacePrefix from './rules/no-legacy-namespace-prefix.js' - -/** @type {import('eslint').ESLint.Plugin} */ -const plugin = { - meta: { - name: 'dify-i18n', - version: '1.0.0', - }, - rules: { - 'no-legacy-namespace-prefix': noLegacyNamespacePrefix, - }, -} - -export default plugin diff --git a/web/eslint-rules/namespaces.js b/web/eslint-rules/namespaces.js deleted file mode 100644 index 77cae65756..0000000000 --- a/web/eslint-rules/namespaces.js +++ /dev/null @@ -1,87 +0,0 @@ -// Auto-generated from i18n-config/i18next-config.ts -// Keep in sync with the namespaces object - -// @keep-sorted -export const NAMESPACES = [ - 'app', - 'appAnnotation', - 'appApi', - 'appDebug', - 'appLog', - 'appOverview', - 'billing', - 'common', - 'custom', - 'dataset', - 'datasetCreation', - 'datasetDocuments', - 'datasetHitTesting', - 'datasetPipeline', - 'datasetSettings', - 'education', - 'explore', - 'layout', - 'login', - 'oauth', - 'pipeline', - 'plugin', - 'pluginTags', - 'pluginTrigger', - 'register', - 'runLog', - 'share', - 'time', - 'tools', - 'workflow', -] - -// Sort by length descending to match longer prefixes first -// e.g., 'datasetDocuments' before 'dataset' -export const NAMESPACES_BY_LENGTH = [...NAMESPACES].sort((a, b) => b.length - a.length) - -/** - * Extract namespace from a translation key - * Returns null if no namespace prefix found or if already in namespace:key format - * @param {string} key - * @returns {{ ns: string, localKey: string } | null} - */ -export function extractNamespace(key) { - // Skip if already in namespace:key format - for (const ns of NAMESPACES_BY_LENGTH) { - if (key.startsWith(`${ns}:`)) { - return null - } - } - // Check for legacy namespace.key format - for (const ns of NAMESPACES_BY_LENGTH) { - if (key.startsWith(`${ns}.`)) { - return { ns, localKey: key.slice(ns.length + 1) } - } - } - return null -} - -/** - * Remove namespace prefix from a string value - * Used for fixing variable declarations - * @param {string} value - * @returns {{ ns: string, newValue: string } | null} - */ -export function removeNamespacePrefix(value) { - // Skip if already in namespace:key format - for (const ns of NAMESPACES_BY_LENGTH) { - if (value.startsWith(`${ns}:`)) { - return null - } - } - // Check for legacy namespace.key format - for (const ns of NAMESPACES_BY_LENGTH) { - if (value.startsWith(`${ns}.`)) { - return { ns, newValue: value.slice(ns.length + 1) } - } - if (value === ns) { - return { ns, newValue: '' } - } - } - return null -} diff --git a/web/eslint-rules/rules/no-legacy-namespace-prefix.js b/web/eslint-rules/rules/no-legacy-namespace-prefix.js deleted file mode 100644 index 2fe96d4e09..0000000000 --- a/web/eslint-rules/rules/no-legacy-namespace-prefix.js +++ /dev/null @@ -1,374 +0,0 @@ -import { extractNamespace, removeNamespacePrefix } from '../namespaces.js' - -/** @type {import('eslint').Rule.RuleModule} */ -export default { - meta: { - type: 'suggestion', - docs: { - description: 'Disallow legacy namespace prefix in i18n translation keys', - }, - fixable: 'code', - schema: [], - messages: { - legacyNamespacePrefix: - 'Translation key "{{key}}" should not include namespace prefix. Use t(\'{{localKey}}\') with useTranslation(\'{{ns}}\') instead.', - legacyNamespacePrefixInVariable: - 'Variable "{{name}}" contains namespace prefix "{{ns}}". Remove the prefix and use useTranslation(\'{{ns}}\') instead.', - }, - }, - create(context) { - const sourceCode = context.sourceCode - - // Track all t() calls to fix - /** @type {Array<{ node: import('estree').CallExpression }>} */ - const tCallsToFix = [] - - // Track variables with namespace prefix - /** @type {Map} */ - const variablesToFix = new Map() - - // Track all namespaces used in the file - /** @type {Set} */ - const namespacesUsed = new Set() - - // Track variable values for template literal analysis - /** @type {Map} */ - const variableValues = new Map() - - /** - * Analyze a template literal and extract namespace info - * @param {import('estree').TemplateLiteral} node - */ - function analyzeTemplateLiteral(node) { - const quasis = node.quasis - const expressions = node.expressions - - const firstQuasi = quasis[0].value.raw - - // Check if first quasi starts with namespace - const extracted = extractNamespace(firstQuasi) - if (extracted) { - const fixedQuasis = [extracted.localKey, ...quasis.slice(1).map(q => q.value.raw)] - return { ns: extracted.ns, canFix: true, fixedQuasis, variableToUpdate: null } - } - - // Check if first expression is a variable with namespace prefix - if (expressions.length > 0 && firstQuasi === '') { - const firstExpr = expressions[0] - if (firstExpr.type === 'Identifier') { - const varValue = variableValues.get(firstExpr.name) - if (varValue) { - const extracted = removeNamespacePrefix(varValue) - if (extracted) { - return { - ns: extracted.ns, - canFix: true, - fixedQuasis: null, - variableToUpdate: { - name: firstExpr.name, - newValue: extracted.newValue, - ns: extracted.ns, - }, - } - } - } - } - } - - return { ns: null, canFix: false, fixedQuasis: null, variableToUpdate: null } - } - - /** - * Build a fixed template literal string - * @param {string[]} quasis - * @param {import('estree').Expression[]} expressions - */ - function buildTemplateLiteral(quasis, expressions) { - let result = '`' - for (let i = 0; i < quasis.length; i++) { - result += quasis[i] - if (i < expressions.length) { - result += `\${${sourceCode.getText(expressions[i])}}` - } - } - result += '`' - return result - } - - /** - * Check if a t() call already has ns in its second argument - * @param {import('estree').CallExpression} node - * @returns {boolean} - */ - function hasNsArgument(node) { - if (node.arguments.length < 2) - return false - const secondArg = node.arguments[1] - if (secondArg.type !== 'ObjectExpression') - return false - return secondArg.properties.some( - prop => prop.type === 'Property' - && prop.key.type === 'Identifier' - && prop.key.name === 'ns', - ) - } - - return { - // Track variable declarations - VariableDeclarator(node) { - if ( - node.id.type === 'Identifier' - && node.init - && node.init.type === 'Literal' - && typeof node.init.value === 'string' - ) { - variableValues.set(node.id.name, node.init.value) - - const extracted = removeNamespacePrefix(node.init.value) - if (extracted) { - variablesToFix.set(node.id.name, { - node, - name: node.id.name, - oldValue: node.init.value, - newValue: extracted.newValue, - ns: extracted.ns, - }) - } - } - }, - - CallExpression(node) { - // Check for t() calls - both direct t() and i18n.t() - const isTCall = ( - node.callee.type === 'Identifier' - && node.callee.name === 't' - ) || ( - node.callee.type === 'MemberExpression' - && node.callee.property.type === 'Identifier' - && node.callee.property.name === 't' - ) - - if (isTCall && node.arguments.length > 0) { - const firstArg = node.arguments[0] - - // Skip if already has ns argument - if (hasNsArgument(node)) - return - - // Case 1: Static string literal - if (firstArg.type === 'Literal' && typeof firstArg.value === 'string') { - const extracted = extractNamespace(firstArg.value) - if (extracted) { - namespacesUsed.add(extracted.ns) - tCallsToFix.push({ node }) - - context.report({ - node: firstArg, - messageId: 'legacyNamespacePrefix', - data: { - key: firstArg.value, - localKey: extracted.localKey, - ns: extracted.ns, - }, - }) - } - } - - // Case 2: Template literal - if (firstArg.type === 'TemplateLiteral') { - const analysis = analyzeTemplateLiteral(firstArg) - if (analysis.ns) { - namespacesUsed.add(analysis.ns) - tCallsToFix.push({ node }) - - if (!analysis.variableToUpdate) { - const firstQuasi = firstArg.quasis[0].value.raw - const extracted = extractNamespace(firstQuasi) - if (extracted) { - context.report({ - node: firstArg, - messageId: 'legacyNamespacePrefix', - data: { - key: `${firstQuasi}...`, - localKey: `${extracted.localKey}...`, - ns: extracted.ns, - }, - }) - } - } - } - } - - // Case 3: Conditional expression - if (firstArg.type === 'ConditionalExpression') { - const consequent = firstArg.consequent - const alternate = firstArg.alternate - let hasNs = false - - if (consequent.type === 'Literal' && typeof consequent.value === 'string') { - const extracted = extractNamespace(consequent.value) - if (extracted) { - hasNs = true - namespacesUsed.add(extracted.ns) - } - } - - if (alternate.type === 'Literal' && typeof alternate.value === 'string') { - const extracted = extractNamespace(alternate.value) - if (extracted) { - hasNs = true - namespacesUsed.add(extracted.ns) - } - } - - if (hasNs) { - tCallsToFix.push({ node }) - - context.report({ - node: firstArg, - messageId: 'legacyNamespacePrefix', - data: { - key: '(conditional)', - localKey: '...', - ns: '...', - }, - }) - } - } - } - }, - - 'Program:exit': function (program) { - if (namespacesUsed.size === 0) - return - - // Report variables with namespace prefix (once per variable) - for (const [, varInfo] of variablesToFix) { - if (namespacesUsed.has(varInfo.ns)) { - context.report({ - node: varInfo.node, - messageId: 'legacyNamespacePrefixInVariable', - data: { - name: varInfo.name, - ns: varInfo.ns, - }, - }) - } - } - - // Report on program with fix - const sortedNamespaces = Array.from(namespacesUsed).sort() - - context.report({ - node: program, - messageId: 'legacyNamespacePrefix', - data: { - key: '(file)', - localKey: '...', - ns: sortedNamespaces.join(', '), - }, - fix(fixer) { - /** @type {import('eslint').Rule.Fix[]} */ - const fixes = [] - - // Fix variable declarations - remove namespace prefix - for (const [, varInfo] of variablesToFix) { - if (namespacesUsed.has(varInfo.ns) && varInfo.node.init) { - fixes.push(fixer.replaceText(varInfo.node.init, `'${varInfo.newValue}'`)) - } - } - - // Fix t() calls - use { ns: 'xxx' } as second argument - for (const { node } of tCallsToFix) { - const firstArg = node.arguments[0] - const secondArg = node.arguments[1] - const hasSecondArg = node.arguments.length >= 2 - - /** - * Add ns to existing object or create new object - * @param {string} ns - */ - const addNsToArgs = (ns) => { - if (hasSecondArg && secondArg.type === 'ObjectExpression') { - // Add ns property to existing object - if (secondArg.properties.length === 0) { - // Empty object: {} -> { ns: 'xxx' } - fixes.push(fixer.replaceText(secondArg, `{ ns: '${ns}' }`)) - } - else { - // Non-empty object: { foo } -> { ns: 'xxx', foo } - const firstProp = secondArg.properties[0] - fixes.push(fixer.insertTextBefore(firstProp, `ns: '${ns}', `)) - } - } - else if (!hasSecondArg) { - // No second argument, add new object - fixes.push(fixer.insertTextAfter(firstArg, `, { ns: '${ns}' }`)) - } - // If second arg exists but is not an object, skip (can't safely add ns) - } - - if (firstArg.type === 'Literal' && typeof firstArg.value === 'string') { - const extracted = extractNamespace(firstArg.value) - if (extracted) { - // Replace key - fixes.push(fixer.replaceText(firstArg, `'${extracted.localKey}'`)) - // Add ns - addNsToArgs(extracted.ns) - } - } - else if (firstArg.type === 'TemplateLiteral') { - const analysis = analyzeTemplateLiteral(firstArg) - if (analysis.canFix && analysis.fixedQuasis) { - // For template literals with namespace prefix directly in template - const newTemplate = buildTemplateLiteral(analysis.fixedQuasis, firstArg.expressions) - fixes.push(fixer.replaceText(firstArg, newTemplate)) - addNsToArgs(analysis.ns) - } - else if (analysis.canFix && analysis.variableToUpdate) { - // Variable's namespace prefix is being removed - const quasis = firstArg.quasis.map(q => q.value.raw) - // If variable becomes empty and next quasi starts with '.', remove the dot - if (analysis.variableToUpdate.newValue === '' && quasis.length > 1 && quasis[1].startsWith('.')) { - quasis[1] = quasis[1].slice(1) - } - const newTemplate = buildTemplateLiteral(quasis, firstArg.expressions) - fixes.push(fixer.replaceText(firstArg, newTemplate)) - addNsToArgs(analysis.ns) - } - } - else if (firstArg.type === 'ConditionalExpression') { - const consequent = firstArg.consequent - const alternate = firstArg.alternate - let ns = null - - if (consequent.type === 'Literal' && typeof consequent.value === 'string') { - const extracted = extractNamespace(consequent.value) - if (extracted) { - ns = extracted.ns - fixes.push(fixer.replaceText(consequent, `'${extracted.localKey}'`)) - } - } - - if (alternate.type === 'Literal' && typeof alternate.value === 'string') { - const extracted = extractNamespace(alternate.value) - if (extracted) { - ns = ns || extracted.ns - fixes.push(fixer.replaceText(alternate, `'${extracted.localKey}'`)) - } - } - - // Add ns argument - if (ns) { - addNsToArgs(ns) - } - } - } - - return fixes - }, - }) - }, - } - }, -} diff --git a/web/eslint.config.mjs b/web/eslint.config.mjs index 7df44f1a40..b085ea3619 100644 --- a/web/eslint.config.mjs +++ b/web/eslint.config.mjs @@ -3,7 +3,6 @@ import antfu from '@antfu/eslint-config' import sonar from 'eslint-plugin-sonarjs' import storybook from 'eslint-plugin-storybook' import tailwind from 'eslint-plugin-tailwindcss' -// import difyI18n from './eslint-rules/index.js' export default antfu( { @@ -156,15 +155,4 @@ export default antfu( 'tailwindcss/migration-from-tailwind-2': 'warn', }, }, - // dify i18n namespace migration - // { - // files: ['**/*.ts', '**/*.tsx'], - // ignores: ['eslint-rules/**', 'i18n/**', 'i18n-config/**'], - // plugins: { - // 'dify-i18n': difyI18n, - // }, - // rules: { - // 'dify-i18n/no-legacy-namespace-prefix': 'error', - // }, - // }, )