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) ⇒ Object

Mark every current target value as intentional (source_hash + target_hash + manual flag) so the manual-edits validator stops flagging it.



102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'lib/locallingo/manager.rb', line 102

def accept_edits!(locale: nil)
  source = load_source_translations
  locales = locale ? [locale] : config.target_locales

  locales.each do |target_locale|
    target = load_locale_translations(target_locale)
    locale_state = @state.load(target_locale)

    target.each do |key, value|
      next unless source[key]

      locale_state[key] = {
        "source_hash" => @state.hash(source[key]),
        "target_hash" => @state.hash(value),
        "manual" => true
      }
    end

    @state.save(target_locale, locale_state) unless dry_run
  end
end

#run_after_translate_hooksObject

Run the configured after_translate hook commands (from the app root).



158
159
160
161
162
163
# File 'lib/locallingo/manager.rb', line 158

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).



125
126
127
# File 'lib/locallingo/manager.rb', line 125

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

Rewrite state from the current translation files (initial setup / after manual edits). Returns the combined state.



131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/locallingo/manager.rb', line 131

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 do |locale|
    target = load_locale_translations(locale)
    locale_state = @state.load(locale)

    target.each_key do |key|
      next unless source[key]

      locale_state[key] = { "source_hash" => @state.hash(source[key]) }
    end
    locale_state.each_key { |key| locale_state.delete(key) unless target.key?(key) }
    @state.save(locale, locale_state) unless dry_run
  end

  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