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}")

Parameters:

  • text (String)

    Text containing variables



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)

Returns Original text with variables.

Returns:

  • (String)

    Original text with variables



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

def original_text
  @original_text
end

#placeholder_mapHash<String, String> (readonly)

Returns Mapping of placeholders to original variables.

Returns:

  • (Hash<String, String>)

    Mapping of placeholders to original variables



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

def placeholder_map
  @placeholder_map
end

#variablesArray<String> (readonly)

Returns Extracted variables in order.

Returns:

  • (Array<String>)

    Extracted variables in order



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

Parameters:

  • text (String)

    Text to check

Returns:

  • (Boolean)

    true if variables are present



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}}"]

Parameters:

  • text (String)

    Text to analyze

Returns:

  • (Array<String>)

    List of variables found



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"

Returns:

  • (String)

    Text with variables replaced by placeholders



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

Parameters:

  • translated_text (String)

    Translated text with placeholders

  • strict (Boolean) (defaults to: true)

    If true, raises error if variables are missing

Returns:

  • (String)

    Translated text with original variables restored

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

Parameters:

  • text (String)

    Text to validate

Returns:

  • (true)

    if all variables are present

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

Returns:

  • (Integer)

    Number of variables



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

Returns:

  • (Boolean)

    true if variables are present



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

def variables?
  !@variables.empty?
end