Class: BetterTranslate::YAMLHandler

Inherits:
Object
  • Object
show all
Defined in:
lib/better_translate/yaml_handler.rb

Overview

Handles YAML file operations

Provides methods for:

  • Reading and parsing YAML files
  • Writing YAML files with proper formatting
  • Merging translations (incremental mode)
  • Handling exclusions
  • Flattening/unflattening nested structures

Examples:

Reading a YAML file

handler = YAMLHandler.new(config)
data = handler.read_yaml("config/locales/en.yml")

Writing translations

handler.write_yaml("config/locales/it.yml", { "it" => { "greeting" => "Ciao" } })

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(config) ⇒ YAMLHandler

Initialize YAML handler

Examples:

config = Configuration.new
handler = YAMLHandler.new(config)

Parameters:



35
36
37
# File 'lib/better_translate/yaml_handler.rb', line 35

def initialize(config)
  @config = config
end

Instance Attribute Details

#configConfiguration (readonly)

Returns Configuration object.

Returns:



25
26
27
# File 'lib/better_translate/yaml_handler.rb', line 25

def config
  @config
end

Instance Method Details

#build_output_path(target_lang_code) ⇒ String

Build output file path for target language

Examples:

path = handler.build_output_path("it")
#=> "config/locales/it.yml"

Parameters:

  • target_lang_code (String)

    Target language code

Returns:

  • (String)

    Output file path



165
166
167
168
169
# File 'lib/better_translate/yaml_handler.rb', line 165

def build_output_path(target_lang_code)
  return "#{target_lang_code}.yml" unless config.output_folder

  File.join(config.output_folder, "#{target_lang_code}.yml")
end

#create_backup_file(file_path) ⇒ void (private)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.

Create backup file with rotation support

Parameters:

  • file_path (String)

    Path to file to backup



179
180
181
182
183
184
185
186
187
188
# File 'lib/better_translate/yaml_handler.rb', line 179

def create_backup_file(file_path)
  return unless File.exist?(file_path)

  # Rotate existing backups if max_backups > 1
  rotate_backups(file_path) if config.max_backups > 1

  # Create primary backup
  backup_path = "#{file_path}.bak"
  FileUtils.cp(file_path, backup_path)
end

#filter_exclusions(strings, target_lang_code) ⇒ Hash

Filter out excluded keys for a specific language

Examples:

filtered = handler.filter_exclusions(strings, "it")

Parameters:

  • strings (Hash)

    Flattened strings

  • target_lang_code (String)

    Target language code

Returns:

  • (Hash)

    Filtered strings



129
130
131
132
133
134
# File 'lib/better_translate/yaml_handler.rb', line 129

def filter_exclusions(strings, target_lang_code)
  excluded_keys = config.global_exclusions.dup
  excluded_keys += config.exclusions_per_language[target_lang_code] || []

  strings.reject { |key, _| excluded_keys.include?(key) }
end

#get_source_stringsHash

Get translatable strings from source YAML

Reads the input file and returns a flattened hash of strings. Removes the root language key if present.

Examples:

strings = handler.get_source_strings
#=> { "greeting" => "Hello", "nav.home" => "Home" }

Returns:

  • (Hash)

    Flattened hash of translatable strings



110
111
112
113
114
115
116
117
118
# File 'lib/better_translate/yaml_handler.rb', line 110

def get_source_strings
  return {} unless config.input_file

  source_data = read_yaml(config.input_file)
  # Remove root language key if present (e.g., "en:")
  source_data = source_data[config.source_language] || source_data

  Utils::HashFlattener.flatten(source_data)
end

#merge_translations(file_path, new_translations) ⇒ Hash

Merge translated strings with existing file (incremental mode)

Examples:

merged = handler.merge_translations("config/locales/it.yml", new_translations)

Parameters:

  • file_path (String)

    Existing file path

  • new_translations (Hash)

    New translations (flattened)

Returns:

  • (Hash)

    Merged translations (nested)



145
146
147
148
149
150
151
152
153
154
# File 'lib/better_translate/yaml_handler.rb', line 145

def merge_translations(file_path, new_translations)
  # @type var existing: Hash[String, untyped]
  existing = File.exist?(file_path) ? read_yaml(file_path) : {}
  existing_flat = Utils::HashFlattener.flatten(existing)

  # Merge: existing takes precedence
  merged = new_translations.merge(existing_flat)

  Utils::HashFlattener.unflatten(merged)
end

#read_yaml(file_path) ⇒ Hash

Read and parse YAML file

Examples:

data = handler.read_yaml("config/locales/en.yml")
#=> { "en" => { "greeting" => "Hello" } }

Parameters:

  • file_path (String)

    Path to YAML file

Returns:

  • (Hash)

    Parsed YAML content

Raises:



50
51
52
53
54
55
56
57
58
59
# File 'lib/better_translate/yaml_handler.rb', line 50

def read_yaml(file_path)
  Validator.validate_file_exists!(file_path)

  content = File.read(file_path)
  YAML.safe_load(content) || {}
rescue Errno::ENOENT => e
  raise FileError.new("File not found: #{file_path}", context: { error: e.message })
rescue Psych::SyntaxError => e
  raise YamlError.new("Invalid YAML syntax in #{file_path}", context: { error: e.message })
end

#rotate_backups(file_path) ⇒ void (private)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.

Rotate backup files, keeping only max_backups

Rotation strategy:

  • .bak is always the most recent
  • .bak.1, .bak.2, etc. are progressively older
  • When we reach max_backups, oldest is deleted

Parameters:

  • file_path (String)

    Base file path



201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
# File 'lib/better_translate/yaml_handler.rb', line 201

def rotate_backups(file_path)
  primary_backup = "#{file_path}.bak"
  return unless File.exist?(primary_backup)

  # Clean up ANY backups that would exceed max_backups after rotation
  # max_backups includes .bak itself, so numbered backups go from 1 to max_backups-1
  # After rotation, .bak -> .bak.1, so we can have at most .bak.1 through .bak.(max_backups-1)
  10.downto(config.max_backups) do |i|
    numbered_backup = "#{file_path}.bak.#{i}"
    FileUtils.rm_f(numbered_backup) if File.exist?(numbered_backup)
  end

  # Rotate numbered backups from high to low to avoid overwrites
  # max_backups=2: nothing to rotate (only .bak -> .bak.1)
  # max_backups=3: .bak.1 -> .bak.2 (if exists)
  (config.max_backups - 2).downto(1) do |i|
    old_path = "#{file_path}.bak.#{i}"
    new_path = "#{file_path}.bak.#{i + 1}"

    FileUtils.mv(old_path, new_path) if File.exist?(old_path)
  end

  # Move primary backup to .bak.1
  FileUtils.mv(primary_backup, "#{file_path}.bak.1")
end

#write_yaml(file_path, data, diff_preview: nil) ⇒ Hash?

Write hash to YAML file

Examples:

handler.write_yaml("config/locales/it.yml", { "it" => { "greeting" => "Ciao" } })

Parameters:

  • file_path (String)

    Output file path

  • data (Hash)

    Data to write

  • diff_preview (DiffPreview, nil) (defaults to: nil)

    Optional diff preview instance

Returns:

  • (Hash, nil)

    Summary hash if dry_run, nil otherwise

Raises:



72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/better_translate/yaml_handler.rb', line 72

def write_yaml(file_path, data, diff_preview: nil)
  summary = nil

  # Show diff preview if in dry run mode
  if config.dry_run && diff_preview
    # @type var existing_data: Hash[String, untyped]
    existing_data = File.exist?(file_path) ? read_yaml(file_path) : {}
    summary = diff_preview.show_diff(existing_data, data, file_path)
  end

  return summary if config.dry_run

  # Create backup if enabled and file exists
  create_backup_file(file_path) if config.create_backup && File.exist?(file_path)

  # Ensure output directory exists
  FileUtils.mkdir_p(File.dirname(file_path))

  File.write(file_path, YAML.dump(data))

  nil
rescue Errno::EACCES => e
  raise FileError.new("Permission denied: #{file_path}", context: { error: e.message })
rescue StandardError => e
  raise FileError.new("Failed to write YAML: #{file_path}", context: { error: e.message })
end