inertia_i18n 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6b68ed790e8d127e23f0b40c11bc74747a92a7075ba09474d4e670fa82ff558c
4
- data.tar.gz: 73eefa0cf481919637be088a6ad9a483e22ad101fb15803d3f28f10e5d96d302
3
+ metadata.gz: 34e9a2d18070430f9210ad477789b8d179c7feb234d9de11f5a6358ce4eda012
4
+ data.tar.gz: 95bb612b926a1a9eaf6b7cbe2d86fb53dfd06dcc62d9b8d7be8f7a70c5a66057
5
5
  SHA512:
6
- metadata.gz: 5b8ffe3ede9a8673882e941e98e822f2afbb6d5f10f48b7ffb83ad538ac4432a5bcd57ccb4ea37ce5c988d1983af1a4f478164a76861e82fea735afeaa37a879
7
- data.tar.gz: a47098ba92a0f0c812e0e1632e6a9113b5f5b5b78600f3d0beb04680d684542557fe6fc4d5d0230ef06408c1342d636f8d654665568810b96fabe3fa85c000d7
6
+ metadata.gz: 2ac2dc6afe2112afdb8cac7a14a11b55880964d0ea86aab494fbb31350096ce502caa64b28b7b9570faa3d833cdadb8b7c1be3a4f95cb382d065c91244103954
7
+ data.tar.gz: 62a53052bfb31864001da174ed667ad6fbdab6d36461b47726e373515a621c7c98d8d45d68ee7dd82931f0d7373acf920c1a967230f6383624c09e0b48888029
data/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.6.0] - 2026-02-09
4
+
5
+ ### Added
6
+
7
+ - **Context Awareness**: `missing` and `unused` commands now report the file path and line number where the key was found (e.g., `app/frontend/User.vue:42`).
8
+ - **Magic Comments**: Support for `// inertia-i18n-use key.name` and `/* inertia-i18n-use key.name */` to mark dynamic keys as used.
9
+ - **i18n-tasks Compatibility**:
10
+ - Support for `// i18n-tasks-use key.name` comments (interoperability).
11
+ - `InertiaI18n::I18nTasks::Scanner` adapter class to allow `i18n-tasks` to use `inertia_i18n`'s frontend scanning logic.
12
+ - **Robust CLI**: `inertia-i18n` command now correctly loads the library in non-Rails environments.
13
+
14
+ ### Fixed
15
+
16
+ - Scanner adapter now returns data in the format `i18n-tasks` expects (including source location).
17
+
3
18
  ## [0.5.0] - 2026-02-06
4
19
 
5
20
  ### Added
data/README.md CHANGED
@@ -231,6 +231,16 @@ Handles:
231
231
  - Template literals: `t(\`user.\${type}.title\`)` (flagged for review)
232
232
  - Dynamic patterns: `t(keyVariable)` (flagged for review)
233
233
 
234
+ ### Magic Comments
235
+
236
+ Use magic comments to mark dynamic keys as used or suppress warnings. Both `inertia-i18n-use` and `i18n-tasks-use` are supported.
237
+
238
+ ```javascript
239
+ // inertia-i18n-use status.active
240
+ // inertia-i18n-use status.inactive
241
+ const statusLabel = t(`status.${status}`)
242
+ ```
243
+
234
244
  ### Health Checks
235
245
 
236
246
  | Check | Description |
@@ -328,7 +338,26 @@ jobs:
328
338
 
329
339
  ## Compatibility with i18n-tasks
330
340
 
331
- If you use [i18n-tasks](https://github.com/glebm/i18n-tasks) for your backend translations, it might flag your frontend keys as "unused" or "missing". To prevent this, configure `i18n-tasks` to ignore the frontend locale directory.
341
+ InertiaI18n works seamlessly with [i18n-tasks](https://github.com/glebm/i18n-tasks).
342
+
343
+ ### 1. Integrating Scanners (Recommended)
344
+
345
+ You can configure `i18n-tasks` to use the `inertia_i18n` scanner. This allows `i18n-tasks` to correctly detect frontend key usage, so you can use `i18n-tasks unused` for your entire project (backend + frontend).
346
+
347
+ Add this to your `config/i18n-tasks.yml`:
348
+
349
+ ```yaml
350
+ <% require 'inertia_i18n/i18n_tasks/scanner' %>
351
+ search:
352
+ scanners:
353
+ - InertiaI18n::I18nTasks::Scanner
354
+ ```
355
+
356
+ Now `i18n-tasks` will "see" keys used in your Vue/React/Svelte files.
357
+
358
+ ### 2. Separating Concerns
359
+
360
+ If you prefer to keep them separate, configure `i18n-tasks` to ignore the frontend locale directory so it doesn't flag frontend keys as "unused" or "missing".
332
361
 
333
362
  Add this to your `config/i18n-tasks.yml`:
334
363
 
data/exe/inertia-i18n CHANGED
@@ -11,6 +11,7 @@ if File.exist?("config/environment.rb")
11
11
  require File.expand_path("config/environment.rb")
12
12
  end
13
13
 
14
+ require "inertia_i18n"
14
15
  require "inertia_i18n/cli"
15
16
 
16
17
  InertiaI18n::Cli.start(ARGV)
@@ -363,7 +363,20 @@ module InertiaI18n
363
363
  if verbose
364
364
  issues.each do |issue|
365
365
  puts " - #{issue[:key]}"
366
+ if issue[:occurrences]&.any?
367
+ issue[:occurrences].each do |loc|
368
+ puts " ↳ #{loc[:file]}:#{loc[:line]}"
369
+ end
370
+ end
366
371
  end
372
+ else
373
+ # In non-verbose mode, show first occurrence for context
374
+ issues.take(5).each do |issue|
375
+ loc = issue[:occurrences]&.first
376
+ loc_str = loc ? " (#{loc[:file]}:#{loc[:line]})" : ""
377
+ puts " - #{issue[:key]}#{loc_str}"
378
+ end
379
+ puts " ... and #{issues.count - 5} more (use --verbose to see all)" if issues.count > 5
367
380
  end
368
381
  puts
369
382
  end
@@ -52,10 +52,19 @@ module InertiaI18n
52
52
  missing = filter_ignored_keys(missing, @config.ignore_missing)
53
53
 
54
54
  missing.each do |key|
55
+ locations = @scan_results.occurrences[key]
56
+ message = "Key '#{key}' is used in code but missing from #{primary_locale}.json"
57
+
58
+ if locations&.any?
59
+ first_loc = locations.first
60
+ message += " (found in #{first_loc[:file]}:#{first_loc[:line]})"
61
+ end
62
+
55
63
  @issues[:missing] << {
56
64
  key: key,
57
65
  severity: :error,
58
- message: "Key '#{key}' is used in code but missing from #{primary_locale}.json"
66
+ message: message,
67
+ occurrences: locations
59
68
  }
60
69
  end
61
70
  end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "i18n/tasks/scanners/file_scanner"
4
+ require "i18n/tasks/scanners/results/occurrence"
5
+
6
+ module InertiaI18n
7
+ module I18nTasks
8
+ class Scanner < ::I18n::Tasks::Scanners::FileScanner
9
+ def scan_file(path)
10
+ # Scan file using InertiaI18n logic
11
+ scanner = InertiaI18n::Scanner.new
12
+ parser = scanner.parser_for_file(path)
13
+ return [] unless parser
14
+ return [] if parser.instance_of?(InertiaI18n::Parsers::BaseParser)
15
+
16
+ # Extract keys: {static: [{key: 'foo', line: 1}], dynamic: [...]}
17
+ results = parser.extract_keys(path)
18
+
19
+ # Convert to i18n-tasks format: [ [key, Occurrence] ]
20
+ results[:static].map do |usage|
21
+ key = usage[:key]
22
+
23
+ # Return the format i18n-tasks expects for key occurrences
24
+ [
25
+ key,
26
+ ::I18n::Tasks::Scanners::Results::Occurrence.new(
27
+ path: path,
28
+ line_num: usage[:line],
29
+ pos: 1,
30
+ line_pos: 1,
31
+ line: "",
32
+ raw_key: key
33
+ )
34
+ ]
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -9,11 +9,33 @@ module InertiaI18n
9
9
 
10
10
  def extract_javascript_keys(content)
11
11
  {
12
- static: extract_static_keys(content).uniq,
12
+ static: (extract_static_keys(content) + extract_magic_comments(content)).uniq { |k| k[:key] },
13
13
  dynamic: extract_dynamic_patterns(content).uniq { |p| p[:pattern] }
14
14
  }
15
15
  end
16
16
 
17
+ def extract_magic_comments(content)
18
+ keys = []
19
+
20
+ # Regex to match both 'inertia-i18n-use' and 'i18n-tasks-use'
21
+ # Group 1: key
22
+ comment_pattern = /(?:inertia-i18n-use|i18n-tasks-use)\s+([a-zA-Z0-9_.]+)/
23
+
24
+ # Single line comments: // inertia-i18n-use key.name
25
+ content.scan(%r{//\s*#{comment_pattern}}) do |match|
26
+ line = content[0..Regexp.last_match.begin(0)].count("\n") + 1
27
+ keys << {key: match[0], line: line}
28
+ end
29
+
30
+ # Block comments: /* inertia-i18n-use key.name */
31
+ content.scan(%r{/\*\s*#{comment_pattern}\s*\*/}) do |match|
32
+ line = content[0..Regexp.last_match.begin(0)].count("\n") + 1
33
+ keys << {key: match[0], line: line}
34
+ end
35
+
36
+ keys
37
+ end
38
+
17
39
  def extract_static_keys(content)
18
40
  keys = []
19
41
 
@@ -23,25 +45,22 @@ module InertiaI18n
23
45
  escaped_func = Regexp.escape(func)
24
46
 
25
47
  # Matches t('key'), t("key"), t(`key`)
26
-
27
- # Uses a lookahead to ensure it's a translation function call
28
- # Uses lookbehind (?<![\w]) to ensure we don't match end of other words (e.g. split)
29
48
  content.scan(/(?<!\w)#{escaped_func}\(\s*(['"`])([^'"`]+)\1/) do |match|
30
49
  quote_type, key = match
31
-
32
- # If it's a backtick, ensure it's not a template literal with interpolation
33
-
34
50
  next if quote_type == "`" && key.include?("${")
35
51
 
36
- keys << key
52
+ line = content[0..Regexp.last_match.begin(0)].count("\n") + 1
53
+ keys << {key: key, line: line}
37
54
  end
38
55
  end
39
56
 
40
- # Extract keys from object properties (e.g., titleKey: "some.key")
57
+ # Extract keys from object properties
41
58
  config.key_properties.each do |prop|
42
- # Matches: titleKey: "some.key" or titleKey: 'some.key'
43
59
  content.scan(/#{Regexp.escape(prop)}\s*:\s*(['"])([^'"]+)\1/) do |_quote, key|
44
- keys << key if looks_like_i18n_key?(key)
60
+ if looks_like_i18n_key?(key)
61
+ line = content[0..Regexp.last_match.begin(0)].count("\n") + 1
62
+ keys << {key: key, line: line}
63
+ end
45
64
  end
46
65
  end
47
66
 
@@ -31,9 +31,9 @@ module InertiaI18n
31
31
  keys = []
32
32
 
33
33
  # v-t directive: v-t="'key'" or v-t='"key"'
34
-
35
34
  content.scan(/v-t\s*=\s*(["'])['"]([^'"]+)['"]\1/) do |match|
36
- keys << match[1]
35
+ line = content[0..Regexp.last_match.begin(0)].count("\n") + 1
36
+ keys << {key: match[1], line: line}
37
37
  end
38
38
 
39
39
  keys
@@ -2,17 +2,30 @@
2
2
 
3
3
  module InertiaI18n
4
4
  class ScanResults
5
- attr_reader :files, :static_keys, :dynamic_patterns
5
+ attr_reader :files, :static_keys, :dynamic_patterns, :occurrences
6
6
 
7
7
  def initialize
8
8
  @files = {}
9
9
  @static_keys = Set.new
10
10
  @dynamic_patterns = []
11
+ @occurrences = Hash.new { |h, k| h[k] = [] }
11
12
  end
12
13
 
13
14
  def add_file(file, keys)
14
15
  @files[file] = keys
15
- keys[:static].each { |key| @static_keys.add(key) }
16
+
17
+ keys[:static].each do |usage|
18
+ # Handle both legacy string keys and new usage objects
19
+ if usage.is_a?(Hash)
20
+ key = usage[:key]
21
+ @static_keys.add(key)
22
+ @occurrences[key] << {file: file, line: usage[:line]}
23
+ else
24
+ @static_keys.add(usage)
25
+ @occurrences[usage] << {file: file, line: nil}
26
+ end
27
+ end
28
+
16
29
  @dynamic_patterns.concat(keys[:dynamic])
17
30
  end
18
31
 
@@ -24,7 +37,8 @@ module InertiaI18n
24
37
  {
25
38
  files: @files,
26
39
  static_keys: @static_keys.to_a,
27
- dynamic_patterns: @dynamic_patterns
40
+ dynamic_patterns: @dynamic_patterns,
41
+ occurrences: @occurrences
28
42
  }
29
43
  end
30
44
  end
@@ -18,12 +18,6 @@ module InertiaI18n
18
18
  results
19
19
  end
20
20
 
21
- private
22
-
23
- def frontend_files
24
- @config.scan_paths.flat_map { |pattern| Dir.glob(pattern) }
25
- end
26
-
27
21
  def parser_for_file(file)
28
22
  ext = File.extname(file).downcase
29
23
 
@@ -40,5 +34,11 @@ module InertiaI18n
40
34
  Parsers::BaseParser.new
41
35
  end
42
36
  end
37
+
38
+ private
39
+
40
+ def frontend_files
41
+ @config.scan_paths.flat_map { |pattern| Dir.glob(pattern) }
42
+ end
43
43
  end
44
44
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module InertiaI18n
4
- VERSION = "0.5.0"
4
+ VERSION = "0.6.0"
5
5
  end
data/lib/inertia_i18n.rb CHANGED
@@ -1,5 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "yaml"
4
+ require "json"
5
+ require "i18n"
6
+
3
7
  require_relative "inertia_i18n/version"
4
8
  require_relative "inertia_i18n/configuration"
5
9
  require_relative "inertia_i18n/converter"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: inertia_i18n
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexey Poimtsev
@@ -161,6 +161,7 @@ files:
161
161
  - lib/inertia_i18n/converter.rb
162
162
  - lib/inertia_i18n/file_converter.rb
163
163
  - lib/inertia_i18n/health_checker.rb
164
+ - lib/inertia_i18n/i18n_tasks/scanner.rb
164
165
  - lib/inertia_i18n/locale_loader.rb
165
166
  - lib/inertia_i18n/normalizer.rb
166
167
  - lib/inertia_i18n/parsers/base_parser.rb