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.
- checksums.yaml +4 -4
- data/README.md +351 -47
- data/lib/fastlane/plugin/translate_gpt_release_notes/actions/translate_gpt_release_notes_action.rb +64 -3
- data/lib/fastlane/plugin/translate_gpt_release_notes/helper/credential_resolver.rb +118 -0
- data/lib/fastlane/plugin/translate_gpt_release_notes/helper/providers/anthropic_provider.rb +119 -0
- data/lib/fastlane/plugin/translate_gpt_release_notes/helper/providers/base_provider.rb +161 -0
- data/lib/fastlane/plugin/translate_gpt_release_notes/helper/providers/deepl_provider.rb +145 -0
- data/lib/fastlane/plugin/translate_gpt_release_notes/helper/providers/gemini_provider.rb +153 -0
- data/lib/fastlane/plugin/translate_gpt_release_notes/helper/providers/openai_provider.rb +136 -0
- data/lib/fastlane/plugin/translate_gpt_release_notes/helper/providers/provider_factory.rb +148 -0
- data/lib/fastlane/plugin/translate_gpt_release_notes/helper/translate_gpt_release_notes_helper.rb +17 -54
- data/lib/fastlane/plugin/translate_gpt_release_notes/version.rb +1 -1
- metadata +52 -4
|
@@ -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
|