Class: Locallingo::Manager

Inherits:
Object
  • Object
show all
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>.yml files

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

Instance Method Summary collapse

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_nameObject (readonly)

Returns the value of attribute cli_name.



32
33
34
# File 'lib/locallingo/manager.rb', line 32

def cli_name
  @cli_name
end

#configObject (readonly)

Returns the value of attribute config.



32
33
34
# File 'lib/locallingo/manager.rb', line 32

def config
  @config
end

#dry_runObject (readonly)

Returns the value of attribute dry_run.



32
33
34
# File 'lib/locallingo/manager.rb', line 32

def dry_run
  @dry_run
end

#loggerObject (readonly)

Returns the value of attribute logger.



32
33
34
# File 'lib/locallingo/manager.rb', line 32

def logger
  @logger
end

#verboseObject (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_hooksObject

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_hashObject

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

#statusObject

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

#validateObject

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