locallingo 0.2.2 → 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 931aa3bf94ee48a2021c8ec6f5de047e593f2f94bc5ba01e20b48bd1b2e96d3c
4
- data.tar.gz: fcbab1174c9dbefb317fdae26a0c0a763f4a3b8026b19f8fe8f08a54d222f8fb
3
+ metadata.gz: 398a4540431fa5298a1eba1fedecef3c4b7344d2749f935a30882d8899ebcb69
4
+ data.tar.gz: c854c5c26ebee79b0d5663614207e0f75368c973058a5ac320f58ec4e45e3835
5
5
  SHA512:
6
- metadata.gz: 9bc87c498dcc47fbda209f58005ce265f09ef5432107bbced4f9185e32ae51873b6aae014f7f11f650cd4905328169cb32e8f477c9ab1d310f2b4692da350ce8
7
- data.tar.gz: 65f2c9cbf024baa4cb89202da61fff2d4feb0de544c2ea85371525714e83a322ac93f065ae08239e67f562d0158b0ddf01acd199e9d9060614f3c02afa8f37a5
6
+ metadata.gz: 5969d9cac09643f81e6e37ae72ad79003373d1870bbaf37300c0e4c5a95449c6c422176aa6ea1938b66a53889669bbf3c0a0f5525e56d7f0afd0e8c9c2c33c44
7
+ data.tar.gz: 22e5a7d6ff926e04baa3e0de58d7cea8d4993356a3ac931de745dbc8e817298b99b2ff591b4e353e1adc9052dd44307fce4cd957605fcd5527c448e83e8d40f4
data/CHANGELOG.md CHANGED
@@ -6,7 +6,36 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ### Fixed
10
+ - `lingo sync` no longer destroys hand-edit protection: it now only refreshes
11
+ each entry's `source_hash` and preserves `target_hash` and `manual: true`
12
+ (the pre-extraction `bin/translate` behavior). Previously a single sync wiped
13
+ both fields for every key, blinding the `manual_edits` validator and causing
14
+ `manual` flags to flip back and forth across branches.
15
+ - `translate --force-key` on a `manual`-flagged key keeps the flag. The value is
16
+ still retranslated on explicit request, but no command removes `manual: true`
17
+ anymore — unprotecting a key requires editing the state JSON by hand.
18
+ - State files whose content is unchanged are no longer rewritten, so operations
19
+ never touch `.i18n-state/` files for unrelated namespaces.
20
+
21
+ ### Changed
22
+ - Unscoped `lingo accept-edits` now accepts only the keys the `manual_edits`
23
+ validator flags (actually hand-edited), instead of stamping `manual: true` on
24
+ every key of the locale. Use the new `--key KEY` (repeatable) for surgical
25
+ accepts, or `--all` for the old blanket behavior (initial adoption).
26
+ - Validator suggestions are scoped and safe to follow verbatim: `manual_edit`
27
+ violations suggest `accept-edits --locale <l> --key <key>`, and `outdated`
28
+ violations on manual keys tell you to update the value by hand and re-accept
29
+ it rather than force-translating over curated text.
30
+
9
31
  ### Added
32
+ - `Locallingo.configure { |c| c.anthropic_api_key = ... }` — gem-level provider
33
+ credentials as Strings or lazy callables, for apps whose keys don't live in
34
+ ENV (Rails credentials, app config objects, vaults). Precedence:
35
+ `Locallingo.configure` → host `RubyLLM.configure` → ENV.
36
+ - `.locallingo.rb` setup file: the CLI loads it from the project root before
37
+ dispatch, so standalone `lingo` runs can configure credentials without
38
+ booting Rails.
10
39
  - Initial extraction from the `bin/translate` toolchain into a standalone gem.
11
40
  - `lingo` CLI with subcommands: `status`, `translate`, `validate`, `quality`,
12
41
  `fix-quality`, `accept-edits`, `hash`, `sync`. Legacy `--flag` forms still work
data/README.md CHANGED
@@ -32,8 +32,21 @@ group :development do
32
32
  end
33
33
  ```
34
34
 
35
- RubyLLM reads provider credentials from ENV (e.g. `OPENAI_API_KEY`,
36
- `ANTHROPIC_API_KEY`). Locallingo never stores keys.
35
+ Provider credentials come from ENV (e.g. `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`)
36
+ or from Ruby, for apps whose keys live elsewhere (Rails credentials, an app
37
+ config object, a vault):
38
+
39
+ ```ruby
40
+ Locallingo.configure do |config|
41
+ config.anthropic_api_key = "sk-ant-..." # a String…
42
+ config.openai_api_key = -> { AppConf.openai_key } # …or a lazy callable
43
+ end
44
+ ```
45
+
46
+ Put that in a `.locallingo.rb` file next to `.locallingo.yml` and the `lingo`
47
+ CLI loads it on start — no Rails boot required. A key set via
48
+ `Locallingo.configure` wins over one set through `RubyLLM.configure`, which
49
+ wins over ENV. Locallingo never stores keys itself.
37
50
 
38
51
  ## Configuration
39
52
 
@@ -91,8 +104,10 @@ lingo validate --strict # CI gate (exit 1 on strict-tier issues)
91
104
  lingo validate --strict-all # stricter CI gate
92
105
  lingo quality --ai # quality linting (+ optional AI pass)
93
106
  lingo fix-quality --locale en # auto-fix fixable issues
94
- lingo accept-edits --locale de # mark current translations as intentional
95
- lingo sync # rebuild drift state from current files
107
+ lingo accept-edits --locale de # protect hand-edited translations (drifted keys only)
108
+ lingo accept-edits --key a.b.c # protect a specific key
109
+ lingo accept-edits --all # protect everything (initial adoption)
110
+ lingo sync # refresh drift state (preserves manual flags)
96
111
  lingo hash # source-translation fingerprint
97
112
  ```
98
113
 
@@ -16,6 +16,11 @@ module Locallingo
16
16
  class CLI
17
17
  CLI_NAME = "lingo"
18
18
 
19
+ # Optional Ruby setup file loaded before dispatch — the hook for apps to
20
+ # configure credentials (via Locallingo.configure or RubyLLM.configure)
21
+ # without booting Rails.
22
+ SETUP_FILENAME = ".locallingo.rb"
23
+
19
24
  # subcommand => the legacy flag it replaces
20
25
  COMMANDS = {
21
26
  "status" => "--status",
@@ -52,6 +57,7 @@ module Locallingo
52
57
  def run
53
58
  command = resolve_command
54
59
  options = parse_options!
60
+ load_setup_file
55
61
  config = Locallingo.configuration(root_path: Dir.pwd, package: options[:package])
56
62
  dispatch(command, config, options)
57
63
  rescue Locallingo::Error => e
@@ -61,6 +67,13 @@ module Locallingo
61
67
 
62
68
  private
63
69
 
70
+ # An absent file is fine; errors in the file propagate loudly. `load` (not
71
+ # require) so repeated in-process invocations re-execute it.
72
+ def load_setup_file
73
+ path = File.join(Dir.pwd, SETUP_FILENAME)
74
+ load path if File.file?(path)
75
+ end
76
+
64
77
  # Determine the subcommand, translating a leading legacy flag (with a
65
78
  # deprecation notice) and defaulting to `status`.
66
79
  def resolve_command
@@ -96,6 +109,10 @@ module Locallingo
96
109
  opts.on("--force-key KEY", "Force re-translation of a specific key") do |v|
97
110
  (@options[:force_keys] ||= []) << v
98
111
  end
112
+ opts.on("--key KEY", "accept-edits: accept a specific key (repeatable)") do |v|
113
+ (@options[:keys] ||= []) << v
114
+ end
115
+ opts.on("--all", "accept-edits: mark every translated key as manual") { @options[:all] = true }
99
116
  opts.on("-v", "--verbose", "Verbose output") { @options[:verbose] = true }
100
117
  opts.on("-n", "--dry-run", "Show what would be done without changing files") { @options[:dry_run] = true }
101
118
  opts.on("--strict", "Fail on strict-tier issues (for CI)") { @options[:strict] = true }
@@ -172,11 +189,25 @@ module Locallingo
172
189
  warn "manual_edits validator is disabled in .locallingo.yml — nothing to accept."
173
190
  return
174
191
  end
175
- manager(config, options).accept_edits!(locale: options[:locale])
176
- puts "✅ Marked current translations as intentional."
192
+
193
+ results = manager(config, options).accept_edits!(
194
+ locale: options[:locale], keys: options[:keys] || [], all: options.fetch(:all, false)
195
+ )
196
+ report_accepted(results)
177
197
  puts "(dry run - no changes made)" if options[:dry_run]
178
198
  end
179
199
 
200
+ def report_accepted(results)
201
+ total = results.values.sum(&:size)
202
+ if total.zero?
203
+ puts "Nothing to accept — no hand-edited translations found."
204
+ return
205
+ end
206
+
207
+ results.each { |locale, keys| puts " #{locale}: #{keys.size} key(s)" if keys.any? }
208
+ puts "✅ Marked #{total} translation(s) as intentional."
209
+ end
210
+
180
211
  def cmd_hash(config, options)
181
212
  hash = manager(config, options).source_hash
182
213
  options[:format] == :json ? puts(JSON.generate({ hash: })) : puts(hash)
@@ -97,27 +97,35 @@ module Locallingo
97
97
  locales_to_process.each { |target_locale| translate_locale(source, target_locale, force:, force_keys:) }
98
98
  end
99
99
 
100
- # Mark every current target value as intentional (source_hash + target_hash +
101
- # manual flag) so the manual-edits validator stops flagging it.
102
- def accept_edits!(locale: nil)
100
+ # Mark hand-edited target values as intentional (source_hash + target_hash +
101
+ # manual flag) so the manual-edits validator stops flagging them and
102
+ # translate won't overwrite them. Unscoped, it accepts exactly the keys the
103
+ # manual-edits validator flags; `keys:` accepts the named keys regardless of
104
+ # drift; `all: true` marks every translated key (initial adoption). Returns
105
+ # `{ locale => accepted_keys }`.
106
+ def accept_edits!(locale: nil, keys: [], all: false)
103
107
  source = load_source_translations
104
108
  locales = locale ? [locale] : config.target_locales
105
109
 
106
- locales.each do |target_locale|
110
+ plans = locales.map do |target_locale|
107
111
  target = load_locale_translations(target_locale)
108
112
  locale_state = @state.load(target_locale)
113
+ accepted = keys_to_accept(source, target, locale_state, keys:, all:)
114
+ [target_locale, target, locale_state, accepted]
115
+ end
109
116
 
110
- target.each do |key, value|
111
- next unless source[key]
117
+ ensure_keys_matched!(keys, plans)
112
118
 
119
+ plans.each_with_object({}) do |(target_locale, target, locale_state, accepted), results|
120
+ accepted.each do |key|
113
121
  locale_state[key] = {
114
122
  "source_hash" => @state.hash(source[key]),
115
- "target_hash" => @state.hash(value),
123
+ "target_hash" => @state.hash(target[key]),
116
124
  "manual" => true
117
125
  }
118
126
  end
119
-
120
127
  @state.save(target_locale, locale_state) unless dry_run
128
+ results[target_locale] = accepted
121
129
  end
122
130
  end
123
131
 
@@ -126,8 +134,11 @@ module Locallingo
126
134
  format("%08x", Zlib.crc32(load_source_translations.to_json))
127
135
  end
128
136
 
129
- # Rewrite state from the current translation files (initial setup / after
130
- # manual edits). Returns the combined state.
137
+ # Refresh source hashes from the current translation files and prune state
138
+ # for keys that no longer exist. Existing `target_hash`/`manual` fields are
139
+ # preserved — hand-edit protection is never dropped by a sync; use
140
+ # `accept_edits!` to resolve hand-edit drift explicitly. Returns the
141
+ # combined state.
131
142
  def sync_state!
132
143
  source = load_source_translations
133
144
 
@@ -136,18 +147,7 @@ module Locallingo
136
147
  en_state.each_key { |key| en_state.delete(key) unless source.key?(key) }
137
148
  @state.save(config.source_locale, en_state) unless dry_run
138
149
 
139
- config.target_locales.each do |locale|
140
- target = load_locale_translations(locale)
141
- locale_state = @state.load(locale)
142
-
143
- target.each_key do |key|
144
- next unless source[key]
145
-
146
- locale_state[key] = { "source_hash" => @state.hash(source[key]) }
147
- end
148
- locale_state.each_key { |key| locale_state.delete(key) unless target.key?(key) }
149
- @state.save(locale, locale_state) unless dry_run
150
- end
150
+ config.target_locales.each { |locale| sync_locale_state(source, locale) }
151
151
 
152
152
  combined = { config.source_locale => @state.load(config.source_locale) }
153
153
  config.target_locales.each { |locale| combined[locale] = @state.load(locale) }
@@ -167,6 +167,44 @@ module Locallingo
167
167
  def missing_validator = @missing_validator ||= Validators::Missing.new(cli_name:)
168
168
  def outdated_validator = @outdated_validator ||= Validators::Outdated.new(cli_name:)
169
169
 
170
+ def sync_locale_state(source, locale)
171
+ target = load_locale_translations(locale)
172
+ locale_state = @state.load(locale)
173
+
174
+ target.each_key do |key|
175
+ next unless source[key]
176
+
177
+ existing = locale_state[key]
178
+ entry = existing.is_a?(Hash) ? existing.dup : {}
179
+ entry["source_hash"] = @state.hash(source[key])
180
+ locale_state[key] = entry
181
+ end
182
+ locale_state.each_key { |key| locale_state.delete(key) unless target.key?(key) }
183
+ @state.save(locale, locale_state) unless dry_run
184
+ end
185
+
186
+ def keys_to_accept(source, target, locale_state, keys:, all:)
187
+ return keys.select { |key| source.key?(key) && target.key?(key) } if keys.any?
188
+ return target.keys.select { |key| source.key?(key) } if all
189
+
190
+ target.keys.select do |key|
191
+ entry = locale_state[key]
192
+ next false unless source.key?(key) && entry.is_a?(Hash) && !entry["manual"]
193
+
194
+ entry["target_hash"] && entry["target_hash"] != @state.hash(target[key])
195
+ end
196
+ end
197
+
198
+ def ensure_keys_matched!(keys, plans)
199
+ return if keys.empty?
200
+
201
+ matched = plans.flat_map { |_, _, _, accepted| accepted }
202
+ missing = keys - matched
203
+ return if missing.empty?
204
+
205
+ raise Error, "accept-edits: key(s) not found in any target locale: #{missing.join(", ")}"
206
+ end
207
+
170
208
  def translate_locale(source, target_locale, force:, force_keys:)
171
209
  log("Processing #{target_locale}...")
172
210
 
@@ -301,10 +339,15 @@ module Locallingo
301
339
  translations.each_key do |key|
302
340
  next unless source[key]
303
341
 
304
- locale_state[key] = {
342
+ existing = locale_state[key]
343
+ entry = {
305
344
  "source_hash" => @state.hash(source[key]),
306
345
  "target_hash" => @state.hash(translations[key])
307
346
  }
347
+ # A force-keyed retranslation may overwrite a manual value on explicit
348
+ # request, but the protection flag itself must survive.
349
+ entry["manual"] = true if existing.is_a?(Hash) && existing["manual"]
350
+ locale_state[key] = entry
308
351
  end
309
352
  end
310
353
 
@@ -31,14 +31,14 @@ module Locallingo
31
31
  @provider = provider.to_sym
32
32
  end
33
33
 
34
- # True when credentials for the configured provider are present in ENV.
35
- # Unknown providers are assumed configured (RubyLLM may source the key
36
- # elsewhere) rather than blocking.
34
+ # True when a key for the configured provider is found in any source
35
+ # (Locallingo settings, host RubyLLM config, ENV). Unknown providers are
36
+ # assumed configured (RubyLLM may source the key elsewhere) rather than
37
+ # blocking.
37
38
  def credentials?
38
- env = CREDENTIAL_ENV[provider]
39
- return true unless env
39
+ return true unless CREDENTIAL_ENV.key?(provider)
40
40
 
41
- !ENV.fetch(env, "").to_s.strip.empty?
41
+ !resolved_api_key.nil?
42
42
  end
43
43
 
44
44
  # Raise a precise error when the provider has no credentials.
@@ -46,8 +46,10 @@ module Locallingo
46
46
  return if credentials?
47
47
 
48
48
  raise MissingCredentialsError,
49
- "No credentials for provider #{provider.inspect} " \
50
- "(expected #{CREDENTIAL_ENV[provider]} in ENV)"
49
+ "No credentials for provider #{provider.inspect}. " \
50
+ "Set #{CREDENTIAL_ENV[provider]} in ENV, call " \
51
+ "Locallingo.configure { |c| c.#{provider}_api_key = ... }, or add a " \
52
+ ".locallingo.rb setup file at the project root (loaded by the CLI)."
51
53
  end
52
54
 
53
55
  # Send +instructions+ (system prompt) + +payload+ (user message, JSON) to
@@ -70,20 +72,55 @@ module Locallingo
70
72
 
71
73
  # RubyLLM does not read provider API keys from ENV on its own, so a
72
74
  # standalone CLI run (no Rails initializer to call RubyLLM.configure)
73
- # would raise "Missing configuration for <provider>". Fill the
74
- # provider's key from ENV unless the host app already configured
75
- # one, which always wins.
75
+ # would raise "Missing configuration for <provider>". Push the resolved
76
+ # key into RubyLLM's config: an explicit `Locallingo.configure` key wins
77
+ # (re-resolved every chat so callables stay live), then a key the host
78
+ # app already set, then the ENV fallback.
76
79
  def configure_credentials!
77
- env = CREDENTIAL_ENV[provider]
78
- return unless env
80
+ return unless CREDENTIAL_ENV.key?(provider)
79
81
 
80
- setting = "#{provider}_api_key"
81
82
  config = ::RubyLLM.config
83
+ setting = key_setting
82
84
  return unless config.respond_to?(setting) && config.respond_to?("#{setting}=")
83
- return unless config.public_send(setting).to_s.strip.empty?
84
85
 
85
- key = ENV.fetch(env, "")
86
- config.public_send("#{setting}=", key) unless key.strip.empty?
86
+ key = settings_api_key
87
+ return if key.nil? && !presence(config.public_send(setting)).nil?
88
+
89
+ key ||= env_api_key
90
+ config.public_send("#{setting}=", key) unless key.nil?
91
+ end
92
+
93
+ # Key resolution across sources, in precedence order. Used by
94
+ # #credentials? as a fail-fast check before any network call.
95
+ def resolved_api_key
96
+ settings_api_key || host_configured_api_key || env_api_key
97
+ end
98
+
99
+ def settings_api_key
100
+ Locallingo.settings.api_key_for(provider)
101
+ end
102
+
103
+ # A key the host app set via RubyLLM.configure — only inspected when
104
+ # ruby_llm is already loaded (we never require it just to peek).
105
+ def host_configured_api_key
106
+ return nil unless defined?(::RubyLLM)
107
+
108
+ config = ::RubyLLM.config
109
+ return nil unless config.respond_to?(key_setting)
110
+
111
+ presence(config.public_send(key_setting))
112
+ end
113
+
114
+ def env_api_key
115
+ env = CREDENTIAL_ENV[provider]
116
+ env ? presence(ENV.fetch(env, "")) : nil
117
+ end
118
+
119
+ def key_setting = "#{provider}_api_key"
120
+
121
+ def presence(value)
122
+ key = value.to_s.strip
123
+ key.empty? ? nil : key
87
124
  end
88
125
  end
89
126
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Locallingo
4
+ # Code-level gem settings, configured via `Locallingo.configure`. Distinct
5
+ # from Locallingo::Configuration, which loads the `.locallingo.yml` project
6
+ # file — Settings holds what must never live in YAML: provider credentials.
7
+ #
8
+ # Locallingo.configure do |config|
9
+ # config.anthropic_api_key = ENV.fetch("MY_KEY") # a String…
10
+ # config.openai_api_key = -> { Vault.read("openai_key") } # …or a callable
11
+ # end
12
+ #
13
+ # Callables are resolved lazily on every use (never memoized), so keys can
14
+ # come from sources that aren't ready at configure time or that rotate.
15
+ class Settings
16
+ PROVIDERS = %i[openai anthropic gemini deepseek openrouter].freeze
17
+
18
+ attr_accessor(*PROVIDERS.map { |name| :"#{name}_api_key" })
19
+
20
+ # The usable key for +provider+: callables are called, results stripped,
21
+ # and blank or unknown-provider values come back as nil.
22
+ def api_key_for(provider)
23
+ return nil unless PROVIDERS.include?(provider.to_sym)
24
+
25
+ value = public_send("#{provider}_api_key")
26
+ value = value.call if value.respond_to?(:call)
27
+ key = value.to_s.strip
28
+ key.empty? ? nil : key
29
+ end
30
+ end
31
+ end
@@ -40,8 +40,9 @@ module Locallingo
40
40
  "This would cause state loss. Fix the JSON manually or restore from git."
41
41
  end
42
42
 
43
- # Save a locale's state, split back into per-namespace files. Namespace files
44
- # that no longer have keys are removed.
43
+ # Save a locale's state, split back into per-namespace files. Files whose
44
+ # content is unchanged are left untouched so unrelated namespaces never
45
+ # churn in diffs. Namespace files that no longer have keys are removed.
45
46
  def save(locale, locale_state)
46
47
  by_namespace = locale_state.each_with_object({}) do |(key, value), groups|
47
48
  namespace = key.split(".").first
@@ -50,7 +51,10 @@ module Locallingo
50
51
 
51
52
  by_namespace.each do |namespace, keys|
52
53
  state_file = File.join(state_dir, "#{namespace}.#{locale}.json")
53
- File.write(state_file, JSON.pretty_generate(keys.sort.to_h))
54
+ content = JSON.pretty_generate(keys.sort.to_h)
55
+ next if File.exist?(state_file) && File.read(state_file) == content
56
+
57
+ File.write(state_file, content)
54
58
  end
55
59
 
56
60
  Dir.glob(File.join(state_dir, "*.#{locale}.json")).each do |file|
@@ -30,7 +30,7 @@ module Locallingo
30
30
  type: :manual_edit,
31
31
  locale:,
32
32
  key:,
33
- suggestion: "Value was hand-edited. Protect it: #{@cli_name} accept-edits --locale #{locale}"
33
+ suggestion: "Value was hand-edited. Protect it: #{@cli_name} accept-edits --locale #{locale} --key #{key}"
34
34
  }
35
35
  end
36
36
  end
@@ -19,7 +19,7 @@ module Locallingo
19
19
  type: :outdated,
20
20
  locale:,
21
21
  key:,
22
- suggestion: "Source changed. Run: #{@cli_name} translate --locale #{locale} --force-key #{key}"
22
+ suggestion: suggestion_for(key, locale, locale_state)
23
23
  }
24
24
  end
25
25
  end
@@ -31,6 +31,19 @@ module Locallingo
31
31
  key if stored && stored != StateStore.hash(value)
32
32
  end
33
33
  end
34
+
35
+ private
36
+
37
+ # Manual keys must not be pushed toward machine retranslation — the value
38
+ # is hand-curated, so the human updates it and re-accepts.
39
+ def suggestion_for(key, locale, locale_state)
40
+ if locale_state.dig(key, "manual")
41
+ "Source changed for a manually-curated key. Update the #{locale} value by hand, " \
42
+ "then run: #{@cli_name} accept-edits --locale #{locale} --key #{key}"
43
+ else
44
+ "Source changed. Run: #{@cli_name} translate --locale #{locale} --force-key #{key}"
45
+ end
46
+ end
34
47
  end
35
48
  end
36
49
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Locallingo
4
- VERSION = "0.2.2"
4
+ VERSION = "0.4.0"
5
5
  end
data/lib/locallingo.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "locallingo/version"
4
+ require_relative "locallingo/settings"
4
5
  require_relative "locallingo/configuration"
5
6
  require_relative "locallingo/json_extraction"
6
7
  require_relative "locallingo/key_flattener"
@@ -35,4 +36,21 @@ module Locallingo
35
36
  def self.configuration(root_path: Dir.pwd, package: nil)
36
37
  Configuration.load(root_path:, package:)
37
38
  end
39
+
40
+ # Code-level settings (provider credentials) — see Locallingo::Settings.
41
+ def self.settings
42
+ @settings ||= Settings.new
43
+ end
44
+
45
+ # Configure the gem from Ruby — the credentials path for apps whose keys
46
+ # don't live in ENV (call it from a Rails initializer or a `.locallingo.rb`
47
+ # setup file next to `.locallingo.yml`; the CLI loads the latter on start).
48
+ def self.configure
49
+ yield settings
50
+ settings
51
+ end
52
+
53
+ def self.reset_settings!
54
+ @settings = nil
55
+ end
38
56
  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.2.2
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mikael Henriksson
@@ -81,6 +81,7 @@ files:
81
81
  - lib/locallingo/quality_checker.rb
82
82
  - lib/locallingo/reporter.rb
83
83
  - lib/locallingo/rubocop.rb
84
+ - lib/locallingo/settings.rb
84
85
  - lib/locallingo/state_store.rb
85
86
  - lib/locallingo/validators/duplicate_values.rb
86
87
  - lib/locallingo/validators/manual_edits.rb