Class: Keela::Strategies::I18nKeys

Inherits:
Keela::Strategy show all
Defined in:
lib/keela/strategies/i18n_keys.rb

Overview

Detects unused I18n translation keys in locale files.

Definitions are extracted from YAML locale files (config/locales/*.yml) and flattened to dot notation (e.g., "users.show.title").

Usage is detected by searching for:

- I18n.t("key") or I18n.t('key')
- t("key") or t('key')
- t(:key)
- .human_attribute_name(:attr)

Pluralization keys (zero, one, two, few, many, other) are grouped: if t("items.count", count: n) is called, all siblings are considered used.

Lazy lookup is supported: t('.title') in app/views/users/show.html.erb resolves to 'users.show.title'.

Constant Summary collapse

PLURAL_SUFFIXES =
%w[zero one two few many other].freeze
VIEW_PATH_REGEX =
%r{(?:ee/)?app/views/(.+)\.html\.(?:erb|haml|slim)$}.freeze
LAZY_LOOKUP_REGEX =
/(?:I18n\.)?t\s*\(\s*['"](\.[^'"]+)['"]/

Instance Method Summary collapse

Methods inherited from Keela::Strategy

#definition_file_pattern

Instance Method Details

#additional_used_names(source_files) ⇒ Object

Build a set of expanded lazy lookup keys from view files Called by the scanner to detect usage via lazy lookup



120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/keela/strategies/i18n_keys.rb', line 120

def additional_used_names(source_files)
  expanded = Set.new

  source_files.each do |filepath, lines|
    prefix = view_path_to_prefix(filepath)
    next unless prefix

    content = lines.join("\n")
    lazy_keys = extract_lazy_keys(content)

    lazy_keys.each do |lazy_key|
      # .title -> users.show.title
      full_key = "#{prefix}#{lazy_key}"
      expanded << full_key
    end
  end

  expanded
end

#default_definition_file_patternObject



32
33
34
35
# File 'lib/keela/strategies/i18n_keys.rb', line 32

def default_definition_file_pattern
  # Match locale YAML files
  %r{config/locales/.*\.ya?ml$}
end

#extract_definition(_line) ⇒ Object



61
62
63
64
# File 'lib/keela/strategies/i18n_keys.rb', line 61

def extract_definition(_line)
  # Not used - we override extract_definitions_from_file instead
  nil
end

#extract_definitions_from_file(filepath, _lines) ⇒ Object

Override: I18n keys need special YAML parsing, not line-by-line



38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# File 'lib/keela/strategies/i18n_keys.rb', line 38

def extract_definitions_from_file(filepath, _lines)
  return [] unless File.exist?(filepath)

  content = YAML.load_file(filepath, permitted_classes: [Symbol]) || {}
  keys = flatten_keys(content).map do |key|
    # Remove the locale prefix (e.g., "en.users.show" -> "users.show")
    key.sub(/^[a-z]{2}(-[A-Z]{2})?\./, "")
  end

  # For pluralization keys, also add the parent key so that
  # t("items.count", count: n) marks all siblings as used
  parent_keys = keys.filter_map do |key|
    parent_key_for_pluralization(key)
  end.uniq

  (keys + parent_keys).uniq.map do |key|
    { name: key, file: filepath }
  end
rescue Psych::SyntaxError => e
  warn "Warning: Could not parse #{filepath}: #{e.message}"
  []
end

#extract_lazy_keys(content) ⇒ Object

Extract lazy lookup keys from view content Returns array of keys like [".title", ".description"]



114
115
116
# File 'lib/keela/strategies/i18n_keys.rb', line 114

def extract_lazy_keys(content)
  content.scan(LAZY_LOOKUP_REGEX).flatten
end

#nameObject



28
29
30
# File 'lib/keela/strategies/i18n_keys.rb', line 28

def name
  "i18n_keys"
end

#skip_comments?Boolean

Returns:

  • (Boolean)


90
91
92
# File 'lib/keela/strategies/i18n_keys.rb', line 90

def skip_comments?
  true
end

#usage_regex(name) ⇒ Object



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/keela/strategies/i18n_keys.rb', line 66

def usage_regex(name)
  # Match various I18n lookup patterns:
  #   I18n.t("users.show.title")
  #   I18n.t('users.show.title')
  #   t("users.show.title")
  #   t('users.show.title')
  #   t(:users_show_title) - symbol form (underscored)
  #
  # For pluralization keys, also match the parent key:
  #   t("items.count", count: n) should match items.count.one, items.count.other, etc.
  quoted_name = Regexp.quote(name)

  # Check if this is a pluralization key and build alternate pattern
  parent_key = parent_key_for_pluralization(name)
  if parent_key
    quoted_parent = Regexp.quote(parent_key)
    # Match either the exact key OR the parent key
    /(?:I18n\.)?t\s*\(\s*["':]+(?:#{quoted_name}|#{quoted_parent})["']?\s*[,)]/
  else
    # Build pattern that matches the key in quotes or as a symbol
    /(?:I18n\.)?t\s*\(\s*["':]+#{quoted_name}["']?\s*[,)]/
  end
end

#view_path_to_prefix(path) ⇒ Object

Convert a view file path to its I18n prefix "app/views/users/show.html.erb" -> "users.show" "app/views/users/_form.html.erb" -> "users.form" "ee/app/views/users/show.html.erb" -> "users.show"



98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/keela/strategies/i18n_keys.rb', line 98

def view_path_to_prefix(path)
  return nil unless path =~ VIEW_PATH_REGEX

  view_path = Regexp.last_match(1)

  # Split into parts and process
  parts = view_path.split("/")

  # Handle partials: _form -> form
  parts[-1] = parts[-1].sub(/^_/, "")

  parts.join(".")
end