You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

generate-i18n-types.js 4.0KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. #!/usr/bin/env node
  2. const fs = require('fs')
  3. const path = require('path')
  4. const { camelCase } = require('lodash')
  5. // Import the NAMESPACES array from i18next-config.ts
  6. function getNamespacesFromConfig() {
  7. const configPath = path.join(__dirname, 'i18next-config.ts')
  8. const configContent = fs.readFileSync(configPath, 'utf8')
  9. // Extract NAMESPACES array using regex
  10. const namespacesMatch = configContent.match(/const NAMESPACES = \[([\s\S]*?)\]/)
  11. if (!namespacesMatch) {
  12. throw new Error('Could not find NAMESPACES array in i18next-config.ts')
  13. }
  14. // Parse the namespaces
  15. const namespacesStr = namespacesMatch[1]
  16. const namespaces = namespacesStr
  17. .split(',')
  18. .map(line => line.trim())
  19. .filter(line => line.startsWith("'") || line.startsWith('"'))
  20. .map(line => line.slice(1, -1)) // Remove quotes
  21. return namespaces
  22. }
  23. function generateTypeDefinitions(namespaces) {
  24. const header = `// TypeScript type definitions for Dify's i18next configuration
  25. // This file is auto-generated. Do not edit manually.
  26. // To regenerate, run: pnpm run gen:i18n-types
  27. import 'react-i18next'
  28. // Extract types from translation files using typeof import pattern`
  29. // Generate individual type definitions
  30. const typeDefinitions = namespaces.map(namespace => {
  31. const typeName = camelCase(namespace).replace(/^\w/, c => c.toUpperCase()) + 'Messages'
  32. return `type ${typeName} = typeof import('../i18n/en-US/${namespace}').default`
  33. }).join('\n')
  34. // Generate Messages interface
  35. const messagesInterface = `
  36. // Complete type structure that matches i18next-config.ts camelCase conversion
  37. export type Messages = {
  38. ${namespaces.map(namespace => {
  39. const camelCased = camelCase(namespace)
  40. const typeName = camelCase(namespace).replace(/^\w/, c => c.toUpperCase()) + 'Messages'
  41. return ` ${camelCased}: ${typeName};`
  42. }).join('\n')}
  43. }`
  44. const utilityTypes = `
  45. // Utility type to flatten nested object keys into dot notation
  46. type FlattenKeys<T> = T extends object
  47. ? {
  48. [K in keyof T]: T[K] extends object
  49. ? \`\${K & string}.\${FlattenKeys<T[K]> & string}\`
  50. : \`\${K & string}\`
  51. }[keyof T]
  52. : never
  53. export type ValidTranslationKeys = FlattenKeys<Messages>`
  54. const moduleDeclarations = `
  55. // Extend react-i18next with Dify's type structure
  56. declare module 'react-i18next' {
  57. interface CustomTypeOptions {
  58. defaultNS: 'translation';
  59. resources: {
  60. translation: Messages;
  61. };
  62. }
  63. }
  64. // Extend i18next for complete type safety
  65. declare module 'i18next' {
  66. interface CustomTypeOptions {
  67. defaultNS: 'translation';
  68. resources: {
  69. translation: Messages;
  70. };
  71. }
  72. }`
  73. return [header, typeDefinitions, messagesInterface, utilityTypes, moduleDeclarations].join('\n\n')
  74. }
  75. function main() {
  76. const args = process.argv.slice(2)
  77. const checkMode = args.includes('--check')
  78. try {
  79. console.log('📦 Generating i18n type definitions...')
  80. // Get namespaces from config
  81. const namespaces = getNamespacesFromConfig()
  82. console.log(`✅ Found ${namespaces.length} namespaces`)
  83. // Generate type definitions
  84. const typeDefinitions = generateTypeDefinitions(namespaces)
  85. const outputPath = path.join(__dirname, '../types/i18n.d.ts')
  86. if (checkMode) {
  87. // Check mode: compare with existing file
  88. if (!fs.existsSync(outputPath)) {
  89. console.error('❌ Type definitions file does not exist')
  90. process.exit(1)
  91. }
  92. const existingContent = fs.readFileSync(outputPath, 'utf8')
  93. if (existingContent.trim() !== typeDefinitions.trim()) {
  94. console.error('❌ Type definitions are out of sync')
  95. console.error(' Run: pnpm run gen:i18n-types')
  96. process.exit(1)
  97. }
  98. console.log('✅ Type definitions are in sync')
  99. } else {
  100. // Generate mode: write file
  101. fs.writeFileSync(outputPath, typeDefinitions)
  102. console.log(`✅ Generated type definitions: ${outputPath}`)
  103. }
  104. } catch (error) {
  105. console.error('❌ Error:', error.message)
  106. process.exit(1)
  107. }
  108. }
  109. if (require.main === module) {
  110. main()
  111. }