fastlane-plugin-translate_gpt_release_notes 0.1.1 → 0.2.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,119 @@
1
+ require 'anthropic'
2
+ require_relative 'base_provider'
3
+
4
+ module Fastlane
5
+ module Helper
6
+ module Providers
7
+ # Provider implementation for Anthropic Claude translation API.
8
+ # Uses the Claude models for high-quality text translation with strong reasoning capabilities.
9
+ class AnthropicProvider < BaseProvider
10
+ # Default model for Anthropic translations
11
+ DEFAULT_MODEL = 'claude-sonnet-4.5'.freeze
12
+
13
+ # Default maximum tokens for translation response
14
+ DEFAULT_MAX_TOKENS = 1024
15
+
16
+ # Default temperature for translation generation (0.5 = balanced creativity)
17
+ DEFAULT_TEMPERATURE = 0.5
18
+
19
+ # Default request timeout in seconds
20
+ DEFAULT_TIMEOUT = 60
21
+
22
+ # Returns the provider identifier string.
23
+ #
24
+ # @return [String] Provider identifier
25
+ def self.provider_name
26
+ 'anthropic'
27
+ end
28
+
29
+ # Returns the human-readable display name for the provider.
30
+ #
31
+ # @return [String] Human-readable name
32
+ def self.display_name
33
+ 'Anthropic Claude'
34
+ end
35
+
36
+ # Returns the list of required credential symbols for this provider.
37
+ #
38
+ # @return [Array<Symbol>] Array of required credential keys
39
+ def self.required_credentials
40
+ [:api_token]
41
+ end
42
+
43
+ # Returns a hash of optional parameter definitions for this provider.
44
+ #
45
+ # @return [Hash] Optional parameter definitions
46
+ def self.optional_params
47
+ {
48
+ model_name: { default: DEFAULT_MODEL, description: 'Anthropic model to use', env: 'ANTHROPIC_MODEL_NAME' },
49
+ max_tokens: { default: DEFAULT_MAX_TOKENS, description: 'Maximum tokens in response', env: 'ANTHROPIC_MAX_TOKENS' },
50
+ temperature: { default: DEFAULT_TEMPERATURE, description: 'Sampling temperature (0-1)', env: 'ANTHROPIC_TEMPERATURE' },
51
+ request_timeout: { default: DEFAULT_TIMEOUT, description: 'Request timeout in seconds', env: 'ANTHROPIC_REQUEST_TIMEOUT' }
52
+ }
53
+ end
54
+
55
+ # Initializes the Anthropic provider with configuration parameters.
56
+ # Sets up the Anthropic::Client with appropriate credentials and timeout.
57
+ #
58
+ # @param params [Hash] Configuration parameters for the provider
59
+ def initialize(params)
60
+ super
61
+
62
+ timeout = @params[:request_timeout] || DEFAULT_TIMEOUT
63
+ @client = Anthropic::Client.new(
64
+ api_key: credential(:api_token),
65
+ timeout: timeout.to_i
66
+ )
67
+ end
68
+
69
+ # Validates the provider configuration.
70
+ # Ensures that the required api_token credential is present.
71
+ #
72
+ # @return [void]
73
+ def validate_config!
74
+ require_credential(:api_token)
75
+ end
76
+
77
+ # Translates text from source locale to target locale using Anthropic's API.
78
+ #
79
+ # @param text [String] The text to translate
80
+ # @param source_locale [String] Source language code (e.g., 'en', 'de')
81
+ # @param target_locale [String] Target language code (e.g., 'es', 'fr')
82
+ # @return [String, nil] Translated text or nil on error
83
+ def translate(text, source_locale, target_locale)
84
+ # Build prompt using inherited method
85
+ prompt = build_prompt(text, source_locale, target_locale)
86
+
87
+ # Add Android limitations if needed
88
+ prompt = apply_android_limitations(prompt) if @params[:platform] == 'android'
89
+
90
+ # Make API call using ruby-anthropic gem API
91
+ response = @client.complete(
92
+ model: @params[:model_name] || DEFAULT_MODEL,
93
+ max_tokens_to_sample: (@params[:max_tokens] || DEFAULT_MAX_TOKENS).to_i,
94
+ temperature: (@params[:temperature] || DEFAULT_TEMPERATURE).to_f,
95
+ prompt: "\n\nHuman: #{prompt}\n\nAssistant:"
96
+ )
97
+
98
+ # Extract text from response
99
+ extract_text_from_response(response)
100
+ rescue StandardError => e
101
+ UI.error "Anthropic provider error: #{e.message}"
102
+ nil
103
+ end
104
+
105
+ private
106
+
107
+ # Extracts translated text from the Anthropic API response
108
+ #
109
+ # @param response [Hash] The API response hash
110
+ # @return [String, nil] The translated text or nil
111
+ def extract_text_from_response(response)
112
+ return nil if response.nil?
113
+
114
+ response['completion']&.strip
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,161 @@
1
+ module Fastlane
2
+ module Helper
3
+ module Providers
4
+ # Abstract base class for all translation providers.
5
+ # Subclasses must implement all abstract methods to provide
6
+ # provider-specific translation functionality.
7
+ class BaseProvider
8
+ # Maximum character length for Google Play release notes
9
+ ANDROID_CHAR_LIMIT = 500
10
+
11
+ attr_reader :params, :config_errors
12
+
13
+ # Initializes the provider with configuration parameters.
14
+ # Automatically validates the configuration.
15
+ #
16
+ # @param params [Hash] Configuration parameters for the provider
17
+ def initialize(params)
18
+ @params = params
19
+ @config_errors = []
20
+ validate_config!
21
+ end
22
+
23
+ # Translates text from source locale to target locale.
24
+ # Must be implemented by subclasses.
25
+ #
26
+ # @param text [String] The text to translate
27
+ # @param source_locale [String] Source language code (e.g., 'en', 'de')
28
+ # @param target_locale [String] Target language code (e.g., 'es', 'fr')
29
+ # @return [String, nil] Translated text or nil on error
30
+ def translate(text, source_locale, target_locale)
31
+ raise NotImplementedError, "#{self.class.name} must implement #translate"
32
+ end
33
+
34
+ # Validates provider-specific configuration.
35
+ # Should populate @config_errors with any validation failures.
36
+ # Must be implemented by subclasses.
37
+ #
38
+ # @return [void]
39
+ def validate_config!
40
+ raise NotImplementedError, "#{self.class.name} must implement #validate_config!"
41
+ end
42
+
43
+ # Returns the provider identifier string.
44
+ # Must be implemented by subclasses.
45
+ #
46
+ # @return [String] Provider identifier (e.g., 'openai', 'anthropic')
47
+ def self.provider_name
48
+ raise NotImplementedError, "#{name} must implement .provider_name"
49
+ end
50
+
51
+ # Returns the human-readable display name for the provider.
52
+ # Must be implemented by subclasses.
53
+ #
54
+ # @return [String] Human-readable name (e.g., 'OpenAI GPT', 'Anthropic Claude')
55
+ def self.display_name
56
+ raise NotImplementedError, "#{name} must implement .display_name"
57
+ end
58
+
59
+ # Returns the list of required credential symbols for this provider.
60
+ # Must be implemented by subclasses.
61
+ #
62
+ # @return [Array<Symbol>] Array of required credential keys
63
+ def self.required_credentials
64
+ raise NotImplementedError, "#{name} must implement .required_credentials"
65
+ end
66
+
67
+ # Returns a hash of optional parameter definitions for this provider.
68
+ # Keys are parameter names, values are hashes with :default and :description.
69
+ # Must be implemented by subclasses.
70
+ #
71
+ # @return [Hash] Optional parameter definitions
72
+ def self.optional_params
73
+ raise NotImplementedError, "#{name} must implement .optional_params"
74
+ end
75
+
76
+ # Checks if the provider configuration is valid.
77
+ #
78
+ # @return [Boolean] true if no configuration errors exist
79
+ def valid?
80
+ @config_errors.empty?
81
+ end
82
+
83
+ protected
84
+
85
+ # Builds a translation prompt for the AI provider.
86
+ # Includes context about platform limitations if applicable.
87
+ #
88
+ # @param text [String] The text to translate
89
+ # @param source_locale [String] Source language code
90
+ # @param target_locale [String] Target language code
91
+ # @return [String] The formatted prompt
92
+ def build_prompt(text, source_locale, target_locale)
93
+ prompt_parts = []
94
+
95
+ # Base translation instruction
96
+ prompt_parts << "Translate the following text from #{source_locale} to #{target_locale}:"
97
+ prompt_parts << ""
98
+ prompt_parts << "\"#{text}\""
99
+
100
+ # Add context if provided
101
+ if @params[:context]
102
+ prompt_parts << ""
103
+ prompt_parts << "Context: #{@params[:context]}"
104
+ end
105
+
106
+ # Apply Android limitations if specified
107
+ if @params[:android_limitations]
108
+ prompt_parts << ""
109
+ prompt_parts << apply_android_limitations("")
110
+ end
111
+
112
+ prompt_parts.join("\n")
113
+ end
114
+
115
+ # Adds Android character limit constraint to the prompt.
116
+ # Google Play has a 500 character limit for release notes.
117
+ #
118
+ # @param prompt [String] The existing prompt to append to
119
+ # @return [String] The prompt with limitation instruction appended
120
+ def apply_android_limitations(prompt)
121
+ prompt + "IMPORTANT: The translated text must not exceed #{ANDROID_CHAR_LIMIT} characters " \
122
+ "(Google Play Store release notes limit). Please provide a concise translation."
123
+ end
124
+
125
+ # Adds a configuration error to the errors list.
126
+ #
127
+ # @param message [String] The error message
128
+ # @return [void]
129
+ def add_config_error(message)
130
+ @config_errors << message
131
+ UI.error("[#{self.class.display_name}] #{message}") if defined?(UI)
132
+ end
133
+
134
+ # Retrieves a credential value from environment variables or params.
135
+ # Checks environment variable first, then falls back to params.
136
+ #
137
+ # @param key [Symbol] The credential key
138
+ # @return [String, nil] The credential value or nil if not found
139
+ def credential(key)
140
+ env_var = "TRANSLATE_#{self.class.provider_name.upcase}_#{key.to_s.upcase}"
141
+ ENV[env_var] || @params[key]
142
+ end
143
+
144
+ # Checks if a required credential is present.
145
+ # Adds a config error if the credential is missing.
146
+ #
147
+ # @param key [Symbol] The credential key to validate
148
+ # @return [Boolean] true if the credential is present
149
+ def require_credential(key)
150
+ value = credential(key)
151
+ if value.nil? || value.to_s.empty?
152
+ add_config_error("Missing required credential: #{key}")
153
+ false
154
+ else
155
+ true
156
+ end
157
+ end
158
+ end
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,145 @@
1
+ require 'deepl'
2
+ require_relative 'base_provider'
3
+
4
+ module Fastlane
5
+ module Helper
6
+ module Providers
7
+ # Provider implementation for DeepL translation API.
8
+ # DeepL is a purpose-built neural machine translation service (not an LLM)
9
+ # known for high-quality European language translations.
10
+ class DeepLProvider < BaseProvider
11
+ # Default request timeout in seconds
12
+ DEFAULT_TIMEOUT = 30
13
+
14
+ # Returns the provider identifier string.
15
+ #
16
+ # @return [String] Provider identifier
17
+ def self.provider_name
18
+ 'deepl'
19
+ end
20
+
21
+ # Returns the human-readable display name for the provider.
22
+ #
23
+ # @return [String] Human-readable name
24
+ def self.display_name
25
+ 'DeepL'
26
+ end
27
+
28
+ # Returns the list of required credential symbols for this provider.
29
+ #
30
+ # @return [Array<Symbol>] Array of required credential keys
31
+ def self.required_credentials
32
+ [:api_token]
33
+ end
34
+
35
+ # Returns a hash of optional parameter definitions for this provider.
36
+ #
37
+ # @return [Hash] Optional parameter definitions
38
+ def self.optional_params
39
+ {
40
+ request_timeout: {
41
+ default: DEFAULT_TIMEOUT,
42
+ env: 'DEEPL_REQUEST_TIMEOUT',
43
+ description: 'Request timeout in seconds'
44
+ },
45
+ formality: {
46
+ default: 'default',
47
+ env: 'DEEPL_FORMALITY',
48
+ description: 'Formality level: default, more, or less'
49
+ }
50
+ }
51
+ end
52
+
53
+ # Free DeepL API keys end with ':fx' and use a different endpoint
54
+ FREE_KEY_SUFFIX = ':fx'.freeze
55
+
56
+ # API endpoints for different key types
57
+ API_HOST_PAID = 'https://api.deepl.com'.freeze
58
+ API_HOST_FREE = 'https://api-free.deepl.com'.freeze
59
+
60
+ # Initializes the DeepL provider with configuration parameters.
61
+ # Configures the DeepL gem with the API authentication key.
62
+ # Automatically detects free vs paid keys and uses the appropriate endpoint.
63
+ #
64
+ # @param params [Hash] Configuration parameters for the provider
65
+ def initialize(params)
66
+ super(params)
67
+
68
+ api_key = params[:api_token].to_s
69
+ return if api_key.nil? || api_key.empty?
70
+
71
+ host = api_key.end_with?(FREE_KEY_SUFFIX) ? API_HOST_FREE : API_HOST_PAID
72
+
73
+ DeepL.configure do |config|
74
+ config.auth_key = api_key
75
+ config.host = host
76
+ end
77
+ end
78
+
79
+ # Validates the provider configuration.
80
+ # Ensures that the required api_token credential is present.
81
+ #
82
+ # @return [void]
83
+ def validate_config!
84
+ require_credential(:api_token)
85
+ end
86
+
87
+ # Translates text from source locale to target locale using DeepL's API.
88
+ #
89
+ # @param text [String] The text to translate
90
+ # @param source_locale [String] Source language code (e.g., 'en-US', 'de-DE')
91
+ # @param target_locale [String] Target language code (e.g., 'es', 'fr')
92
+ # @return [String, nil] Translated text or nil on error
93
+ def translate(text, source_locale, target_locale)
94
+ # DeepL uses ISO 639-1 language codes (2-letter codes)
95
+ # Convert locales like 'en-US' to 'EN'
96
+ source_lang = normalize_locale(source_locale)
97
+ target_lang = normalize_locale(target_locale)
98
+
99
+ # Build options hash
100
+ options = {}
101
+
102
+ # Add formality if specified (not available for all languages)
103
+ formality = @params[:formality].to_s.strip
104
+ options[:formality] = formality unless formality.empty? || formality == 'default'
105
+
106
+ # DeepL supports context parameter for better translations
107
+ if @params[:context] && !@params[:context].empty?
108
+ options[:context] = @params[:context]
109
+ end
110
+
111
+ # Make API call
112
+ result = DeepL.translate(text, source_lang, target_lang, options)
113
+
114
+ translated = result.text
115
+
116
+ # Handle Android 500 character limit
117
+ if @params[:platform] == 'android' && translated.length > 500
118
+ UI.warning "DeepL translation exceeds 500 characters (#{translated.length}), truncating..."
119
+ translated = translated[0...500]
120
+ end
121
+
122
+ translated
123
+ rescue DeepL::Exceptions::RequestError => e
124
+ UI.error "DeepL API error: #{e.message}"
125
+ nil
126
+ rescue StandardError => e
127
+ UI.error "DeepL provider error: #{e.message}"
128
+ nil
129
+ end
130
+
131
+ private
132
+
133
+ # Normalizes locale codes for DeepL API.
134
+ # DeepL uses 2-letter ISO 639-1 codes (e.g., 'EN', 'DE', 'FR').
135
+ # Converts 'en-US' → 'EN', 'de-DE' → 'DE'.
136
+ #
137
+ # @param locale [String] The locale string to normalize
138
+ # @return [String] The normalized 2-letter language code
139
+ def normalize_locale(locale)
140
+ locale.to_s.split('-').first.upcase
141
+ end
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,153 @@
1
+ require 'net/http'
2
+ require 'json'
3
+ require_relative 'base_provider'
4
+
5
+ module Fastlane
6
+ module Helper
7
+ module Providers
8
+ # Provider implementation for Google Gemini translation API using direct HTTP calls.
9
+ # Offers cost-effective translations suitable for high-volume use cases.
10
+ class GeminiProvider < BaseProvider
11
+ # Default model for Gemini translations
12
+ DEFAULT_MODEL = 'gemini-2.5-flash'.freeze
13
+
14
+ # Default temperature for translation generation (0.5 = balanced creativity)
15
+ DEFAULT_TEMPERATURE = 0.5
16
+
17
+ # Default request timeout in seconds
18
+ DEFAULT_TIMEOUT = 60
19
+
20
+ # Base URL for Gemini Generative Language API
21
+ API_BASE_URL = 'https://generativelanguage.googleapis.com'.freeze
22
+
23
+ # Returns the provider identifier string.
24
+ #
25
+ # @return [String] Provider identifier
26
+ def self.provider_name
27
+ 'gemini'
28
+ end
29
+
30
+ # Returns the human-readable display name for the provider.
31
+ #
32
+ # @return [String] Human-readable name
33
+ def self.display_name
34
+ 'Google Gemini'
35
+ end
36
+
37
+ # Returns the list of required credential symbols for this provider.
38
+ #
39
+ # @return [Array<Symbol>] Array of required credential keys
40
+ def self.required_credentials
41
+ [:api_token]
42
+ end
43
+
44
+ # Returns a hash of optional parameter definitions for this provider.
45
+ #
46
+ # @return [Hash] Optional parameter definitions
47
+ def self.optional_params
48
+ {
49
+ model_name: { default: DEFAULT_MODEL, description: 'Gemini model to use', env: 'GEMINI_MODEL_NAME' },
50
+ temperature: { default: DEFAULT_TEMPERATURE, description: 'Sampling temperature (0-1)', env: 'GEMINI_TEMPERATURE' },
51
+ request_timeout: { default: DEFAULT_TIMEOUT, description: 'Request timeout in seconds', env: 'GEMINI_REQUEST_TIMEOUT' }
52
+ }
53
+ end
54
+
55
+ # Initializes the Gemini provider with configuration parameters.
56
+ #
57
+ # @param params [Hash] Configuration parameters for the provider
58
+ def initialize(params)
59
+ super
60
+ @api_key = params[:api_token]
61
+ @model = @params[:model_name] || DEFAULT_MODEL
62
+ @temperature = (@params[:temperature] || DEFAULT_TEMPERATURE).to_f
63
+ @timeout = (@params[:request_timeout] || DEFAULT_TIMEOUT).to_i
64
+ end
65
+
66
+ # Validates the provider configuration.
67
+ # Ensures that the required api_token credential is present.
68
+ #
69
+ # @return [void]
70
+ def validate_config!
71
+ require_credential(:api_token)
72
+ end
73
+
74
+ # Translates text from source locale to target locale using Gemini's API.
75
+ #
76
+ # @param text [String] The text to translate
77
+ # @param source_locale [String] Source language code (e.g., 'en', 'de')
78
+ # @param target_locale [String] Target language code (e.g., 'es', 'fr')
79
+ # @return [String, nil] Translated text or nil on error
80
+ def translate(text, source_locale, target_locale)
81
+ # Build prompt using inherited method
82
+ prompt = build_prompt(text, source_locale, target_locale)
83
+
84
+ # Add Android limitations if needed
85
+ prompt = apply_android_limitations(prompt) if @params[:platform] == 'android'
86
+
87
+ # Make API call
88
+ result = make_api_request(prompt)
89
+
90
+ # Extract text from response
91
+ extract_text_from_response(result)
92
+ rescue StandardError => e
93
+ UI.error "Gemini provider error: #{e.message}"
94
+ nil
95
+ end
96
+
97
+ private
98
+
99
+ # Makes the HTTP API request to Gemini.
100
+ #
101
+ # @param prompt [String] The prompt to send
102
+ # @return [Hash] The parsed JSON response
103
+ def make_api_request(prompt)
104
+ uri = URI("#{API_BASE_URL}/v1beta/models/#{@model}:generateContent")
105
+ uri.query = URI.encode_www_form(key: @api_key)
106
+
107
+ request_body = {
108
+ contents: [{
109
+ role: 'user',
110
+ parts: { text: prompt }
111
+ }],
112
+ generationConfig: {
113
+ temperature: @temperature
114
+ }
115
+ }
116
+
117
+ http = Net::HTTP.new(uri.host, uri.port)
118
+ http.use_ssl = true
119
+ http.read_timeout = @timeout
120
+
121
+ request = Net::HTTP::Post.new(uri.request_uri)
122
+ request['Content-Type'] = 'application/json'
123
+ request.body = request_body.to_json
124
+
125
+ response = http.request(request)
126
+
127
+ unless response.is_a?(Net::HTTPSuccess)
128
+ raise "API request failed: #{response.code} - #{response.message}"
129
+ end
130
+
131
+ JSON.parse(response.body)
132
+ end
133
+
134
+ # Extracts the translated text from the API response.
135
+ #
136
+ # @param response [Hash] The parsed JSON response
137
+ # @return [String, nil] The translated text or nil
138
+ def extract_text_from_response(response)
139
+ candidates = response['candidates']
140
+ return nil if candidates.nil? || candidates.empty?
141
+
142
+ content = candidates.dig(0, 'content')
143
+ return nil if content.nil?
144
+
145
+ parts = content['parts']
146
+ return nil if parts.nil? || parts.empty?
147
+
148
+ parts.dig(0, 'text')&.strip
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end