llm_translate 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,189 @@
1
+ # llm_translate.yml - 翻译工具配置文件
2
+
3
+ # AI 模型配置
4
+ ai:
5
+ # API 密钥
6
+ api_key: xxxx
7
+
8
+ # API 主机地址
9
+ host: https://aihubmix.com
10
+
11
+ # 模型提供商
12
+ provider: "claude"
13
+
14
+ # 模型名称
15
+ model: "claude-3-7-sonnet-20250219"
16
+
17
+ # 模型参数
18
+ temperature: 0.3
19
+ max_tokens: 4000
20
+ top_p: 1.0
21
+
22
+ # 请求重试配置
23
+ retry_attempts: 3
24
+ retry_delay: 2 # 秒
25
+
26
+ # 请求超时时间
27
+ timeout: 60 # 秒
28
+
29
+ # 翻译配置
30
+ translation:
31
+ # 默认翻译 prompt
32
+ default_prompt: |
33
+ 请将以下 Markdown 内容翻译为中文,保持所有格式不变:
34
+ - 保留代码块、链接、图片等 Markdown 语法
35
+ - 保留英文的专业术语和产品名称
36
+ - 确保翻译自然流畅
37
+
38
+ 内容:
39
+ {content}
40
+
41
+ # 目标语言
42
+ target_language: "zh-CN"
43
+
44
+ # 源语言(auto 为自动检测)
45
+ source_language: "auto"
46
+
47
+ # 是否保留原文格式
48
+ preserve_formatting: true
49
+
50
+ # 是否翻译代码注释
51
+ translate_code_comments: false
52
+
53
+ # 需要保留不翻译的内容模式
54
+ preserve_patterns:
55
+ - "```[\\s\\S]*?```" # 代码块
56
+ - "`[^`]+`" # 行内代码
57
+ - "\\[.*?\\]\\(.*?\\)" # 链接
58
+ - "!\\[.*?\\]\\(.*?\\)" # 图片
59
+
60
+ # 文件处理配置
61
+ files:
62
+ # 输入目录
63
+ input_directory: "./docs"
64
+
65
+ # 输出目录
66
+ output_directory: "./docs-translated"
67
+
68
+ # 输入文件
69
+ input_file: "./README.md"
70
+
71
+ # 输出文件
72
+ output_file: "./README.zh.md"
73
+
74
+ # 文件名后缀策略
75
+ filename_strategy: "suffix" # suffix, replace, directory
76
+ filename_suffix: ".zh" # 仅当 strategy 为 suffix 时使用
77
+
78
+ # 包含的文件模式
79
+ include_patterns:
80
+ - "**/*.md"
81
+ - "**/*.markdown"
82
+
83
+ # 排除的文件模式
84
+ exclude_patterns:
85
+ - "**/node_modules/**"
86
+ - "**/.*"
87
+ - "**/*.tmp"
88
+ - "**/README.md" # 示例:排除 README 文件
89
+
90
+ # 是否保持目录结构
91
+ preserve_directory_structure: true
92
+
93
+ # 文件覆盖策略
94
+ overwrite_policy: "ask" # ask, overwrite, skip, backup
95
+
96
+ # 备份目录(当 overwrite_policy 为 backup 时)
97
+ backup_directory: "./backups"
98
+
99
+ # 日志配置
100
+ logging:
101
+ # 日志级别
102
+ level: "info" # debug, info, warn, error
103
+
104
+ # 日志输出位置
105
+ output: "console" # console, file, both
106
+
107
+ # 日志文件路径(当 output 包含 file 时)
108
+ file_path: "./logs/llm_translate.log"
109
+
110
+ # 是否记录详细的翻译过程
111
+ verbose_translation: false
112
+
113
+ # 错误日志文件
114
+ error_log_path: "./logs/errors.log"
115
+
116
+ # 错误处理配置
117
+ error_handling:
118
+ # 遇到错误时的行为
119
+ on_error: "log_and_continue" # stop, log_and_continue, skip_file
120
+
121
+ # 最大连续错误数(超过则停止)
122
+ max_consecutive_errors: 5
123
+
124
+ # 错误重试次数
125
+ retry_on_failure: 2
126
+
127
+ # 生成错误报告
128
+ generate_error_report: true
129
+ error_report_path: "./logs/error_report.md"
130
+
131
+ # 性能配置
132
+ performance:
133
+ # 并发处理文件数
134
+ concurrent_files: 3
135
+
136
+ # 批处理大小(同时翻译的文件数)
137
+ batch_size: 5
138
+
139
+ # 请求间隔(避免 API 限流)
140
+ request_interval: 1 # 秒
141
+
142
+ # 内存使用限制
143
+ max_memory_mb: 500
144
+
145
+ # 输出配置
146
+ output:
147
+ # 是否显示进度条
148
+ show_progress: true
149
+
150
+ # 是否显示翻译统计
151
+ show_statistics: true
152
+
153
+ # 是否生成翻译报告
154
+ generate_report: true
155
+ report_path: "./reports/translation_report.md"
156
+
157
+ # 输出格式
158
+ format: "markdown" # markdown, json, yaml
159
+
160
+ # 是否保留元数据
161
+ include_metadata: true
162
+
163
+ # 预设配置(可通过 --preset 参数使用)
164
+ presets:
165
+ chinese:
166
+ translation:
167
+ target_language: "zh-CN"
168
+ default_prompt: "翻译为简体中文,保持技术术语的准确性"
169
+
170
+ japanese:
171
+ translation:
172
+ target_language: "ja"
173
+ default_prompt: "日本語に翻訳してください。技術用語は正確に保ってください"
174
+
175
+ english:
176
+ translation:
177
+ target_language: "en"
178
+ default_prompt: "Translate to English, maintaining technical accuracy"
179
+
180
+ # 自定义 Hook(高级功能)
181
+ hooks:
182
+ # 翻译前处理
183
+ pre_translation: null
184
+
185
+ # 翻译后处理
186
+ post_translation: null
187
+
188
+ # 文件处理完成后
189
+ post_file_processing: null
data/content/prompt.md ADDED
@@ -0,0 +1,8 @@
1
+ ## Translator
2
+
3
+ ruby Gem 实现传入的 markdown 文件,使用 AI 进行翻译,再生成文件
4
+
5
+ - 指定传入的目录
6
+ - 指定输出目录
7
+ - 翻译的 prompt 可以自定义
8
+ - llm 使用 rubyllm gem
data/content/todo.md ADDED
@@ -0,0 +1,115 @@
1
+ # Translator Ruby Gem 开发计划
2
+
3
+ ## 项目概述
4
+ 实现一个 Ruby Gem,用于将 Markdown 文件通过 AI 进行翻译并生成新文件。
5
+
6
+ ## 详细任务列表
7
+
8
+ ### 1. 项目基础设施
9
+
10
+ #### 1.1 设置 Ruby Gem 项目结构
11
+ - [ ] 创建 gemspec 文件
12
+ - [ ] 创建 Gemfile 和依赖管理
13
+ - [ ] 设置 lib 目录结构
14
+ - [ ] 创建 bin 目录和可执行文件
15
+ - [ ] 配置 .gitignore 和基础文件
16
+
17
+ #### 1.2 实现命令行界面
18
+ - [ ] 使用 OptionParser 或 Thor 创建 CLI
19
+ - [ ] 支持指定输入目录参数
20
+ - [ ] 支持指定输出目录参数
21
+ - [ ] 支持自定义翻译 prompt 参数
22
+ - [ ] 添加帮助信息和版本显示
23
+
24
+ ### 2. 核心功能开发
25
+
26
+ #### 2.1 实现 Markdown 文件发现功能
27
+ - [ ] 递归扫描指定目录中的所有 .md 文件
28
+ - [ ] 支持文件过滤和排除规则
29
+ - [ ] 维护原始目录结构信息
30
+
31
+ #### 2.2 集成 rubyllm gem
32
+ - [ ] 添加 rubyllm 到依赖列表 https://rubyllm.com/
33
+ - [ ] 配置 AI 模型连接参数
34
+ - [ ] 实现 API 调用封装
35
+ - [ ] 处理 API 限流和重试机制
36
+
37
+ #### 2.3 实现翻译核心逻辑
38
+ - [ ] 读取 Markdown 文件内容
39
+ - [ ] 解析 Markdown 格式结构
40
+ - [ ] 调用 AI 进行翻译
41
+ - [ ] 保持 Markdown 格式完整性(代码块、链接、图片等)
42
+ - [ ] 处理多语言内容混合情况
43
+
44
+ #### 2.4 实现可自定义翻译 prompt 功能
45
+ - [ ] 支持从配置文件读取 prompt
46
+ - [ ] 提供默认翻译 prompt 模板
47
+ - [ ] 支持 prompt 变量替换(如目标语言)
48
+
49
+ #### 2.5 实现输出文件管理
50
+ - [ ] 在指定目录生成翻译后的文件
51
+ - [ ] 保持原始目录结构
52
+ - [ ] 支持文件名后缀配置(如 .zh.md)
53
+ - [ ] 处理文件覆盖和备份策略
54
+
55
+ ### 3. 质量保证
56
+
57
+ #### 3.1 添加错误处理和日志
58
+ - [ ] 文件读写错误处理
59
+ - [ ] AI 请求失败处理
60
+ - [ ] 网络连接异常处理
61
+ - [ ] 添加详细的日志记录
62
+ - [ ] 实现优雅的错误恢复机制
63
+
64
+ #### 3.2 实现配置系统
65
+ - [ ] 支持配置文件(YAML)
66
+ - [ ] API key 环境变量
67
+ - [ ] 模型参数配置(温度、最大长度等)
68
+ - [ ] 翻译选项配置
69
+ - [ ] 环境变量支持
70
+
71
+ #### 3.3 编写测试用例
72
+ - [ ] 单元测试:核心模块测试
73
+ - [ ] Mock AI 请求进行离线测试
74
+ - [ ] 测试各种 Markdown 格式
75
+
76
+ #### 3.4 执行结果记录
77
+ - [] 中间出错了不跳出,而是记录在报错日志中
78
+
79
+
80
+ ### 4. 文档和发布
81
+
82
+ #### 4.1 完善文档
83
+ - [ ] 更新 README.md 文件
84
+ - [ ] 编写详细的使用说明
85
+ - [ ] API 文档和代码注释
86
+ - [ ] 配置文件示例
87
+ - [ ] 常见问题解答
88
+
89
+ #### 4.2 打包发布
90
+ - [ ] 配置 gem 打包流程
91
+ - [ ] 设置版本管理策略
92
+ - [ ] 准备发布到 RubyGems
93
+ - [ ] 创建 GitHub Release
94
+ - [ ] 设置持续集成流程
95
+
96
+ ## 技术栈
97
+ - **语言**: Ruby
98
+ - **CLI 框架**: OptionParser 或 Thor
99
+ - **AI 集成**: rubyllm gem
100
+ - **测试框架**: RSpec
101
+ - **配置管理**: YAML
102
+ - **日志**: Ruby Logger
103
+
104
+ ## 开发优先级
105
+ 1. **高优先级**: 项目结构设置、基础 CLI、文件发现、AI 集成
106
+ 2. **中优先级**: 翻译逻辑、输出管理、错误处理
107
+ 3. **低优先级**: 高级配置、测试完善、文档编写、发布准备
108
+
109
+ ## 预期功能
110
+ ```bash
111
+
112
+ # 使用配置文件
113
+ translator --config ./translator.yml
114
+
115
+ ```
data/exe/llm_translate ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../lib/llm_translate'
5
+
6
+ LlmTranslate::CLI.start(ARGV)
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ruby_llm'
4
+ module LlmTranslate
5
+ class AiClient
6
+ attr_reader :config, :logger
7
+
8
+ def initialize(config, logger)
9
+ @config = config
10
+ @logger = logger
11
+ @client = initialize_client
12
+ end
13
+
14
+ def translate(content, custom_prompt = nil)
15
+ prompt = build_prompt(content, custom_prompt)
16
+
17
+ logger.log_ai_request(prompt.length, config.ai_model)
18
+
19
+ retries = 0
20
+ begin
21
+ response = make_request(prompt)
22
+
23
+ raise TranslationError, 'Empty response from AI service' unless response && !response.empty?
24
+
25
+ logger.log_ai_response(response.length)
26
+ response.strip
27
+ rescue StandardError => e
28
+ retries += 1
29
+ unless retries <= config.retry_attempts
30
+ raise TranslationError, "AI translation failed after #{config.retry_attempts} attempts: #{e.message}"
31
+ end
32
+
33
+ logger.warn "AI request failed (attempt #{retries}/#{config.retry_attempts}): #{e.message}"
34
+ sleep(config.retry_delay * retries) # Exponential backoff
35
+ retry
36
+ end
37
+ end
38
+
39
+ def test_connection
40
+ test_prompt = 'Hello, world!'
41
+
42
+ begin
43
+ response = make_request(test_prompt)
44
+ !response.nil? && !response.empty?
45
+ rescue StandardError => e
46
+ logger.error "AI connection test failed: #{e.message}"
47
+ false
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ def initialize_client
54
+ configure_ruby_llm
55
+ end
56
+
57
+ def configure_ruby_llm
58
+ RubyLLM.configure do |config_obj|
59
+ # For aihubmix.com or any custom host, use OpenAI-compatible API
60
+ config_obj.openai_api_key = config.api_key
61
+ config_obj.openai_api_base = config.ai_host
62
+ config_obj.default_model = config.ai_model
63
+ end
64
+ end
65
+
66
+ def make_request(prompt)
67
+ chat = RubyLLM.chat
68
+ .with_model(config.ai_model)
69
+ .with_temperature(config.temperature)
70
+
71
+ response = chat.ask(prompt)
72
+
73
+ # Handle different response formats
74
+ case response
75
+ when RubyLLM::Message
76
+ response.content
77
+ when String
78
+ response
79
+ when Hash
80
+ response['content'] || response[:content] || response.dig('choices', 0, 'message', 'content')
81
+ else
82
+ response.to_s
83
+ end
84
+ end
85
+
86
+ def build_prompt(content, custom_prompt = nil)
87
+ template = custom_prompt || config.default_prompt
88
+
89
+ # Replace {content} placeholder with actual content
90
+ template.gsub('{content}', content)
91
+ .gsub('{target_language}', config.target_language)
92
+ .gsub('{source_language}', config.source_language)
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,205 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+ require 'yaml'
5
+
6
+ module LlmTranslate
7
+ class CLI < Thor
8
+ desc 'translate', 'Translate markdown files using AI'
9
+ option :config, aliases: '-c', type: :string, default: './llm_translate.yml',
10
+ desc: 'Path to configuration file'
11
+ option :input, aliases: '-i', type: :string,
12
+ desc: 'Input directory (overrides config)'
13
+ option :output, aliases: '-o', type: :string,
14
+ desc: 'Output directory (overrides config)'
15
+ option :prompt, aliases: '-p', type: :string,
16
+ desc: 'Custom translation prompt (overrides config)'
17
+ option :verbose, aliases: '-v', type: :boolean, default: false,
18
+ desc: 'Enable verbose output'
19
+ option :dry_run, aliases: '-d', type: :boolean, default: false,
20
+ desc: 'Perform a dry run without actual translation'
21
+
22
+ def translate
23
+ config_path = options[:config]
24
+
25
+ unless File.exist?(config_path)
26
+ say "Configuration file not found: #{config_path}", :red
27
+ say 'Please create a configuration file or specify a valid path with --config', :yellow
28
+ exit 1
29
+ end
30
+
31
+ begin
32
+ config = Config.load(config_path, options)
33
+ logger = Logger.new(config)
34
+
35
+ logger.info 'Starting translation process...'
36
+ if config.single_file_mode?
37
+ logger.info "Input file: #{config.input_file}"
38
+ logger.info "Output file: #{config.output_file}"
39
+ else
40
+ logger.info "Input directory: #{config.input_directory}"
41
+ logger.info "Output directory: #{config.output_directory}"
42
+ end
43
+
44
+ logger.info 'DRY RUN MODE - No files will be translated' if options[:dry_run]
45
+
46
+ # Initialize components
47
+ ai_client = AiClient.new(config, logger)
48
+ translator_engine = TranslatorEngine.new(config, logger, ai_client)
49
+
50
+ # Determine files to translate
51
+ if config.single_file_mode?
52
+ # Single file mode
53
+ unless File.exist?(config.input_file)
54
+ logger.error "Input file not found: #{config.input_file}"
55
+ return
56
+ end
57
+ files = [config.input_file]
58
+ logger.info "Single file mode: translating #{config.input_file}"
59
+ else
60
+ # Directory mode
61
+ file_finder = FileFinder.new(config, logger)
62
+ files = file_finder.find_markdown_files
63
+
64
+ if files.empty?
65
+ logger.warn "No markdown files found in #{config.input_directory}"
66
+ return
67
+ end
68
+
69
+ logger.info "Found #{files.length} markdown files to translate"
70
+ end
71
+
72
+ # Translate files
73
+ success_count = 0
74
+ error_count = 0
75
+
76
+ files.each_with_index do |file_path, index|
77
+ logger.info "[#{index + 1}/#{files.length}] Processing: #{file_path}"
78
+
79
+ translator_engine.translate_file(file_path) unless options[:dry_run]
80
+
81
+ success_count += 1
82
+ logger.info "✓ Successfully processed: #{file_path}"
83
+ rescue StandardError => e
84
+ error_count += 1
85
+ logger.error "✗ Failed to process #{file_path}: #{e.message}"
86
+
87
+ if config.should_stop_on_error?(error_count)
88
+ logger.error 'Stopping due to too many consecutive errors'
89
+ break
90
+ end
91
+ end
92
+
93
+ # Summary
94
+ logger.info 'Translation completed!'
95
+ logger.info "Success: #{success_count}, Errors: #{error_count}"
96
+
97
+ generate_report(config, success_count, error_count, files) if config.generate_report?
98
+ rescue ConfigurationError => e
99
+ say "Configuration error: #{e.message}", :red
100
+ exit 1
101
+ rescue StandardError => e
102
+ say "Unexpected error: #{e.message}", :red
103
+ say e.backtrace.join("\n") if options[:verbose]
104
+ exit 1
105
+ end
106
+ end
107
+
108
+ desc 'version', 'Show version'
109
+ def version
110
+ say "LlmTranslate #{LlmTranslate::VERSION}"
111
+ end
112
+
113
+ desc 'init', 'Initialize a new configuration file'
114
+ option :output, aliases: '-o', type: :string, default: './llm_translate.yml',
115
+ desc: 'Output path for configuration file'
116
+
117
+ def init
118
+ config_path = options[:output]
119
+
120
+ return if File.exist?(config_path) && !yes?('Configuration file already exists. Overwrite? (y/N)')
121
+
122
+ # Copy the sample configuration
123
+ sample_config = File.join(__dir__, '../../content/llm_translate.yml')
124
+ if File.exist?(sample_config)
125
+ FileUtils.cp(sample_config, config_path)
126
+ say "Configuration file created: #{config_path}", :green
127
+ say 'Please edit the file to configure your API keys and preferences', :yellow
128
+ else
129
+ # Create a minimal config if sample doesn't exist
130
+ create_minimal_config(config_path)
131
+ end
132
+ end
133
+
134
+ private
135
+
136
+ def generate_report(config, success_count, error_count, files)
137
+ return unless config.generate_report?
138
+
139
+ report_path = config.report_path
140
+ FileUtils.mkdir_p(File.dirname(report_path))
141
+
142
+ File.write(report_path, <<~REPORT)
143
+ # Translation Report
144
+
145
+ **Date**: #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}
146
+
147
+ ## Summary
148
+ - Total files: #{files.length}
149
+ - Successfully translated: #{success_count}
150
+ - Errors: #{error_count}
151
+ - Success rate: #{(success_count.to_f / files.length * 100).round(2)}%
152
+
153
+ ## Configuration
154
+ - Input directory: #{config.input_directory}
155
+ - Output directory: #{config.output_directory}
156
+ - Target language: #{config.target_language}
157
+ - AI Provider: #{config.ai_provider}
158
+ - Model: #{config.ai_model}
159
+ REPORT
160
+
161
+ say "Report generated: #{report_path}", :green
162
+ end
163
+
164
+ def create_minimal_config(config_path)
165
+ config_content = <<~YAML
166
+ # LlmTranslate Configuration
167
+ ai:
168
+ api_key: sk-
169
+ host: https://aihubmix.com/v1
170
+ provider: "openai"
171
+ model: "gpt-4o-mini"
172
+ temperature: 0.3
173
+
174
+ translation:
175
+ target_language: "zh-CN"
176
+ default_prompt: |
177
+ Please translate the following Markdown content to Chinese, keeping all formatting intact:
178
+ - Preserve code blocks, links, images, and other Markdown syntax
179
+ - Keep English technical terms and product names
180
+ - Ensure natural and fluent translation
181
+ #{' '}
182
+ Content:
183
+ {content}
184
+
185
+ files:
186
+ # Directory mode (default)
187
+ input_directory: "./docs"
188
+ output_directory: "./docs-translated"
189
+ filename_suffix: ".zh"
190
+ #{' '}
191
+ # Single file mode (uncomment to use)
192
+ # input_file: "./README.md"
193
+ # output_file: "./README.zh.md"
194
+
195
+ logging:
196
+ level: "info"
197
+ output: "console"
198
+ YAML
199
+
200
+ File.write(config_path, config_content)
201
+ say "Minimal configuration file created: #{config_path}", :green
202
+ say 'Please edit the file to configure your API keys and preferences', :yellow
203
+ end
204
+ end
205
+ end