Class: Locallingo::Manager
- Inherits:
-
Object
- Object
- Locallingo::Manager
- Defined in:
- lib/locallingo/manager.rb
Overview
Manages translations with source-hash change detection.
- Tracks source hashes to detect changes (drift)
- Only translates missing/changed keys
- Validates translation completeness (config-selected validators)
- Merges LLM output back into the flat
<namespace>.<locale>.ymlfiles
Everything app-specific (locales, provider/model, prompt context/glossary, per-language guides, which validators run) comes from the Configuration.
Constant Summary collapse
- MAX_RETRIES =
3- MAX_MISSING_RETRIES =
2- BASE_SLEEP_DURATION =
1.0
Instance Attribute Summary collapse
-
#cli_name ⇒ Object
readonly
Returns the value of attribute cli_name.
-
#config ⇒ Object
readonly
Returns the value of attribute config.
-
#dry_run ⇒ Object
readonly
Returns the value of attribute dry_run.
-
#logger ⇒ Object
readonly
Returns the value of attribute logger.
-
#verbose ⇒ Object
readonly
Returns the value of attribute verbose.
Instance Method Summary collapse
-
#accept_edits!(locale: nil, keys: [], all: false) ⇒ Object
Mark hand-edited target values as intentional (source_hash + target_hash + manual flag) so the manual-edits validator stops flagging them and translate won't overwrite them.
-
#initialize(config: nil, root_path: nil, package: nil, dry_run: false, verbose: false, logger: nil, cli_name: "lingo") ⇒ Manager
constructor
A new instance of Manager.
-
#run_after_translate_hooks ⇒ Object
Run the configured after_translate hook commands (from the app root).
-
#source_hash ⇒ Object
CRC32 hash of the current source translations (change-detection fingerprint).
-
#status ⇒ Object
Current translation status per target locale.
-
#sync_state! ⇒ Object
Refresh source hashes from the current translation files, backfill a
target_hashbaseline for entries that lack one, and prune state for keys that no longer exist. -
#translate!(locale: nil, force: false, force_keys: []) ⇒ Object
Translate missing/changed keys for one or all target locales.
-
#validate ⇒ Object
Validate translations; returns an array of violation hashes.
Constructor Details
#initialize(config: nil, root_path: nil, package: nil, dry_run: false, verbose: false, logger: nil, cli_name: "lingo") ⇒ Manager
Returns a new instance of Manager.
34 35 36 37 38 39 40 41 42 43 |
# File 'lib/locallingo/manager.rb', line 34 def initialize(config: nil, root_path: nil, package: nil, dry_run: false, verbose: false, logger: nil, cli_name: "lingo") @config = config || Configuration.load(root_path: root_path || Dir.pwd, package:) @dry_run = dry_run @verbose = verbose @cli_name = cli_name @state = StateStore.new(@config.state_dir) @provider = Providers::RubyLLM.new(provider: @config.provider) @logger = logger || build_logger end |
Instance Attribute Details
#cli_name ⇒ Object (readonly)
Returns the value of attribute cli_name.
32 33 34 |
# File 'lib/locallingo/manager.rb', line 32 def cli_name @cli_name end |
#config ⇒ Object (readonly)
Returns the value of attribute config.
32 33 34 |
# File 'lib/locallingo/manager.rb', line 32 def config @config end |
#dry_run ⇒ Object (readonly)
Returns the value of attribute dry_run.
32 33 34 |
# File 'lib/locallingo/manager.rb', line 32 def dry_run @dry_run end |
#logger ⇒ Object (readonly)
Returns the value of attribute logger.
32 33 34 |
# File 'lib/locallingo/manager.rb', line 32 def logger @logger end |
#verbose ⇒ Object (readonly)
Returns the value of attribute verbose.
32 33 34 |
# File 'lib/locallingo/manager.rb', line 32 def verbose @verbose end |
Instance Method Details
#accept_edits!(locale: nil, keys: [], all: false) ⇒ Object
Mark hand-edited target values as intentional (source_hash + target_hash +
manual flag) so the manual-edits validator stops flagging them and
translate won't overwrite them. Unscoped, it accepts exactly the keys the
manual-edits validator flags; keys: accepts the named keys regardless of
drift; all: true marks every translated key (initial adoption). Returns
{ locale => accepted_keys }.
106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 |
# File 'lib/locallingo/manager.rb', line 106 def accept_edits!(locale: nil, keys: [], all: false) source = load_source_translations locales = locale ? [locale] : config.target_locales plans = locales.map do |target_locale| target = load_locale_translations(target_locale) locale_state = @state.load(target_locale) accepted = keys_to_accept(source, target, locale_state, keys:, all:) [target_locale, target, locale_state, accepted] end ensure_keys_matched!(keys, plans) plans.each_with_object({}) do |(target_locale, target, locale_state, accepted), results| accepted.each do |key| locale_state[key] = { "source_hash" => @state.hash(source[key]), "target_hash" => @state.hash(target[key]), "manual" => true } end @state.save(target_locale, locale_state) unless dry_run results[target_locale] = accepted end end |
#run_after_translate_hooks ⇒ Object
Run the configured after_translate hook commands (from the app root).
159 160 161 162 163 164 |
# File 'lib/locallingo/manager.rb', line 159 def run_after_translate_hooks config.after_translate.each do |command| log("Running: #{command}") Dir.chdir(config.root_path) { system(command) } end end |
#source_hash ⇒ Object
CRC32 hash of the current source translations (change-detection fingerprint).
133 134 135 |
# File 'lib/locallingo/manager.rb', line 133 def source_hash format("%08x", Zlib.crc32(load_source_translations.to_json)) end |
#status ⇒ Object
Current translation status per target locale.
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
# File 'lib/locallingo/manager.rb', line 46 def status source = load_source_translations config.target_locales.each_with_object({}) do |locale, results| target = load_locale_translations(locale) locale_state = @state.load(locale) missing = source.keys - target.keys outdated = outdated_validator.outdated_keys(source, locale_state) results[locale] = { total_keys: source.keys.size, translated: target.keys.size, missing: missing.size, outdated: outdated.size, missing_keys: missing.first(10), outdated_keys: outdated.first(10) } end end |
#sync_state! ⇒ Object
Refresh source hashes from the current translation files, backfill a
target_hash baseline for entries that lack one, and prune state for
keys that no longer exist. An existing target_hash and the manual
flag are never touched — hand-edit protection is never dropped by a
sync; use accept_edits! to resolve hand-edit drift explicitly.
Returns the combined state.
143 144 145 146 147 148 149 150 151 152 153 154 155 156 |
# File 'lib/locallingo/manager.rb', line 143 def sync_state! source = load_source_translations en_state = @state.load(config.source_locale) source.each { |key, value| en_state[key] = { "source_hash" => @state.hash(value) } } en_state.each_key { |key| en_state.delete(key) unless source.key?(key) } @state.save(config.source_locale, en_state) unless dry_run config.target_locales.each { |locale| sync_locale_state(source, locale) } combined = { config.source_locale => @state.load(config.source_locale) } config.target_locales.each { |locale| combined[locale] = @state.load(locale) } combined end |
#translate!(locale: nil, force: false, force_keys: []) ⇒ Object
Translate missing/changed keys for one or all target locales.
91 92 93 94 95 96 97 98 |
# File 'lib/locallingo/manager.rb', line 91 def translate!(locale: nil, force: false, force_keys: []) @provider.ensure_credentials! locales_to_process = locale ? [locale] : config.target_locales source = load_source_translations locales_to_process.each { |target_locale| translate_locale(source, target_locale, force:, force_keys:) } end |
#validate ⇒ Object
Validate translations; returns an array of violation hashes. Which checks run is config-driven.
68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 |
# File 'lib/locallingo/manager.rb', line 68 def validate violations = [] source = load_source_translations violations.concat(Validators::DuplicateValues.new.call(source:)) if config.validator_enabled?(:duplicate_values) config.target_locales.each do |locale| target = load_locale_translations(locale) locale_state = @state.load(locale) violations.concat(missing_validator.call(source:, target:, locale:)) if config.validator_enabled?(:missing) if config.validator_enabled?(:outdated) violations.concat(outdated_validator.call(source:, locale_state:, locale:)) end if config.validator_enabled?(:manual_edits) violations.concat(Validators::ManualEdits.new(cli_name:).call(target:, locale_state:, locale:)) end end violations end |