Class: BetterTranslate::VariableExtractor

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

Overview

Extracts and preserves interpolation variables during translation

Supports multiple variable formats:

  • Rails I18n: %name, %count
  • I18n.js: {user}, {email}
  • ES6 templates: $var
  • Simple braces: name

Variables are extracted before translation, replaced with safe placeholders, and then restored after translation to ensure they remain unchanged.

Examples:

Basic usage

extractor = VariableExtractor.new("Hello %{name}, you have {{count}} messages")
safe_text = extractor.extract
#=> "Hello VARIABLE_0, you have VARIABLE_1 messages"

translated = translate(safe_text)  # "Ciao VARIABLE_0, hai VARIABLE_1 messaggi"
final = extractor.restore(translated)
#=> "Ciao %{name}, hai {{count}} messaggi"

Variable validation

extractor = VariableExtractor.new("Total: %{amount}")
extractor.extract
extractor.validate_variables!("Totale: %{amount}")  #=> true
extractor.validate_variables!("Totale:")  # raises ValidationError

Constant Summary collapse

VARIABLE_PATTERNS =

Variable patterns to detect and preserve

{
  rails_template: /%\{[^}]+\}/, # %{name}, %{count}
  rails_annotated: /%<[^>]+>[a-z]/i,    # %<name>s, %<count>d
  i18n_js: /\{\{[^}]+\}\}/,             # {{user}}, {{email}}
  es6: /\$\{[^}]+\}/,                   # ${var}
  simple: /\{[a-zA-Z_][a-zA-Z0-9_]*\}/  # {name} but not {1,2,3}
}.freeze
COMBINED_PATTERN =

Combined pattern to match any variable format

Regexp.union(*VARIABLE_PATTERNS.values).freeze
PLACEHOLDER_PREFIX =

Placeholder prefix

"VARIABLE_"
PLACEHOLDER_SUFFIX =

Placeholder suffix

""

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(text) ⇒ VariableExtractor

Initialize extractor with text

Examples:

extractor = VariableExtractor.new("Hello %{name}")


65
66
67
68
69
70
# File 'lib/better_translate/variable_extractor.rb', line 65

def initialize(text)
  @original_text = text
  @variables = []
  @placeholder_map = {}
  @reverse_map = {}
end

Instance Attribute Details

#original_textString (readonly)



50
51
52
# File 'lib/better_translate/variable_extractor.rb', line 50

def original_text
  @original_text
end

#placeholder_mapHash<String, String> (readonly)



56
57
58
# File 'lib/better_translate/variable_extractor.rb', line 56

def placeholder_map
  @placeholder_map
end

#variablesArray<String> (readonly)



53
54
55
# File 'lib/better_translate/variable_extractor.rb', line 53

def variables
  @variables
end

Class Method Details

.contains_variables?(text) ⇒ Boolean

Check if text contains variables

Static method to quickly check if text contains any supported variable format.

Examples:

VariableExtractor.contains_variables?("Hello %{name}")  #=> true
VariableExtractor.contains_variables?("Hello world")    #=> false


253
254
255
256
257
# File 'lib/better_translate/variable_extractor.rb', line 253

def self.contains_variables?(text)
  return false if text.nil? || text.empty?

  text.match?(COMBINED_PATTERN)
end

.find_variables(text) ⇒ Array<String>

Extract variables from text without creating instance

Static method to find all variables in text without needing to instantiate the extractor.

Examples:

VariableExtractor.find_variables("Hi %{name}, {{count}} items")
#=> ["%{name}", "{{count}}"]


235
236
237
238
239
# File 'lib/better_translate/variable_extractor.rb', line 235

def self.find_variables(text)
  return [] if text.nil? || text.empty?

  text.scan(COMBINED_PATTERN)
end

Instance Method Details

#extractString

Extract variables and replace with placeholders

Scans the text for all supported variable formats and replaces them with numbered placeholders (VARIABLE_0, VARIABLE_1, etc.).

Examples:

extractor = VariableExtractor.new("Hello %{name}")
extractor.extract  #=> "Hello VARIABLE_0"


83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'lib/better_translate/variable_extractor.rb', line 83

def extract
  return "" if original_text.nil? || original_text.empty?

  result = original_text.dup
  index = 0

  # Find and replace all variables
  result.gsub!(COMBINED_PATTERN) do |match|
    placeholder = "#{PLACEHOLDER_PREFIX}#{index}#{PLACEHOLDER_SUFFIX}"
    @variables << match
    @placeholder_map[placeholder] = match
    @reverse_map[match] = placeholder
    index += 1
    placeholder
  end

  result
end

#restore(translated_text, strict: true) ⇒ String

Restore variables from placeholders in translated text

Replaces all placeholders with their original variable formats. In strict mode, validates that all original variables are present.

Examples:

Successful restore

extractor = VariableExtractor.new("Hello %{name}")
extractor.extract
extractor.restore("Ciao VARIABLE_0")  #=> "Ciao %{name}"

Strict mode with missing variable

extractor = VariableExtractor.new("Hello %{name}")
extractor.extract
extractor.restore("Ciao", strict: true)  # raises ValidationError

Raises:



122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
# File 'lib/better_translate/variable_extractor.rb', line 122

def restore(translated_text, strict: true)
  return "" if translated_text.nil? || translated_text.empty?

  result = translated_text.dup

  # Restore all placeholders
  @placeholder_map.each do |placeholder, original_var|
    result.gsub!(placeholder, original_var)
  end

  # Validate all variables are present
  validate_variables!(result) if strict

  result
end

#validate_variables!(text) ⇒ true

Validate that all original variables are present in text

Checks that:

  1. All original variables are still present
  2. No unexpected/extra variables have been added

Examples:

Valid text

extractor = VariableExtractor.new("Hello %{name}")
extractor.extract
extractor.validate_variables!("Ciao %{name}")  #=> true

Missing variable

extractor = VariableExtractor.new("Hello %{name}")
extractor.extract
extractor.validate_variables!("Ciao")  # raises ValidationError

Raises:



184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
# File 'lib/better_translate/variable_extractor.rb', line 184

def validate_variables!(text)
  # @type var missing: Array[String]
  missing = []
  # @type var extra: Array[String]
  extra = []

  # Check for missing variables
  @variables.each do |var|
    var_str = var.is_a?(String) ? var : var.to_s
    missing << var_str unless text.include?(var_str)
  end

  # Check for extra/unknown variables (potential corruption)
  found_vars = text.scan(COMBINED_PATTERN)
  found_vars.each do |var|
    var_str = var.is_a?(String) ? var : var.to_s
    extra << var_str unless @variables.include?(var_str)
  end

  if missing.any? || extra.any?
    # @type var error_msg: Array[String]
    error_msg = []
    error_msg << "Missing variables: #{missing.join(", ")}" if missing.any?
    error_msg << "Unexpected variables: #{extra.join(", ")}" if extra.any?

    raise ValidationError.new(
      "Variable validation failed: #{error_msg.join("; ")}",
      context: {
        original_variables: @variables,
        missing: missing,
        extra: extra,
        text: text
      }
    )
  end

  true
end

#variable_countInteger

Get count of variables

Examples:

extractor = VariableExtractor.new("Hi %{name}, {{count}} items")
extractor.extract
extractor.variable_count  #=> 2


160
161
162
# File 'lib/better_translate/variable_extractor.rb', line 160

def variable_count
  @variables.size
end

#variables?Boolean

Check if text contains variables

Examples:

extractor = VariableExtractor.new("Hello %{name}")
extractor.extract
extractor.variables?  #=> true


147
148
149
# File 'lib/better_translate/variable_extractor.rb', line 147

def variables?
  !@variables.empty?
end