locallingo 0.5.0 → 0.6.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.
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../state_store"
3
+ require_relative "../drift"
4
4
 
5
5
  module Locallingo
6
6
  module Validators
@@ -12,19 +12,15 @@ module Locallingo
12
12
  # it was edited by a human — surface it so the operator can protect it with
13
13
  # `accept-edits` before the next translate run overwrites it.
14
14
  class ManualEdits
15
- def initialize(cli_name: "lingo")
15
+ def initialize(cli_name: "lingo", drift: Drift.new)
16
16
  @cli_name = cli_name
17
+ @drift = drift
17
18
  end
18
19
 
19
20
  # +target+ is the flat target hash; +locale_state+ its loaded state.
20
21
  def call(target:, locale_state:, locale:)
21
22
  target.filter_map do |key, value|
22
- entry = locale_state[key]
23
- next unless entry.is_a?(Hash)
24
- next if entry["manual"]
25
-
26
- stored = entry["target_hash"]
27
- next unless stored && stored != StateStore.hash(value)
23
+ next unless @drift.hand_edited?(target: value, entry: locale_state[key])
28
24
 
29
25
  {
30
26
  type: :manual_edit,
@@ -1,14 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../state_store"
3
+ require_relative "../drift"
4
4
 
5
5
  module Locallingo
6
6
  module Validators
7
7
  # Reports target keys whose recorded source hash no longer matches the
8
8
  # current source value — i.e. the English text changed after translation.
9
9
  class Outdated
10
- def initialize(cli_name: "lingo")
10
+ def initialize(cli_name: "lingo", drift: Drift.new)
11
11
  @cli_name = cli_name
12
+ @drift = drift
12
13
  end
13
14
 
14
15
  # +source+ is the flat source hash; +locale_state+ is the target locale's
@@ -27,8 +28,7 @@ module Locallingo
27
28
  # Keys whose stored source_hash differs from the current source hash.
28
29
  def outdated_keys(source, locale_state)
29
30
  source.filter_map do |key, value|
30
- stored = locale_state.dig(key, "source_hash")
31
- key if stored && stored != StateStore.hash(value)
31
+ key if @drift.outdated?(source: value, entry: locale_state[key])
32
32
  end
33
33
  end
34
34
 
@@ -37,7 +37,7 @@ module Locallingo
37
37
  # Manual keys must not be pushed toward machine retranslation — the value
38
38
  # is hand-curated, so the human updates it and re-accepts.
39
39
  def suggestion_for(key, locale, locale_state)
40
- if locale_state.dig(key, "manual")
40
+ if @drift.manual?(locale_state[key])
41
41
  "Source changed for a manually-curated key. Update the #{locale} value by hand, " \
42
42
  "then run: #{@cli_name} accept-edits --locale #{locale} --key #{key}"
43
43
  else
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Locallingo
4
- VERSION = "0.5.0"
4
+ VERSION = "0.6.0"
5
5
  end
data/lib/locallingo.rb CHANGED
@@ -1,13 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "monitor"
3
4
  require_relative "locallingo/version"
4
5
  require_relative "locallingo/settings"
5
6
  require_relative "locallingo/configuration"
6
7
  require_relative "locallingo/json_extraction"
7
8
  require_relative "locallingo/key_flattener"
8
9
  require_relative "locallingo/state_store"
10
+ require_relative "locallingo/drift"
9
11
  require_relative "locallingo/providers/ruby_llm"
10
12
  require_relative "locallingo/batch_translator"
13
+ require_relative "locallingo/prompt"
14
+ require_relative "locallingo/translator"
15
+ require_relative "locallingo/record_translator"
11
16
  require_relative "locallingo/validators/missing"
12
17
  require_relative "locallingo/validators/outdated"
13
18
  require_relative "locallingo/validators/duplicate_values"
@@ -38,20 +43,59 @@ module Locallingo
38
43
  Configuration.load(root_path:, package:)
39
44
  end
40
45
 
41
- # Code-level settings (provider credentials) — see Locallingo::Settings.
42
- def self.settings
43
- @settings ||= Settings.new
46
+ # Boot-time state, finalized: always a frozen Settings, so any thread or
47
+ # Ractor reads finished settings and only `configure` replaces them. With
48
+ # String keys (or none) it is Ractor-shareable; a callable key is not, so it
49
+ # keeps the settings readable on the main Ractor only.
50
+ #
51
+ # rubocop:disable ThreadSafety/ClassInstanceVariable -- @settings is written only by configure and reset_settings!, at boot on the main Ractor, and always holds a frozen Settings; @configure_lock (engines without Ractor only) is set once, at load
52
+ @settings = Settings.new.freeze
53
+
54
+ # Serializes configure: one Monitor per Ractor, in Ractor-local storage (only
55
+ # the main Ractor can publish @settings, so in practice the main Ractor's).
56
+ # The main Ractor's is made here, at load, never lazily. A Monitor, so a
57
+ # configure nested in a configure block does not deadlock. Engines without
58
+ # Ractor keep it in @configure_lock.
59
+ if defined?(::Ractor)
60
+ ::Ractor.current[:locallingo_configure_lock] = Monitor.new
61
+ else
62
+ @configure_lock = Monitor.new
44
63
  end
45
64
 
65
+ # Code-level settings (provider credentials) — see Locallingo::Settings.
66
+ def self.settings = @settings
67
+
46
68
  # Configure the gem from Ruby — the credentials path for apps whose keys
47
69
  # don't live in ENV (call it from a Rails initializer or a `.locallingo.rb`
48
70
  # setup file next to `.locallingo.yml`; the CLI loads the latter on start).
71
+ # Yields a copy of the current settings, then publishes it frozen in one
72
+ # assignment: a concurrent reader sees the old settings or the new, never
73
+ # half of them. Concurrent calls run one at a time, so neither drops the
74
+ # other's keys. Calling it again keeps the keys it does not set.
49
75
  def self.configure
50
- yield settings
51
- settings
76
+ configure_lock.synchronize do
77
+ draft = settings.dup
78
+ yield draft
79
+ @settings = draft.freeze
80
+ end
52
81
  end
53
82
 
83
+ # Back to frozen, blank defaults (for tests).
54
84
  def self.reset_settings!
55
- @settings = nil
85
+ @settings = Settings.new.freeze
86
+ end
87
+
88
+ # Another Ractor's lock is made on first use: atomically on Ruby 3.4+; below,
89
+ # two of its threads could each make one, which is harmless because a
90
+ # non-main Ractor cannot publish settings anyway.
91
+ def self.configure_lock
92
+ return @configure_lock unless defined?(::Ractor)
93
+
94
+ key = :locallingo_configure_lock
95
+ return ::Ractor.store_if_absent(key) { Monitor.new } if ::Ractor.respond_to?(:store_if_absent)
96
+
97
+ ::Ractor.current[key] ||= Monitor.new
56
98
  end
99
+ private_class_method :configure_lock
100
+ # rubocop:enable ThreadSafety/ClassInstanceVariable
57
101
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: locallingo
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mikael Henriksson
@@ -69,21 +69,27 @@ files:
69
69
  - config/locallingo.default.yml
70
70
  - exe/lingo
71
71
  - lib/locallingo.rb
72
+ - lib/locallingo/active_record.rb
72
73
  - lib/locallingo/batch_translator.rb
73
74
  - lib/locallingo/cli.rb
74
75
  - lib/locallingo/configuration.rb
76
+ - lib/locallingo/content_leaves.rb
77
+ - lib/locallingo/drift.rb
75
78
  - lib/locallingo/json_extraction.rb
76
79
  - lib/locallingo/key_flattener.rb
77
80
  - lib/locallingo/manager.rb
81
+ - lib/locallingo/prompt.rb
78
82
  - lib/locallingo/providers/ruby_llm.rb
79
83
  - lib/locallingo/quality/british_spellings.rb
80
84
  - lib/locallingo/quality/static_rules.rb
81
85
  - lib/locallingo/quality/terminology.rb
82
86
  - lib/locallingo/quality_checker.rb
87
+ - lib/locallingo/record_translator.rb
83
88
  - lib/locallingo/reporter.rb
84
89
  - lib/locallingo/rubocop.rb
85
90
  - lib/locallingo/settings.rb
86
91
  - lib/locallingo/state_store.rb
92
+ - lib/locallingo/translator.rb
87
93
  - lib/locallingo/validators/duplicate_values.rb
88
94
  - lib/locallingo/validators/manual_edits.rb
89
95
  - lib/locallingo/validators/missing.rb