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,136 @@
1
+ require 'openai'
2
+ require_relative 'base_provider'
3
+
4
+ module Fastlane
5
+ module Helper
6
+ module Providers
7
+ # Provider implementation for OpenAI GPT translation API.
8
+ # Supports various GPT models including gpt-5.2 for translation tasks.
9
+ class OpenAIProvider < BaseProvider
10
+ # Default model for OpenAI translations
11
+ DEFAULT_MODEL = 'gpt-5.2'.freeze
12
+
13
+ # Default temperature for translation generation (0.5 = balanced creativity)
14
+ DEFAULT_TEMPERATURE = 0.5
15
+
16
+ # Default request timeout in seconds
17
+ DEFAULT_TIMEOUT = 30
18
+
19
+ # Returns the provider identifier string.
20
+ #
21
+ # @return [String] Provider identifier
22
+ def self.provider_name
23
+ 'openai'
24
+ end
25
+
26
+ # Returns the human-readable display name for the provider.
27
+ #
28
+ # @return [String] Human-readable name
29
+ def self.display_name
30
+ 'OpenAI GPT'
31
+ end
32
+
33
+ # Returns the list of required credential symbols for this provider.
34
+ #
35
+ # @return [Array<Symbol>] Array of required credential keys
36
+ def self.required_credentials
37
+ [:api_token]
38
+ end
39
+
40
+ # Returns a hash of optional parameter definitions for this provider.
41
+ #
42
+ # @return [Hash] Optional parameter definitions
43
+ def self.optional_params
44
+ {
45
+ model_name: { default: DEFAULT_MODEL, description: 'OpenAI model to use' },
46
+ temperature: { default: DEFAULT_TEMPERATURE, description: 'Sampling temperature (0-2)' },
47
+ service_tier: { default: nil, description: 'Service tier (e.g., "flex")' },
48
+ request_timeout: { default: DEFAULT_TIMEOUT, description: 'Request timeout in seconds' }
49
+ }
50
+ end
51
+
52
+ # Initializes the OpenAI provider with configuration parameters.
53
+ # Sets up the OpenAI::Client with appropriate credentials and timeout.
54
+ #
55
+ # @param params [Hash] Configuration parameters for the provider
56
+ def initialize(params)
57
+ super
58
+
59
+ timeout = normalized_timeout
60
+ @client = OpenAI::Client.new(
61
+ access_token: credential(:api_token),
62
+ request_timeout: timeout
63
+ )
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 OpenAI'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 build_prompt 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
+ # Build parameters hash
88
+ parameters = {
89
+ model: @params[:model_name] || DEFAULT_MODEL,
90
+ messages: [{ role: 'user', content: prompt }],
91
+ temperature: (@params[:temperature] || DEFAULT_TEMPERATURE).to_f
92
+ }
93
+
94
+ # Add service_tier if present
95
+ service_tier = @params[:service_tier].to_s.strip
96
+ parameters[:service_tier] = service_tier unless service_tier.empty?
97
+
98
+ # Make API call
99
+ response = @client.chat(parameters: parameters)
100
+
101
+ # Handle errors and extract text
102
+ if (error = response.dig('error', 'message'))
103
+ UI.error "OpenAI translation error: #{error}"
104
+ nil
105
+ else
106
+ response.dig('choices', 0, 'message', 'content')&.strip
107
+ end
108
+ rescue StandardError => e
109
+ UI.error "OpenAI provider error: #{e.message}"
110
+ nil
111
+ end
112
+
113
+ private
114
+
115
+ # Normalizes the request timeout value based on service tier.
116
+ # Flex service tier requires a minimum timeout of 900 seconds.
117
+ #
118
+ # @return [Integer, nil] Normalized timeout value or nil
119
+ def normalized_timeout
120
+ service_tier = @params[:service_tier].to_s.strip
121
+ raw_timeout = @params[:request_timeout]
122
+
123
+ # Use default timeout if not specified
124
+ timeout = raw_timeout.nil? ? DEFAULT_TIMEOUT : raw_timeout.to_i
125
+
126
+ if service_tier == 'flex' && timeout > 0 && timeout < 900
127
+ UI.message('Flex processing detected; increasing request_timeout to 900s.')
128
+ return 900
129
+ end
130
+
131
+ timeout
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,148 @@
1
+ require_relative '../credential_resolver'
2
+ require_relative 'base_provider'
3
+ require_relative 'openai_provider'
4
+ require_relative 'anthropic_provider'
5
+ require_relative 'gemini_provider'
6
+ require_relative 'deepl_provider'
7
+
8
+ module Fastlane
9
+ module Helper
10
+ module Providers
11
+ # ProviderFactory is the central component for creating provider instances.
12
+ # It uses CredentialResolver to resolve API keys and instantiates the
13
+ # appropriate provider based on the provider_name parameter.
14
+ #
15
+ # This class provides a unified interface for creating any supported provider
16
+ # with automatic credential resolution and validation.
17
+ #
18
+ # @example Creating a provider with automatic credential resolution
19
+ # provider = ProviderFactory.create('openai', { model_name: 'gpt-5.2' })
20
+ #
21
+ # @example Creating a provider with explicit API key
22
+ # provider = ProviderFactory.create_with_key('openai', 'sk-...', { model_name: 'gpt-5.2' })
23
+ #
24
+ class ProviderFactory
25
+ # Mapping of provider names to their respective provider classes.
26
+ # Used to look up and instantiate the correct provider implementation.
27
+ PROVIDERS = {
28
+ OpenAIProvider.provider_name => OpenAIProvider,
29
+ AnthropicProvider.provider_name => AnthropicProvider,
30
+ GeminiProvider.provider_name => GeminiProvider,
31
+ DeepLProvider.provider_name => DeepLProvider
32
+ }.freeze
33
+
34
+ # Default provider to use when none is specified.
35
+ DEFAULT_PROVIDER = 'openai'.freeze
36
+
37
+ # Creates a provider instance with automatic credential resolution.
38
+ #
39
+ # This method resolves the API key using CredentialResolver, merges it into
40
+ # the params, and instantiates the appropriate provider class.
41
+ #
42
+ # @param provider_name [String, nil] The provider identifier (e.g., 'openai', 'anthropic').
43
+ # Defaults to DEFAULT_PROVIDER if nil or not provided.
44
+ # @param params [Hash] Configuration parameters for the provider.
45
+ # May include provider-specific options and credential overrides.
46
+ # @return [BaseProvider] An instance of the requested provider class.
47
+ # @raise [FastlaneCore::Interface::FastlaneError] If the provider name is unknown
48
+ # or if no API key can be resolved.
49
+ def self.create(provider_name, params)
50
+ provider_name = provider_name.to_s.empty? ? DEFAULT_PROVIDER : provider_name.to_s.downcase
51
+ provider_class = PROVIDERS[provider_name]
52
+
53
+ unless provider_class
54
+ UI.user_error!("Unknown provider '#{provider_name}'. Available: #{available_provider_names.join(', ')}")
55
+ return nil
56
+ end
57
+
58
+ # Resolve API key
59
+ api_key = CredentialResolver.resolve(provider_name, params)
60
+
61
+ unless api_key
62
+ UI.user_error!("No API key found for provider '#{provider_name}'. #{CredentialResolver.credential_help(provider_name)}")
63
+ return nil
64
+ end
65
+
66
+ # Merge API key into params
67
+ provider_params = params.merge(api_token: api_key)
68
+
69
+ provider_class.new(provider_params)
70
+ end
71
+
72
+ # Creates a provider instance with an explicit API key.
73
+ #
74
+ # This method bypasses credential resolution and uses the provided API key
75
+ # directly. Useful when the key is obtained from an external source or
76
+ # when credential resolution is not desired.
77
+ #
78
+ # @param provider_name [String] The provider identifier (e.g., 'openai', 'anthropic').
79
+ # @param api_key [String] The API key to use for authentication.
80
+ # @param params [Hash] Optional configuration parameters for the provider.
81
+ # @return [BaseProvider] An instance of the requested provider class.
82
+ # @raise [FastlaneCore::Interface::FastlaneError] If the provider name is unknown.
83
+ def self.create_with_key(provider_name, api_key, params = {})
84
+ provider_name = provider_name.to_s.downcase
85
+ provider_class = PROVIDERS[provider_name]
86
+
87
+ unless provider_class
88
+ UI.user_error!("Unknown provider '#{provider_name}'")
89
+ return nil
90
+ end
91
+
92
+ provider_params = params.merge(api_token: api_key)
93
+ provider_class.new(provider_params)
94
+ end
95
+
96
+ # Returns an array of all available provider names.
97
+ #
98
+ # @return [Array<String>] Array of provider identifiers.
99
+ def self.available_provider_names
100
+ PROVIDERS.keys.freeze
101
+ end
102
+
103
+ # Returns a hash mapping provider names to their display names.
104
+ #
105
+ # @return [Hash<String, String>] Hash with provider names as keys and
106
+ # human-readable display names as values.
107
+ def self.provider_display_names
108
+ PROVIDERS.transform_values(&:display_name)
109
+ end
110
+
111
+ # Checks if a provider name is valid.
112
+ #
113
+ # @param provider_name [String] The provider identifier to check.
114
+ # @return [Boolean] true if the provider is supported, false otherwise.
115
+ def self.valid_provider?(provider_name)
116
+ PROVIDERS.key?(provider_name.to_s.downcase)
117
+ end
118
+
119
+ # Gets the full configuration for a provider.
120
+ #
121
+ # Returns a comprehensive hash containing all configuration details
122
+ # for the specified provider, including name, display name, required
123
+ # credentials, optional parameters, and credential help text.
124
+ #
125
+ # @param provider_name [String] The provider identifier.
126
+ # @return [Hash] Provider configuration hash with keys:
127
+ # - :name [String] Provider identifier
128
+ # - :display_name [String] Human-readable name
129
+ # - :required_credentials [Array<Symbol>] Required credential symbols
130
+ # - :optional_params [Hash] Optional parameter definitions
131
+ # - :credential_help [String] Help text for configuring credentials
132
+ # @return [Hash] Empty hash if provider is not found.
133
+ def self.provider_config(provider_name)
134
+ provider_class = PROVIDERS[provider_name.to_s.downcase]
135
+ return {} unless provider_class
136
+
137
+ {
138
+ name: provider_class.provider_name,
139
+ display_name: provider_class.display_name,
140
+ required_credentials: provider_class.required_credentials,
141
+ optional_params: provider_class.optional_params,
142
+ credential_help: CredentialResolver.credential_help(provider_name)
143
+ }
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
@@ -1,6 +1,5 @@
1
1
  require 'fastlane_core/ui/ui'
2
- require 'openai'
3
- require 'json'
2
+ require_relative 'providers/provider_factory'
4
3
 
5
4
  module Fastlane
6
5
  UI = FastlaneCore::UI unless Fastlane.const_defined?("UI")
@@ -9,65 +8,29 @@ module Fastlane
9
8
  class TranslateGptReleaseNotesHelper
10
9
  def initialize(params)
11
10
  @params = params
12
- @params[:request_timeout] = normalize_request_timeout(@params)
13
- @client = OpenAI::Client.new(
14
- access_token: params[:api_token],
15
- request_timeout: @params[:request_timeout]
16
- )
17
- end
18
-
19
- # Request a translation from the GPT API
20
- def translate_text(text, target_locale, platform)
21
- source_locale = @params[:master_locale]
22
- prompt = "Translate this text from #{source_locale} to #{target_locale}:\n#{text}"
23
-
24
-
25
- # Add condition for Android platform
26
- if platform == 'android'
27
- prompt += "\n\nNote: The length of the translated text should be 500 symbols maximum. Rephrase a little if needed."
28
- end
11
+ provider_name = params[:provider] || 'openai'
29
12
 
30
- # Context handling
31
- if @params[:context] && !@params[:context].empty?
32
- prompt = "Context: #{@params[:context]}\n" + prompt
13
+ # Validate provider selection
14
+ unless Providers::ProviderFactory.valid_provider?(provider_name)
15
+ UI.warning "Unknown provider '#{provider_name}', falling back to OpenAI"
16
+ provider_name = 'openai'
33
17
  end
34
18
 
35
- # Updated API call with max_tokens
36
- parameters = {
37
- model: @params[:model_name] || 'gpt-5.2',
38
- messages: [{ role: "user", content: prompt }],
39
- temperature: @params[:temperature] || 0.5
40
- }
19
+ # Create provider via factory (handles credential resolution)
20
+ @provider = Providers::ProviderFactory.create(provider_name, params)
41
21
 
42
- service_tier = @params[:service_tier].to_s.strip
43
- parameters[:service_tier] = service_tier unless service_tier.empty?
44
-
45
- response = @client.chat(parameters: parameters)
46
-
47
-
48
- error = response.dig("error", "message")
49
- if error
50
- UI.error "Error translating text: #{error}"
51
- return nil
52
- else
53
- translated_text = response.dig("choices", 0, "message", "content").strip
54
- UI.message "Translated text: #{translated_text}"
55
- return translated_text
22
+ # Validate provider configuration
23
+ unless @provider.valid?
24
+ UI.user_error!("Provider configuration errors: #{@provider.config_errors.join(', ')}")
56
25
  end
57
26
  end
58
27
 
59
- def normalize_request_timeout(params)
60
- service_tier = params[:service_tier].to_s.strip
61
- raw_timeout = params[:request_timeout]
62
- return nil if raw_timeout.nil?
63
- timeout = raw_timeout.to_i
64
- if service_tier == "flex" && timeout > 0 && timeout < 900
65
- UI.message("Flex processing detected; increasing request_timeout to 900s.")
66
- return 900
67
- end
68
- timeout
28
+ # Request a translation from the configured provider
29
+ def translate_text(text, target_locale, _platform)
30
+ source_locale = @params[:master_locale]
31
+ @provider.translate(text, source_locale, target_locale)
69
32
  end
70
-
33
+
71
34
  # Sleep for a specified number of seconds, displaying a progress bar
72
35
  def wait(seconds = @params[:request_timeout])
73
36
  sleep_time = 0
@@ -105,7 +68,7 @@ module Fastlane
105
68
  white: 37,
106
69
  reset: 0,
107
70
  }
108
-
71
+
109
72
  def self.colorize(text, color)
110
73
  color_code = COLORS[color.to_sym]
111
74
  "\e[#{color_code}m#{text}\e[0m"
@@ -1,5 +1,5 @@
1
1
  module Fastlane
2
2
  module TranslateGptReleaseNotes
3
- VERSION = "0.1.1"
3
+ VERSION = "0.2.0"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fastlane-plugin-translate_gpt_release_notes
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Anton Karliner
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-01-07 00:00:00.000000000 Z
11
+ date: 2026-01-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ruby-openai
@@ -52,6 +52,48 @@ dependencies:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: 1.18.9
55
+ - !ruby/object:Gem::Dependency
56
+ name: anthropic
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.16'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.16'
69
+ - !ruby/object:Gem::Dependency
70
+ name: deepl-rb
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.6'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.6'
83
+ - !ruby/object:Gem::Dependency
84
+ name: openssl
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: 3.2.0
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: 3.2.0
55
97
  - !ruby/object:Gem::Dependency
56
98
  name: bundler
57
99
  requirement: !ruby/object:Gem::Requirement
@@ -202,6 +244,13 @@ files:
202
244
  - README.md
203
245
  - lib/fastlane/plugin/translate_gpt_release_notes.rb
204
246
  - lib/fastlane/plugin/translate_gpt_release_notes/actions/translate_gpt_release_notes_action.rb
247
+ - lib/fastlane/plugin/translate_gpt_release_notes/helper/credential_resolver.rb
248
+ - lib/fastlane/plugin/translate_gpt_release_notes/helper/providers/anthropic_provider.rb
249
+ - lib/fastlane/plugin/translate_gpt_release_notes/helper/providers/base_provider.rb
250
+ - lib/fastlane/plugin/translate_gpt_release_notes/helper/providers/deepl_provider.rb
251
+ - lib/fastlane/plugin/translate_gpt_release_notes/helper/providers/gemini_provider.rb
252
+ - lib/fastlane/plugin/translate_gpt_release_notes/helper/providers/openai_provider.rb
253
+ - lib/fastlane/plugin/translate_gpt_release_notes/helper/providers/provider_factory.rb
205
254
  - lib/fastlane/plugin/translate_gpt_release_notes/helper/translate_gpt_release_notes_helper.rb
206
255
  - lib/fastlane/plugin/translate_gpt_release_notes/version.rb
207
256
  homepage: https://github.com/antonkarliner/fastlane-plugin-translate_gpt_release_notes
@@ -229,6 +278,5 @@ requirements: []
229
278
  rubygems_version: 3.4.10
230
279
  signing_key:
231
280
  specification_version: 4
232
- summary: Translate release notes or changelogs for iOS and Android apps using OpenAI
233
- GPT API
281
+ summary: 'Translate release notes using AI providers: OpenAI, Claude, Gemini, or DeepL'
234
282
  test_files: []