lara-sdk 1.0.6 → 1.2.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4f716cc4e241ce65c527b569fa22d48c570b974f29a8274c09c46af198e2f3e1
4
- data.tar.gz: 43bc6db5d3199a3959f329d9f6b425341655432a9fbfe308bc424fb18ed3002b
3
+ metadata.gz: 20907c5322ca25cec94d4475f543ba03a04f611c8707711ca5b188b1aac5d708
4
+ data.tar.gz: 41f47a2c9a56145711d5275a9f8db3b1dc9a66e3b514d4d4106cbf2fa69bcb8e
5
5
  SHA512:
6
- metadata.gz: d99be00c98ce8ecb50ccf6e0f3efb0d5a726340136cb3630c18e8ac7b3f2dab0d33fd69742859a6a0a4cfc19b3c89e7f8128d21edef48287c63f13133e665ac8
7
- data.tar.gz: 4ccfd2e79297dff673b1fd06b00ab9a465cdab46c62e104f7a26d47e2ba08945d3d84b6fc22b64bde22078a612d24ab2dccef37b1e09f8b4d3fbbde335b62ed3
6
+ metadata.gz: 323a5b9dbe032b6cc66ca30f42cf599ebd451f29b87c9a7fa7ed0f98769b91a952c57ece507d4bf9a9a120159364c6c224059a4b7a4a09b76ec65c3a0e50f078
7
+ data.tar.gz: 6123b7826259215d940b6e06e75b912756f9351cf750837e793059488472dac3ff3fc47906a4e6922b966873ea9803e240e588d9e9cf80f8c67509e3798136a0
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "json"
4
+ require "base64"
5
+
3
6
  module Lara
4
7
  # JWT authentication token for API access
5
8
  class AuthToken
@@ -8,10 +11,34 @@ module Lara
8
11
  def initialize(token, refresh_token)
9
12
  @token = token
10
13
  @refresh_token = refresh_token
14
+ @expires_at_ms = parse_expires_at_ms(token)
11
15
  end
12
16
 
13
17
  def to_s
14
18
  token
15
19
  end
20
+
21
+ def token_expired?
22
+ @expires_at_ms <= (Time.now.to_f * 1000).to_i + 5_000 # 5 seconds buffer
23
+ end
24
+
25
+ private
26
+
27
+ def parse_expires_at_ms(token)
28
+ return 0 if token.nil? || token.empty?
29
+
30
+ parts = token.split(".")
31
+ return 0 if parts.length != 3
32
+
33
+ b64 = parts[1].tr("-_", "+/")
34
+ b64 += "=" * (4 - (b64.length % 4)) if b64.length % 4 != 0
35
+
36
+ exp = JSON.parse(Base64.decode64(b64))["exp"]
37
+ return 0 unless exp.is_a?(Numeric)
38
+
39
+ (exp * 1000).to_i
40
+ rescue StandardError
41
+ 0
42
+ end
16
43
  end
17
44
  end
data/lib/lara/client.rb CHANGED
@@ -7,6 +7,7 @@ require "openssl"
7
7
  require "base64"
8
8
  require "digest"
9
9
  require "uri"
10
+ require "monitor"
10
11
 
11
12
  module Lara
12
13
  # This class is used to interact with Lara via the REST API.
@@ -31,7 +32,7 @@ module Lara
31
32
  @connection_timeout = connection_timeout
32
33
  @read_timeout = read_timeout
33
34
  @extra_headers = {}
34
- @auth_mutex = Mutex.new
35
+ @auth_mutex = Monitor.new
35
36
 
36
37
  @connection = build_connection
37
38
  end
@@ -93,23 +94,37 @@ module Lara
93
94
  make_request(method, path, body: body, files: files, headers: headers, params: params,
94
95
  raw_response: raw_response, &callback)
95
96
  rescue LaraApiError => e
96
- # Auto-refresh token on 401 with jwt expired
97
- raise unless e.status_code == 401 && e.message.include?("jwt expired")
97
+ raise unless e.status_code == 401
98
98
 
99
- refresh_token
100
- # Retry once with new token
99
+ @auth_mutex.synchronize { refresh_or_reauthenticate }
101
100
  make_request(method, path, body: body, files: files, headers: headers, params: params,
102
101
  raw_response: raw_response, &callback)
103
102
  end
104
103
 
105
104
  def ensure_valid_token
106
105
  @auth_mutex.synchronize do
107
- return if @auth_token
106
+ return if @auth_token && !@auth_token.token_expired?
108
107
 
109
- raise LaraError, "No authentication method available" unless @credentials
108
+ refresh_or_reauthenticate
109
+ end
110
+ end
111
+
112
+ def refresh_or_reauthenticate
113
+ if @auth_token&.refresh_token && !@auth_token.refresh_token.empty?
114
+ begin
115
+ do_refresh
116
+ return
117
+ rescue StandardError
118
+ raise unless @credentials
119
+ end
120
+ end
110
121
 
122
+ if @credentials
111
123
  @auth_token = authenticate
124
+ return
112
125
  end
126
+
127
+ raise LaraError, "No authentication method available for token renewal"
113
128
  end
114
129
 
115
130
  def authenticate
@@ -143,47 +158,37 @@ module Lara
143
158
  raise LaraApiError.from_response(response) unless response.success?
144
159
 
145
160
  data = JSON.parse(response.body)
146
- refresh_token = response.headers["x-lara-refresh-token"]
161
+ refresh_token_value = response.headers["x-lara-refresh-token"]
147
162
 
148
- raise LaraError, "Missing refresh token in authentication response" unless refresh_token
163
+ raise LaraError, "Missing refresh token in authentication response" unless refresh_token_value
149
164
 
150
- AuthToken.new(data["token"], refresh_token)
165
+ AuthToken.new(data["token"], refresh_token_value)
151
166
  end
152
167
 
153
- def refresh_token
154
- @auth_mutex.synchronize do
155
- return unless @auth_token
156
-
157
- conn = Faraday.new(url: @base_url) do |c|
158
- c.adapter Faraday.default_adapter
159
- end
168
+ def do_refresh
169
+ raise LaraError, "No refresh token available" unless @auth_token&.refresh_token
160
170
 
161
- response = conn.post("/v2/auth/refresh") do |req|
162
- req.headers = {
163
- "Date" => Time.now.utc.strftime("%a, %d %b %Y %H:%M:%S GMT"),
164
- "X-Lara-SDK-Name" => "lara-ruby",
165
- "X-Lara-SDK-Version" => Lara::VERSION,
166
- "Authorization" => "Bearer #{@auth_token&.refresh_token}"
167
- }
168
- end
171
+ conn = Faraday.new(url: @base_url) do |c|
172
+ c.adapter Faraday.default_adapter
173
+ end
169
174
 
170
- if response.success?
171
- data = JSON.parse(response.body)
172
- refresh_token = response.headers["x-lara-refresh-token"]
173
-
174
- if refresh_token
175
- @auth_token = AuthToken.new(data["token"], refresh_token)
176
- else
177
- # Refresh failed, force re-authentication
178
- @auth_token = nil
179
- ensure_valid_token
180
- end
181
- else
182
- # Refresh failed, force re-authentication
183
- @auth_token = nil
184
- ensure_valid_token
185
- end
175
+ response = conn.post("/v2/auth/refresh") do |req|
176
+ req.headers = {
177
+ "Date" => Time.now.utc.strftime("%a, %d %b %Y %H:%M:%S GMT"),
178
+ "X-Lara-SDK-Name" => "lara-ruby",
179
+ "X-Lara-SDK-Version" => Lara::VERSION,
180
+ "Authorization" => "Bearer #{@auth_token.refresh_token}"
181
+ }
186
182
  end
183
+
184
+ raise LaraApiError.from_response(response) unless response.success?
185
+
186
+ data = JSON.parse(response.body)
187
+ refresh_token_value = response.headers["x-lara-refresh-token"]
188
+
189
+ raise LaraError, "Missing refresh token in refresh response" unless refresh_token_value
190
+
191
+ @auth_token = AuthToken.new(data["token"], refresh_token_value)
187
192
  end
188
193
 
189
194
  def make_request(method, path, body: nil, files: nil, headers: nil, params: nil, raw_response: false, &callback)
@@ -221,8 +226,14 @@ module Lara
221
226
  end
222
227
 
223
228
  def parse_json(body)
224
- parsed = body.nil? || body.empty? ? {} : JSON.parse(body)
225
- parsed.is_a?(Hash) && parsed.key?("content") ? parsed["content"] : parsed
229
+ return {} if body.nil? || body.empty?
230
+
231
+ parsed = JSON.parse(body)
232
+ if parsed.is_a?(Hash) && parsed.key?("content")
233
+ inner = parsed["content"]
234
+ return inner if inner.is_a?(Hash) || inner.is_a?(Array)
235
+ end
236
+ parsed
226
237
  end
227
238
 
228
239
  def parse_stream_response(body, &block)
data/lib/lara/errors.rb CHANGED
@@ -17,15 +17,16 @@ module Lara
17
17
  class LaraApiError < LaraError
18
18
  attr_reader :type
19
19
 
20
- # Builds an error from an HTTP response with JSON body:
21
- # { "error": { "type": "...", "message": "..." } }
20
+ # Builds an error from an HTTP response with JSON body.
21
+ # Supports both { "error": { "type": "...", "message": "..." } }
22
+ # and { "type": "...", "message": "..." } response formats.
22
23
  def self.from_response(response)
23
24
  data = begin
24
25
  JSON.parse(response.body)
25
26
  rescue StandardError
26
27
  {}
27
28
  end
28
- error = data["error"] || {}
29
+ error = data["error"] || data
29
30
  error_type = error["type"] || "UnknownError"
30
31
  error_message = error["message"] || "An unknown error occurred"
31
32
  new(response.status, error_type, error_message)
@@ -39,6 +39,83 @@ module Lara
39
39
  end
40
40
  end
41
41
 
42
+ class DetectedProfanity < Base
43
+ attr_reader :text, :start_char_index, :end_char_index, :score
44
+
45
+ def initialize(text:, start_char_index:, end_char_index:, score:)
46
+ super()
47
+ @text = text
48
+ @start_char_index = start_char_index
49
+ @end_char_index = end_char_index
50
+ @score = score
51
+ end
52
+ end
53
+
54
+ class ProfanityDetectResult < Base
55
+ attr_reader :masked_text, :profanities
56
+
57
+ def initialize(masked_text:, profanities: [])
58
+ super()
59
+ @masked_text = masked_text
60
+ @profanities = profanities.map do |p|
61
+ DetectedProfanity.new(
62
+ text: p["text"] || p[:text],
63
+ start_char_index: p["start_char_index"] || p[:start_char_index],
64
+ end_char_index: p["end_char_index"] || p[:end_char_index],
65
+ score: p["score"] || p[:score]
66
+ )
67
+ end
68
+ end
69
+ end
70
+
71
+ class StyleguideChange < Base
72
+ attr_reader :id, :original_translation, :refined_translation, :explanation
73
+
74
+ def initialize(id:, original_translation:, refined_translation:, explanation:)
75
+ super()
76
+ @id = id
77
+ @original_translation = original_translation
78
+ @refined_translation = refined_translation
79
+ @explanation = explanation
80
+ end
81
+
82
+ def to_s
83
+ "StyleguideChange{id='#{id}', explanation='#{explanation}'}"
84
+ end
85
+ end
86
+
87
+ class StyleguideResults < Base
88
+ attr_reader :original_translation, :changes
89
+
90
+ def initialize(original_translation:, changes: [])
91
+ super()
92
+ @original_translation = original_translation
93
+ @changes = changes
94
+ end
95
+
96
+ def to_s
97
+ "StyleguideResults{changes=#{changes&.size || 0}}"
98
+ end
99
+ end
100
+
101
+ class Styleguide < Base
102
+ attr_reader :id, :name, :content, :owner_id, :created_at, :updated_at
103
+
104
+ def initialize(id:, name:, content: nil, owner_id: nil, created_at: nil, updated_at: nil, **_kwargs)
105
+ super()
106
+ @id = id
107
+ @name = name
108
+ @content = content
109
+ @owner_id = owner_id
110
+ @created_at = Base.parse_time(created_at)
111
+ @updated_at = Base.parse_time(updated_at)
112
+ end
113
+
114
+ def to_s
115
+ "Styleguide{id='#{id}', name='#{name}'}"
116
+ end
117
+ end
118
+
42
119
  class DetectPrediction < Base
43
120
  attr_reader :language, :confidence
44
121
 
@@ -60,10 +137,20 @@ module Lara
60
137
  end
61
138
  end
62
139
 
140
+ class QualityEstimationResult < Base
141
+ attr_reader :score
142
+
143
+ def initialize(score:)
144
+ super()
145
+ @score = score
146
+ end
147
+ end
148
+
63
149
  class TextResult < Base
64
150
  attr_reader :content_type, :source_language, :translation,
65
151
  :adapted_to, :glossaries,
66
- :adapted_to_matches, :glossaries_matches
152
+ :adapted_to_matches, :glossaries_matches,
153
+ :profanities, :styleguide_results
67
154
 
68
155
  def self.from_hash(hash)
69
156
  return nil unless hash.is_a?(Hash)
@@ -78,6 +165,8 @@ module Lara
78
165
 
79
166
  adapted_to_matches = convert_matches(hash["adapted_to_matches"], NGMemoryMatch)
80
167
  glossaries_matches = convert_matches(hash["glossaries_matches"], NGGlossaryMatch)
168
+ profanities = convert_profanities(hash["profanities"])
169
+ styleguide_results = convert_styleguide_results(hash["styleguide_results"])
81
170
 
82
171
  new(
83
172
  content_type: hash["content_type"],
@@ -86,12 +175,15 @@ module Lara
86
175
  adapted_to: hash["adapted_to"],
87
176
  glossaries: hash["glossaries"],
88
177
  adapted_to_matches: adapted_to_matches,
89
- glossaries_matches: glossaries_matches
178
+ glossaries_matches: glossaries_matches,
179
+ profanities: profanities,
180
+ styleguide_results: styleguide_results
90
181
  )
91
182
  end
92
183
 
93
184
  def initialize(content_type:, source_language:, translation:, adapted_to: nil, glossaries: nil,
94
- adapted_to_matches: nil, glossaries_matches: nil)
185
+ adapted_to_matches: nil, glossaries_matches: nil, profanities: nil,
186
+ styleguide_results: nil)
95
187
  super()
96
188
  @content_type = content_type
97
189
  @source_language = source_language
@@ -100,6 +192,8 @@ module Lara
100
192
  @glossaries = glossaries
101
193
  @adapted_to_matches = adapted_to_matches
102
194
  @glossaries_matches = glossaries_matches
195
+ @profanities = profanities
196
+ @styleguide_results = styleguide_results
103
197
  end
104
198
 
105
199
  class << self
@@ -119,6 +213,46 @@ module Lara
119
213
  end
120
214
  end
121
215
 
216
+ def convert_profanities(value)
217
+ return nil if value.nil?
218
+
219
+ if value.is_a?(Hash)
220
+ ProfanityDetectResult.new(
221
+ masked_text: value["masked_text"],
222
+ profanities: value["profanities"] || []
223
+ )
224
+ elsif value.is_a?(Array)
225
+ value.map do |v|
226
+ next nil if v.nil?
227
+
228
+ ProfanityDetectResult.new(
229
+ masked_text: v["masked_text"],
230
+ profanities: v["profanities"] || []
231
+ )
232
+ end
233
+ end
234
+ end
235
+
236
+ def convert_styleguide_results(value)
237
+ return nil if value.nil?
238
+ return nil unless value.is_a?(Hash)
239
+
240
+ original_translation = value["original_translation"]
241
+ changes = (value["changes"] || []).map do |c|
242
+ StyleguideChange.new(
243
+ id: c["id"],
244
+ original_translation: c["original_translation"],
245
+ refined_translation: c["refined_translation"],
246
+ explanation: c["explanation"]
247
+ )
248
+ end
249
+
250
+ StyleguideResults.new(
251
+ original_translation: original_translation,
252
+ changes: changes
253
+ )
254
+ end
255
+
122
256
  def build_match(klass, h)
123
257
  case klass.name.split("::").last
124
258
  when "NGMemoryMatch"
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lara
4
+ class Styleguides
5
+ def initialize(client)
6
+ @client = client
7
+ end
8
+
9
+ # @return [Array<Lara::Models::Styleguide>]
10
+ def list
11
+ (@client.get("/v2/styleguides") || []).map do |h|
12
+ Lara::Models::Styleguide.new(**h.transform_keys(&:to_sym))
13
+ end
14
+ end
15
+
16
+ # @return [Lara::Models::Styleguide,nil]
17
+ def get(id)
18
+ Lara::Models::Styleguide.new(**@client.get("/v2/styleguides/#{id}").transform_keys(&:to_sym))
19
+ rescue Lara::LaraApiError => e
20
+ return nil if e.status_code == 404
21
+
22
+ raise
23
+ end
24
+ end
25
+ end
@@ -28,12 +28,13 @@ module Lara
28
28
  connection_timeout: connection_timeout, read_timeout: read_timeout)
29
29
  @memories = Memories.new(@client)
30
30
  @glossaries = Glossaries.new(@client)
31
+ @styleguides = Styleguides.new(@client)
31
32
  @documents = Documents.new(@client)
32
33
  @images = Images.new(@client)
33
34
  @audio = AudioTranslator.new(@client)
34
35
  end
35
36
 
36
- attr_reader :client, :memories, :glossaries, :documents, :images, :audio
37
+ attr_reader :client, :memories, :glossaries, :styleguides, :documents, :images, :audio
37
38
 
38
39
  # Translates text with optional tuning parameters.
39
40
  # @param text [String, Array<String>, Array<Lara::Models::TextBlock>]
@@ -55,12 +56,16 @@ module Lara
55
56
  # @param reasoning [Boolean] When true with a block, yields partial results during reasoning
56
57
  # @param headers [Hash,nil]
57
58
  # @param metadata [String, Hash, nil]
59
+ # @param profanity_filter [String,nil] One of "detect", "avoid", "hide"
58
60
  # @yield [Lara::Models::TextResult] Partial translation result (only when reasoning is true)
59
61
  # @return [Lara::Models::TextResult] Final translation result
60
62
  def translate(text, target:, source: nil, source_hint: nil, adapt_to: nil, glossaries: nil,
61
63
  instructions: nil, content_type: nil, multiline: true, timeout_ms: nil,
62
64
  priority: nil, use_cache: nil, cache_ttl_s: nil, no_trace: false, verbose: false,
63
- style: nil, reasoning: false, headers: nil, metadata: nil, &callback)
65
+ style: nil, reasoning: false, headers: nil, metadata: nil,
66
+ profanity_filter: nil,
67
+ styleguide_id: nil, styleguide_reasoning: nil,
68
+ styleguide_explanation_language: nil, &callback)
64
69
  q = normalize_text_input(text)
65
70
 
66
71
  use_cache_value = case use_cache
@@ -86,7 +91,11 @@ module Lara
86
91
  verbose: verbose,
87
92
  style: style,
88
93
  reasoning: reasoning,
89
- metadata: metadata
94
+ metadata: metadata,
95
+ profanity_filter: profanity_filter,
96
+ styleguide_id: styleguide_id,
97
+ styleguide_reasoning: styleguide_reasoning,
98
+ styleguide_explanation_language: styleguide_explanation_language
90
99
  }.compact
91
100
 
92
101
  request_headers = {}
@@ -112,7 +121,7 @@ module Lara
112
121
  body[:passlist] = passlist if passlist&.any?
113
122
  body = body.compact
114
123
 
115
- result = @client.post("/v2/detect", body: body)
124
+ result = @client.post("/v2/detect/language", body: body)
116
125
  Lara::Models::DetectResult.new(
117
126
  language: result["language"],
118
127
  content_type: result["content_type"],
@@ -120,6 +129,48 @@ module Lara
120
129
  )
121
130
  end
122
131
 
132
+ VALID_CONTENT_TYPES = %w[text/plain text/html text/xml application/xliff+xml].freeze
133
+
134
+ # Detects profanities in the given text.
135
+ # @param text [String] Text to check for profanities
136
+ # @param language [String] Language code (e.g. "en")
137
+ # @param content_type [String] One of "text/plain", "text/html", "text/xml", "application/xliff+xml"
138
+ # @return [Lara::Models::ProfanityDetectResult]
139
+ def detect_profanities(text, language:, content_type: "text/plain")
140
+ unless VALID_CONTENT_TYPES.include?(content_type)
141
+ raise ArgumentError, "Invalid content_type '#{content_type}'. Must be one of: #{VALID_CONTENT_TYPES.join(', ')}"
142
+ end
143
+
144
+ body = { text: text, language: language, content_type: content_type }
145
+ result = @client.post("/v2/detect/profanities", body: body)
146
+ Lara::Models::ProfanityDetectResult.new(
147
+ masked_text: result["masked_text"],
148
+ profanities: result["profanities"] || []
149
+ )
150
+ end
151
+
152
+ # Estimates translation quality for a sentence pair (or batch of pairs).
153
+ # @param source [String]
154
+ # @param target [String]
155
+ # @param sentence [String, Array<String>]
156
+ # @param translation [String, Array<String>]
157
+ # @return [Lara::Models::QualityEstimationResult, Array<Lara::Models::QualityEstimationResult>]
158
+ def quality_estimation(source:, target:, sentence:, translation:)
159
+ body = {
160
+ source: source,
161
+ target: target,
162
+ sentence: sentence,
163
+ translation: translation
164
+ }
165
+
166
+ result = @client.post("/v2/detect/quality-estimation", body: body)
167
+ if result.is_a?(Array)
168
+ result.map { |r| Lara::Models::QualityEstimationResult.new(score: r["score"] || r[:score]) }
169
+ else
170
+ Lara::Models::QualityEstimationResult.new(score: result["score"] || result[:score])
171
+ end
172
+ end
173
+
123
174
  # Lists supported language codes.
124
175
  def get_languages
125
176
  @client.get("/v2/languages")
data/lib/lara/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Lara
4
- VERSION = "1.0.6"
4
+ VERSION = "1.2.1"
5
5
  end
data/lib/lara.rb CHANGED
@@ -8,6 +8,7 @@ require_relative "lara/client"
8
8
  require_relative "lara/translator"
9
9
  require_relative "lara/memories"
10
10
  require_relative "lara/glossaries"
11
+ require_relative "lara/styleguides"
11
12
  require_relative "lara/s3_client"
12
13
  require_relative "lara/documents"
13
14
  require_relative "lara/images"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lara-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.6
4
+ version: 1.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Translated
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-03-11 00:00:00.000000000 Z
11
+ date: 2026-04-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -167,6 +167,7 @@ files:
167
167
  - lib/lara/models/memories.rb
168
168
  - lib/lara/models/text.rb
169
169
  - lib/lara/s3_client.rb
170
+ - lib/lara/styleguides.rb
170
171
  - lib/lara/translator.rb
171
172
  - lib/lara/version.rb
172
173
  homepage: https://laratranslate.com