| working-directory: ./web | working-directory: ./web | ||||
| run: pnpm run auto-gen-i18n ${{ env.FILE_ARGS }} | 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 | - name: Create Pull Request | ||||
| if: env.FILES_CHANGED == 'true' | if: env.FILES_CHANGED == 'true' | ||||
| uses: peter-evans/create-pull-request@v6 | uses: peter-evans/create-pull-request@v6 | ||||
| with: | with: | ||||
| token: ${{ secrets.GITHUB_TOKEN }} | 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 | branch: chore/automated-i18n-updates |
| working-directory: ./web | working-directory: ./web | ||||
| run: pnpm install --frozen-lockfile | 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 | - name: Run tests | ||||
| if: steps.changed-files.outputs.any_changed == 'true' | if: steps.changed-files.outputs.any_changed == 'true' | ||||
| working-directory: ./web | working-directory: ./web |
| from typing import Optional, Union | from typing import Optional, Union | ||||
| from sqlalchemy import select | from sqlalchemy import select | ||||
| from sqlalchemy.orm import Session | |||||
| from controllers.service_api.wraps import create_or_update_end_user_for_user_id | 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 | from core.app.app_config.common.parameters_mapping import get_parameters_from_feature_dict | ||||
| """ | """ | ||||
| get the user by user id | 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: | if not user: | ||||
| raise ValueError("user not found") | raise ValueError("user not found") |
| import { | import { | ||||
| genNewNodeTitleFromOld, | genNewNodeTitleFromOld, | ||||
| generateNewNode, | generateNewNode, | ||||
| getNestedNodePosition, | |||||
| getNodeCustomTypeByNodeDataType, | getNodeCustomTypeByNodeDataType, | ||||
| getNodesConnectedSourceOrTargetHandleIdsMap, | getNodesConnectedSourceOrTargetHandleIdsMap, | ||||
| getTopLeftNodePosition, | getTopLeftNodePosition, | ||||
| }) | }) | ||||
| newChildren.push(newIterationStartNode!) | newChildren.push(newIterationStartNode!) | ||||
| } | } | ||||
| if (nodeToPaste.data.type === BlockEnum.Loop) { | |||||
| else if (nodeToPaste.data.type === BlockEnum.Loop) { | |||||
| newLoopStartNode!.parentId = newNode.id; | newLoopStartNode!.parentId = newNode.id; | ||||
| (newNode.data as LoopNodeType).start_node_id = newLoopStartNode!.id | (newNode.data as LoopNodeType).start_node_id = newLoopStartNode!.id | ||||
| }) | }) | ||||
| newChildren.push(newLoopStartNode!) | 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) | nodesToPaste.push(newNode) | ||||
| nodesToPaste.push(...newChildren) | nodesToPaste.push(...newChildren) | ||||
| }) | }) | ||||
| // only handle edge when paste nested block | |||||
| edges.forEach((edge) => { | edges.forEach((edge) => { | ||||
| const sourceId = idMapping[edge.source] | const sourceId = idMapping[edge.source] | ||||
| const targetId = idMapping[edge.target] | const targetId = idMapping[edge.target] |
| } | } | ||||
| } | } | ||||
| 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) => { | export const hasRetryNode = (nodeType?: BlockEnum) => { | ||||
| return nodeType === BlockEnum.LLM || nodeType === BlockEnum.Tool || nodeType === BlockEnum.HttpRequest || nodeType === BlockEnum.Code | return nodeType === BlockEnum.LLM || nodeType === BlockEnum.Tool || nodeType === BlockEnum.HttpRequest || nodeType === BlockEnum.Code | ||||
| } | } |
| let MDXComponent: (props: any) => JSX.Element | let MDXComponent: (props: any) => JSX.Element | ||||
| export default MDXComponent | export default MDXComponent | ||||
| } | } | ||||
| import './types/i18n' |
| #!/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() | |||||
| } |
| #!/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() | |||||
| } |
| "uglify-embed": "node ./bin/uglify-embed", | "uglify-embed": "node ./bin/uglify-embed", | ||||
| "check-i18n": "node ./i18n-config/check-i18n.js", | "check-i18n": "node ./i18n-config/check-i18n.js", | ||||
| "auto-gen-i18n": "node ./i18n-config/auto-gen-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": "jest", | ||||
| "test:watch": "jest --watch", | "test:watch": "jest --watch", | ||||
| "storybook": "storybook dev -p 6006", | "storybook": "storybook dev -p 6006", |
| // 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; | |||||
| }; | |||||
| } | |||||
| } |