fastlane-plugin-translate_gpt_release_notes 0.1.0 → 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 +352 -40
- data/lib/fastlane/plugin/translate_gpt_release_notes/actions/translate_gpt_release_notes_action.rb +80 -5
- 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 +19 -40
- data/lib/fastlane/plugin/translate_gpt_release_notes/version.rb +1 -1
- metadata +52 -4
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
module Fastlane
|
|
2
|
+
module Helper
|
|
3
|
+
# CredentialResolver manages multiple provider API keys simultaneously,
|
|
4
|
+
# allowing users to configure keys for all providers and select which
|
|
5
|
+
# to use via the provider parameter.
|
|
6
|
+
#
|
|
7
|
+
# This class provides a centralized way to resolve API credentials from
|
|
8
|
+
# various sources (parameters, environment variables) with a defined
|
|
9
|
+
# priority order.
|
|
10
|
+
class CredentialResolver
|
|
11
|
+
# Maps each provider to its credential configuration
|
|
12
|
+
# Each provider has:
|
|
13
|
+
# - env_vars: Array of environment variable names to check (in order)
|
|
14
|
+
# - param_key: Symbol for the parameter key in the params hash
|
|
15
|
+
PROVIDER_CREDENTIALS = {
|
|
16
|
+
'openai' => {
|
|
17
|
+
env_vars: ['OPENAI_API_KEY', 'GPT_API_KEY'], # GPT_API_KEY for backward compatibility
|
|
18
|
+
param_key: :openai_api_key
|
|
19
|
+
},
|
|
20
|
+
'anthropic' => {
|
|
21
|
+
env_vars: ['ANTHROPIC_API_KEY'],
|
|
22
|
+
param_key: :anthropic_api_key
|
|
23
|
+
},
|
|
24
|
+
'gemini' => {
|
|
25
|
+
env_vars: ['GEMINI_API_KEY'],
|
|
26
|
+
param_key: :gemini_api_key
|
|
27
|
+
},
|
|
28
|
+
'deepl' => {
|
|
29
|
+
env_vars: ['DEEPL_API_KEY'],
|
|
30
|
+
param_key: :deepl_api_key
|
|
31
|
+
}
|
|
32
|
+
}.freeze
|
|
33
|
+
|
|
34
|
+
# Resolves the API key for a given provider following priority order:
|
|
35
|
+
# 1. Direct parameter (e.g., params[:openai_api_key])
|
|
36
|
+
# 2. Environment variables in order defined in PROVIDER_CREDENTIALS
|
|
37
|
+
# 3. Legacy fallback for OpenAI (GPT_API_KEY) if defined in env_vars
|
|
38
|
+
#
|
|
39
|
+
# @param provider_name [String] The provider identifier (e.g., 'openai', 'anthropic')
|
|
40
|
+
# @param params [Hash] Hash of parameters that may contain API keys
|
|
41
|
+
# @return [String, nil] The resolved API key or nil if not found
|
|
42
|
+
def self.resolve(provider_name, params = {})
|
|
43
|
+
config = provider_config(provider_name)
|
|
44
|
+
return nil unless config
|
|
45
|
+
|
|
46
|
+
# Priority 1: Check direct parameter
|
|
47
|
+
param_value = params[config[:param_key]]
|
|
48
|
+
return param_value.to_s.strip unless param_value.nil? || param_value.to_s.strip.empty?
|
|
49
|
+
|
|
50
|
+
# Priority 2: Check environment variables in order
|
|
51
|
+
config[:env_vars].each do |env_var|
|
|
52
|
+
env_value = ENV[env_var]
|
|
53
|
+
return env_value.to_s.strip unless env_value.nil? || env_value.to_s.strip.empty?
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
nil
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Checks if credentials exist for a given provider.
|
|
60
|
+
#
|
|
61
|
+
# @param provider_name [String] The provider identifier
|
|
62
|
+
# @param params [Hash] Hash of parameters that may contain API keys
|
|
63
|
+
# @return [Boolean] true if credentials exist, false otherwise
|
|
64
|
+
def self.credentials_exist?(provider_name, params = {})
|
|
65
|
+
!resolve(provider_name, params).nil?
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Returns an array of provider names that have configured credentials.
|
|
69
|
+
#
|
|
70
|
+
# @param params [Hash] Hash of parameters that may contain API keys
|
|
71
|
+
# @return [Array<String>] Array of provider names with valid credentials
|
|
72
|
+
def self.available_providers(params = {})
|
|
73
|
+
PROVIDER_CREDENTIALS.keys.select do |provider_name|
|
|
74
|
+
credentials_exist?(provider_name, params)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Returns help text explaining how to configure credentials for a provider.
|
|
79
|
+
#
|
|
80
|
+
# @param provider_name [String] The provider identifier
|
|
81
|
+
# @return [String] Help text for configuring credentials
|
|
82
|
+
def self.credential_help(provider_name)
|
|
83
|
+
config = provider_config(provider_name)
|
|
84
|
+
return "Unknown provider: #{provider_name}" unless config
|
|
85
|
+
|
|
86
|
+
env_vars = config[:env_vars]
|
|
87
|
+
param_key = config[:param_key]
|
|
88
|
+
|
|
89
|
+
if env_vars.length == 1
|
|
90
|
+
"Set #{env_vars.first} environment variable, or pass :#{param_key} parameter"
|
|
91
|
+
else
|
|
92
|
+
env_vars_str = env_vars.join(' or ')
|
|
93
|
+
"Set #{env_vars_str} environment variable, or pass :#{param_key} parameter"
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Returns an array of all supported provider names.
|
|
98
|
+
#
|
|
99
|
+
# @return [Array<String>] Array of all supported provider names
|
|
100
|
+
def self.all_providers
|
|
101
|
+
PROVIDER_CREDENTIALS.keys.freeze
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
# Retrieves the credential configuration for a provider.
|
|
107
|
+
# Handles case-insensitive provider names by downcasing.
|
|
108
|
+
#
|
|
109
|
+
# @param provider_name [String] The provider identifier
|
|
110
|
+
# @return [Hash, nil] The credential configuration or nil if provider not found
|
|
111
|
+
def self.provider_config(provider_name)
|
|
112
|
+
return nil if provider_name.nil?
|
|
113
|
+
|
|
114
|
+
PROVIDER_CREDENTIALS[provider_name.to_s.downcase]
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -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
|