| @@ -67,12 +67,22 @@ jobs: | |||
| working-directory: ./web | |||
| run: pnpm run auto-gen-i18n ${{ env.FILE_ARGS }} | |||
| - name: Generate i18n type definitions | |||
| if: env.FILES_CHANGED == 'true' | |||
| working-directory: ./web | |||
| run: pnpm run gen:i18n-types | |||
| - name: Create Pull Request | |||
| if: env.FILES_CHANGED == 'true' | |||
| uses: peter-evans/create-pull-request@v6 | |||
| with: | |||
| token: ${{ secrets.GITHUB_TOKEN }} | |||
| commit-message: Update i18n files based on en-US changes | |||
| title: 'chore: translate i18n files' | |||
| body: This PR was automatically created to update i18n files based on changes in en-US locale. | |||
| commit-message: Update i18n files and type definitions based on en-US changes | |||
| title: 'chore: translate i18n files and update type definitions' | |||
| body: | | |||
| This PR was automatically created to update i18n files and TypeScript type definitions based on changes in en-US locale. | |||
| **Changes included:** | |||
| - Updated translation files for all locales | |||
| - Regenerated TypeScript type definitions for type safety | |||
| branch: chore/automated-i18n-updates | |||
| @@ -47,6 +47,11 @@ jobs: | |||
| working-directory: ./web | |||
| run: pnpm install --frozen-lockfile | |||
| - name: Check i18n types synchronization | |||
| if: steps.changed-files.outputs.any_changed == 'true' | |||
| working-directory: ./web | |||
| run: pnpm run check:i18n-types | |||
| - name: Run tests | |||
| if: steps.changed-files.outputs.any_changed == 'true' | |||
| working-directory: ./web | |||
| @@ -2,6 +2,7 @@ from collections.abc import Generator, Mapping | |||
| from typing import Optional, Union | |||
| from sqlalchemy import select | |||
| from sqlalchemy.orm import Session | |||
| from controllers.service_api.wraps import create_or_update_end_user_for_user_id | |||
| from core.app.app_config.common.parameters_mapping import get_parameters_from_feature_dict | |||
| @@ -193,11 +194,12 @@ class PluginAppBackwardsInvocation(BaseBackwardsInvocation): | |||
| """ | |||
| get the user by user id | |||
| """ | |||
| stmt = select(EndUser).where(EndUser.id == user_id) | |||
| user = db.session.scalar(stmt) | |||
| if not user: | |||
| stmt = select(Account).where(Account.id == user_id) | |||
| user = db.session.scalar(stmt) | |||
| with Session(db.engine, expire_on_commit=False) as session: | |||
| stmt = select(EndUser).where(EndUser.id == user_id) | |||
| user = session.scalar(stmt) | |||
| if not user: | |||
| stmt = select(Account).where(Account.id == user_id) | |||
| user = session.scalar(stmt) | |||
| if not user: | |||
| raise ValueError("user not found") | |||
| @@ -38,6 +38,7 @@ import { | |||
| import { | |||
| genNewNodeTitleFromOld, | |||
| generateNewNode, | |||
| getNestedNodePosition, | |||
| getNodeCustomTypeByNodeDataType, | |||
| getNodesConnectedSourceOrTargetHandleIdsMap, | |||
| getTopLeftNodePosition, | |||
| @@ -1307,8 +1308,7 @@ export const useNodesInteractions = () => { | |||
| }) | |||
| newChildren.push(newIterationStartNode!) | |||
| } | |||
| if (nodeToPaste.data.type === BlockEnum.Loop) { | |||
| else if (nodeToPaste.data.type === BlockEnum.Loop) { | |||
| newLoopStartNode!.parentId = newNode.id; | |||
| (newNode.data as LoopNodeType).start_node_id = newLoopStartNode!.id | |||
| @@ -1318,6 +1318,44 @@ export const useNodesInteractions = () => { | |||
| }) | |||
| newChildren.push(newLoopStartNode!) | |||
| } | |||
| else { | |||
| // single node paste | |||
| const selectedNode = nodes.find(node => node.selected) | |||
| if (selectedNode) { | |||
| const commonNestedDisallowPasteNodes = [ | |||
| // end node only can be placed outermost layer | |||
| BlockEnum.End, | |||
| ] | |||
| // handle disallow paste node | |||
| if (commonNestedDisallowPasteNodes.includes(nodeToPaste.data.type)) | |||
| return | |||
| // handle paste to nested block | |||
| if (selectedNode.data.type === BlockEnum.Iteration) { | |||
| newNode.data.isInIteration = true | |||
| newNode.data.iteration_id = selectedNode.data.iteration_id | |||
| newNode.parentId = selectedNode.id | |||
| newNode.positionAbsolute = { | |||
| x: newNode.position.x, | |||
| y: newNode.position.y, | |||
| } | |||
| // set position base on parent node | |||
| newNode.position = getNestedNodePosition(newNode, selectedNode) | |||
| } | |||
| else if (selectedNode.data.type === BlockEnum.Loop) { | |||
| newNode.data.isInLoop = true | |||
| newNode.data.loop_id = selectedNode.data.loop_id | |||
| newNode.parentId = selectedNode.id | |||
| newNode.positionAbsolute = { | |||
| x: newNode.position.x, | |||
| y: newNode.position.y, | |||
| } | |||
| // set position base on parent node | |||
| newNode.position = getNestedNodePosition(newNode, selectedNode) | |||
| } | |||
| } | |||
| } | |||
| nodesToPaste.push(newNode) | |||
| @@ -1325,6 +1363,7 @@ export const useNodesInteractions = () => { | |||
| nodesToPaste.push(...newChildren) | |||
| }) | |||
| // only handle edge when paste nested block | |||
| edges.forEach((edge) => { | |||
| const sourceId = idMapping[edge.source] | |||
| const targetId = idMapping[edge.target] | |||
| @@ -135,6 +135,13 @@ export const getTopLeftNodePosition = (nodes: Node[]) => { | |||
| } | |||
| } | |||
| export const getNestedNodePosition = (node: Node, parentNode: Node) => { | |||
| return { | |||
| x: node.position.x - parentNode.position.x, | |||
| y: node.position.y - parentNode.position.y, | |||
| } | |||
| } | |||
| export const hasRetryNode = (nodeType?: BlockEnum) => { | |||
| return nodeType === BlockEnum.LLM || nodeType === BlockEnum.Tool || nodeType === BlockEnum.HttpRequest || nodeType === BlockEnum.Code | |||
| } | |||
| @@ -8,3 +8,5 @@ declare module '*.mdx' { | |||
| let MDXComponent: (props: any) => JSX.Element | |||
| export default MDXComponent | |||
| } | |||
| import './types/i18n' | |||
| @@ -0,0 +1,120 @@ | |||
| #!/usr/bin/env node | |||
| const fs = require('fs') | |||
| const path = require('path') | |||
| const { camelCase } = require('lodash') | |||
| // Import the NAMESPACES array from i18next-config.ts | |||
| function getNamespacesFromConfig() { | |||
| const configPath = path.join(__dirname, 'i18next-config.ts') | |||
| const configContent = fs.readFileSync(configPath, 'utf8') | |||
| // Extract NAMESPACES array using regex | |||
| const namespacesMatch = configContent.match(/const NAMESPACES = \[([\s\S]*?)\]/) | |||
| if (!namespacesMatch) { | |||
| throw new Error('Could not find NAMESPACES array in i18next-config.ts') | |||
| } | |||
| // Parse the namespaces | |||
| const namespacesStr = namespacesMatch[1] | |||
| const namespaces = namespacesStr | |||
| .split(',') | |||
| .map(line => line.trim()) | |||
| .filter(line => line.startsWith("'") || line.startsWith('"')) | |||
| .map(line => line.slice(1, -1)) // Remove quotes | |||
| return namespaces | |||
| } | |||
| function getNamespacesFromTypes() { | |||
| const typesPath = path.join(__dirname, '../types/i18n.d.ts') | |||
| if (!fs.existsSync(typesPath)) { | |||
| return null | |||
| } | |||
| const typesContent = fs.readFileSync(typesPath, 'utf8') | |||
| // Extract namespaces from Messages type | |||
| const messagesMatch = typesContent.match(/export type Messages = \{([\s\S]*?)\}/) | |||
| if (!messagesMatch) { | |||
| return null | |||
| } | |||
| // Parse the properties | |||
| const propertiesStr = messagesMatch[1] | |||
| const properties = propertiesStr | |||
| .split('\n') | |||
| .map(line => line.trim()) | |||
| .filter(line => line.includes(':')) | |||
| .map(line => line.split(':')[0].trim()) | |||
| .filter(prop => prop.length > 0) | |||
| return properties | |||
| } | |||
| function main() { | |||
| try { | |||
| console.log('🔍 Checking i18n types synchronization...') | |||
| // Get namespaces from config | |||
| const configNamespaces = getNamespacesFromConfig() | |||
| console.log(`📦 Found ${configNamespaces.length} namespaces in config`) | |||
| // Convert to camelCase for comparison | |||
| const configCamelCase = configNamespaces.map(ns => camelCase(ns)).sort() | |||
| // Get namespaces from type definitions | |||
| const typeNamespaces = getNamespacesFromTypes() | |||
| if (!typeNamespaces) { | |||
| console.error('❌ Type definitions file not found or invalid') | |||
| console.error(' Run: pnpm run gen:i18n-types') | |||
| process.exit(1) | |||
| } | |||
| console.log(`🔧 Found ${typeNamespaces.length} namespaces in types`) | |||
| const typeCamelCase = typeNamespaces.sort() | |||
| // Compare arrays | |||
| const configSet = new Set(configCamelCase) | |||
| const typeSet = new Set(typeCamelCase) | |||
| // Find missing in types | |||
| const missingInTypes = configCamelCase.filter(ns => !typeSet.has(ns)) | |||
| // Find extra in types | |||
| const extraInTypes = typeCamelCase.filter(ns => !configSet.has(ns)) | |||
| let hasErrors = false | |||
| if (missingInTypes.length > 0) { | |||
| hasErrors = true | |||
| console.error('❌ Missing in type definitions:') | |||
| missingInTypes.forEach(ns => console.error(` - ${ns}`)) | |||
| } | |||
| if (extraInTypes.length > 0) { | |||
| hasErrors = true | |||
| console.error('❌ Extra in type definitions:') | |||
| extraInTypes.forEach(ns => console.error(` - ${ns}`)) | |||
| } | |||
| if (hasErrors) { | |||
| console.error('\n💡 To fix synchronization issues:') | |||
| console.error(' Run: pnpm run gen:i18n-types') | |||
| process.exit(1) | |||
| } | |||
| console.log('✅ i18n types are synchronized') | |||
| } catch (error) { | |||
| console.error('❌ Error:', error.message) | |||
| process.exit(1) | |||
| } | |||
| } | |||
| if (require.main === module) { | |||
| main() | |||
| } | |||
| @@ -0,0 +1,135 @@ | |||
| #!/usr/bin/env node | |||
| const fs = require('fs') | |||
| const path = require('path') | |||
| const { camelCase } = require('lodash') | |||
| // Import the NAMESPACES array from i18next-config.ts | |||
| function getNamespacesFromConfig() { | |||
| const configPath = path.join(__dirname, 'i18next-config.ts') | |||
| const configContent = fs.readFileSync(configPath, 'utf8') | |||
| // Extract NAMESPACES array using regex | |||
| const namespacesMatch = configContent.match(/const NAMESPACES = \[([\s\S]*?)\]/) | |||
| if (!namespacesMatch) { | |||
| throw new Error('Could not find NAMESPACES array in i18next-config.ts') | |||
| } | |||
| // Parse the namespaces | |||
| const namespacesStr = namespacesMatch[1] | |||
| const namespaces = namespacesStr | |||
| .split(',') | |||
| .map(line => line.trim()) | |||
| .filter(line => line.startsWith("'") || line.startsWith('"')) | |||
| .map(line => line.slice(1, -1)) // Remove quotes | |||
| return namespaces | |||
| } | |||
| function generateTypeDefinitions(namespaces) { | |||
| const header = `// TypeScript type definitions for Dify's i18next configuration | |||
| // This file is auto-generated. Do not edit manually. | |||
| // To regenerate, run: pnpm run gen:i18n-types | |||
| import 'react-i18next' | |||
| // Extract types from translation files using typeof import pattern` | |||
| // Generate individual type definitions | |||
| const typeDefinitions = namespaces.map(namespace => { | |||
| const typeName = camelCase(namespace).replace(/^\w/, c => c.toUpperCase()) + 'Messages' | |||
| return `type ${typeName} = typeof import('../i18n/en-US/${namespace}').default` | |||
| }).join('\n') | |||
| // Generate Messages interface | |||
| const messagesInterface = ` | |||
| // Complete type structure that matches i18next-config.ts camelCase conversion | |||
| export type Messages = { | |||
| ${namespaces.map(namespace => { | |||
| const camelCased = camelCase(namespace) | |||
| const typeName = camelCase(namespace).replace(/^\w/, c => c.toUpperCase()) + 'Messages' | |||
| return ` ${camelCased}: ${typeName};` | |||
| }).join('\n')} | |||
| }` | |||
| const utilityTypes = ` | |||
| // Utility type to flatten nested object keys into dot notation | |||
| type FlattenKeys<T> = T extends object | |||
| ? { | |||
| [K in keyof T]: T[K] extends object | |||
| ? \`\${K & string}.\${FlattenKeys<T[K]> & string}\` | |||
| : \`\${K & string}\` | |||
| }[keyof T] | |||
| : never | |||
| export type ValidTranslationKeys = FlattenKeys<Messages>` | |||
| const moduleDeclarations = ` | |||
| // Extend react-i18next with Dify's type structure | |||
| declare module 'react-i18next' { | |||
| interface CustomTypeOptions { | |||
| defaultNS: 'translation'; | |||
| resources: { | |||
| translation: Messages; | |||
| }; | |||
| } | |||
| } | |||
| // Extend i18next for complete type safety | |||
| declare module 'i18next' { | |||
| interface CustomTypeOptions { | |||
| defaultNS: 'translation'; | |||
| resources: { | |||
| translation: Messages; | |||
| }; | |||
| } | |||
| }` | |||
| return [header, typeDefinitions, messagesInterface, utilityTypes, moduleDeclarations].join('\n\n') | |||
| } | |||
| function main() { | |||
| const args = process.argv.slice(2) | |||
| const checkMode = args.includes('--check') | |||
| try { | |||
| console.log('📦 Generating i18n type definitions...') | |||
| // Get namespaces from config | |||
| const namespaces = getNamespacesFromConfig() | |||
| console.log(`✅ Found ${namespaces.length} namespaces`) | |||
| // Generate type definitions | |||
| const typeDefinitions = generateTypeDefinitions(namespaces) | |||
| const outputPath = path.join(__dirname, '../types/i18n.d.ts') | |||
| if (checkMode) { | |||
| // Check mode: compare with existing file | |||
| if (!fs.existsSync(outputPath)) { | |||
| console.error('❌ Type definitions file does not exist') | |||
| process.exit(1) | |||
| } | |||
| const existingContent = fs.readFileSync(outputPath, 'utf8') | |||
| if (existingContent.trim() !== typeDefinitions.trim()) { | |||
| console.error('❌ Type definitions are out of sync') | |||
| console.error(' Run: pnpm run gen:i18n-types') | |||
| process.exit(1) | |||
| } | |||
| console.log('✅ Type definitions are in sync') | |||
| } else { | |||
| // Generate mode: write file | |||
| fs.writeFileSync(outputPath, typeDefinitions) | |||
| console.log(`✅ Generated type definitions: ${outputPath}`) | |||
| } | |||
| } catch (error) { | |||
| console.error('❌ Error:', error.message) | |||
| process.exit(1) | |||
| } | |||
| } | |||
| if (require.main === module) { | |||
| main() | |||
| } | |||
| @@ -35,6 +35,8 @@ | |||
| "uglify-embed": "node ./bin/uglify-embed", | |||
| "check-i18n": "node ./i18n-config/check-i18n.js", | |||
| "auto-gen-i18n": "node ./i18n-config/auto-gen-i18n.js", | |||
| "gen:i18n-types": "node ./i18n-config/generate-i18n-types.js", | |||
| "check:i18n-types": "node ./i18n-config/check-i18n-sync.js", | |||
| "test": "jest", | |||
| "test:watch": "jest --watch", | |||
| "storybook": "storybook dev -p 6006", | |||
| @@ -0,0 +1,96 @@ | |||
| // TypeScript type definitions for Dify's i18next configuration | |||
| // This file is auto-generated. Do not edit manually. | |||
| // To regenerate, run: pnpm run gen:i18n-types | |||
| import 'react-i18next' | |||
| // Extract types from translation files using typeof import pattern | |||
| type AppAnnotationMessages = typeof import('../i18n/en-US/app-annotation').default | |||
| type AppApiMessages = typeof import('../i18n/en-US/app-api').default | |||
| type AppDebugMessages = typeof import('../i18n/en-US/app-debug').default | |||
| type AppLogMessages = typeof import('../i18n/en-US/app-log').default | |||
| type AppOverviewMessages = typeof import('../i18n/en-US/app-overview').default | |||
| type AppMessages = typeof import('../i18n/en-US/app').default | |||
| type BillingMessages = typeof import('../i18n/en-US/billing').default | |||
| type CommonMessages = typeof import('../i18n/en-US/common').default | |||
| type CustomMessages = typeof import('../i18n/en-US/custom').default | |||
| type DatasetCreationMessages = typeof import('../i18n/en-US/dataset-creation').default | |||
| type DatasetDocumentsMessages = typeof import('../i18n/en-US/dataset-documents').default | |||
| type DatasetHitTestingMessages = typeof import('../i18n/en-US/dataset-hit-testing').default | |||
| type DatasetSettingsMessages = typeof import('../i18n/en-US/dataset-settings').default | |||
| type DatasetMessages = typeof import('../i18n/en-US/dataset').default | |||
| type EducationMessages = typeof import('../i18n/en-US/education').default | |||
| type ExploreMessages = typeof import('../i18n/en-US/explore').default | |||
| type LayoutMessages = typeof import('../i18n/en-US/layout').default | |||
| type LoginMessages = typeof import('../i18n/en-US/login').default | |||
| type OauthMessages = typeof import('../i18n/en-US/oauth').default | |||
| type PluginTagsMessages = typeof import('../i18n/en-US/plugin-tags').default | |||
| type PluginMessages = typeof import('../i18n/en-US/plugin').default | |||
| type RegisterMessages = typeof import('../i18n/en-US/register').default | |||
| type RunLogMessages = typeof import('../i18n/en-US/run-log').default | |||
| type ShareMessages = typeof import('../i18n/en-US/share').default | |||
| type TimeMessages = typeof import('../i18n/en-US/time').default | |||
| type ToolsMessages = typeof import('../i18n/en-US/tools').default | |||
| type WorkflowMessages = typeof import('../i18n/en-US/workflow').default | |||
| // Complete type structure that matches i18next-config.ts camelCase conversion | |||
| export type Messages = { | |||
| appAnnotation: AppAnnotationMessages; | |||
| appApi: AppApiMessages; | |||
| appDebug: AppDebugMessages; | |||
| appLog: AppLogMessages; | |||
| appOverview: AppOverviewMessages; | |||
| app: AppMessages; | |||
| billing: BillingMessages; | |||
| common: CommonMessages; | |||
| custom: CustomMessages; | |||
| datasetCreation: DatasetCreationMessages; | |||
| datasetDocuments: DatasetDocumentsMessages; | |||
| datasetHitTesting: DatasetHitTestingMessages; | |||
| datasetSettings: DatasetSettingsMessages; | |||
| dataset: DatasetMessages; | |||
| education: EducationMessages; | |||
| explore: ExploreMessages; | |||
| layout: LayoutMessages; | |||
| login: LoginMessages; | |||
| oauth: OauthMessages; | |||
| pluginTags: PluginTagsMessages; | |||
| plugin: PluginMessages; | |||
| register: RegisterMessages; | |||
| runLog: RunLogMessages; | |||
| share: ShareMessages; | |||
| time: TimeMessages; | |||
| tools: ToolsMessages; | |||
| workflow: WorkflowMessages; | |||
| } | |||
| // Utility type to flatten nested object keys into dot notation | |||
| type FlattenKeys<T> = T extends object | |||
| ? { | |||
| [K in keyof T]: T[K] extends object | |||
| ? `${K & string}.${FlattenKeys<T[K]> & string}` | |||
| : `${K & string}` | |||
| }[keyof T] | |||
| : never | |||
| export type ValidTranslationKeys = FlattenKeys<Messages> | |||
| // Extend react-i18next with Dify's type structure | |||
| declare module 'react-i18next' { | |||
| type CustomTypeOptions = { | |||
| defaultNS: 'translation'; | |||
| resources: { | |||
| translation: Messages; | |||
| }; | |||
| } | |||
| } | |||
| // Extend i18next for complete type safety | |||
| declare module 'i18next' { | |||
| type CustomTypeOptions = { | |||
| defaultNS: 'translation'; | |||
| resources: { | |||
| translation: Messages; | |||
| }; | |||
| } | |||
| } | |||