Skip to content

Conversation

@justin808
Copy link
Member

@justin808 justin808 commented Nov 22, 2025

Summary

The react_on_rails:locale rake task is now idempotent, automatically skipping generation when locale files are already up-to-date. This addresses issue #2090 by making it safe to call multiple times (e.g., in Shakapacker's precompile_hook) without duplicate work or race conditions.

Key Improvements

  • Idempotent behavior: Task skips regeneration when locale files are newer than source YAML files
  • Force flag: Added force=true option to override timestamp checking and force regeneration
  • Clear messaging: Outputs helpful messages when skipping or generating files
  • Safe coordination: Can be called multiple times in precompile hooks, development servers, and CI without issues

Changes

Core Implementation

  • lib/tasks/locale.rake: Added force=true parameter support and improved task description
  • lib/react_on_rails/locales/base.rb: Modified compile() and Base#initialize() to accept and handle force parameter, added informative output messages

Tests

  • spec/react_on_rails/locales_spec.rb: Added tests for force parameter propagation to ToJson and ToJs
  • spec/react_on_rails/locales_to_js_spec.rb: Added test verifying force flag bypasses timestamp checking

Documentation

  • docs/building-features/i18n.md: Added recommended pattern for using Shakapacker's precompile_hook with idempotent locale generation
  • CHANGELOG.md: Added entry documenting the improvement

Usage

Normal use (skips if up-to-date):

bundle exec rake react_on_rails:locale

Force regeneration:

bundle exec rake react_on_rails:locale force=true

Recommended shakapacker.yml configuration:

default: &default
  precompile_hook: "bundle exec rake react_on_rails:locale"

Testing

All existing tests pass, plus new tests added to verify:

  • Force parameter is properly passed through the call chain
  • Idempotent behavior works correctly (skips when up-to-date)
  • Force flag bypasses the up-to-date check

Fixes #2090

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Improved

    • Automatic precompile hook coordination in the development environment prevents duplicate hook execution during startup.
    • Idempotent locale generation skips regeneration when locales are current (optional force flag available to regenerate).
  • Documentation

    • Enhanced guidance for development process management and i18n configuration workflow.

✏️ Tip: You can customize this high-level summary in your review settings.

## Summary

The `react_on_rails:locale` rake task is now idempotent, automatically
skipping generation when locale files are already up-to-date. This
addresses issue #2090 by making it safe to call multiple times (e.g.,
in Shakapacker's `precompile_hook`) without duplicate work or race
conditions.

## Key Improvements

- **Idempotent behavior**: Task skips regeneration when locale files
  are newer than source YAML files
- **Force flag**: Added `force=true` option to override timestamp
  checking and force regeneration
- **Clear messaging**: Outputs helpful messages when skipping or
  generating files
- **Safe coordination**: Can be called multiple times in precompile
  hooks, development servers, and CI without issues

## Changes

### Core Implementation

- `lib/tasks/locale.rake`: Added `force=true` parameter support and
  improved task description
- `lib/react_on_rails/locales/base.rb`: Modified `compile()` and
  `Base#initialize()` to accept and handle `force` parameter, added
  informative output messages

### Tests

- `spec/react_on_rails/locales_spec.rb`: Added tests for force
  parameter propagation to ToJson and ToJs
- `spec/react_on_rails/locales_to_js_spec.rb`: Added test verifying
  force flag bypasses timestamp checking

### Documentation

- `docs/building-features/i18n.md`: Added recommended pattern for using
  Shakapacker's `precompile_hook` with idempotent locale generation
- `CHANGELOG.md`: Added entry documenting the improvement

## Usage

### Normal use (skips if up-to-date):
```bash
bundle exec rake react_on_rails:locale
```

### Force regeneration:
```bash
bundle exec rake react_on_rails:locale force=true
```

### Recommended shakapacker.yml configuration:
```yaml
default: &default
  precompile_hook: "bundle exec rake react_on_rails:locale"
```

Fixes #2090

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 22, 2025

Walkthrough

This PR implements automatic precompile hook coordination in React on Rails' development server manager and adds idempotent locale generation. The changes enable bin/dev to run Shakapacker's precompile hook once before spawning processes and set an environment variable to prevent duplicate execution, while making locale generation skip regeneration if files are already up-to-date.

Changes

Cohort / File(s) Summary
Changelog and Documentation
CHANGELOG.md, docs/building-features/i18n.md, docs/building-features/process-managers.md
Updates changelog with new features; rewrites i18n locale-generation guidance to recommend Shakapacker precompile_hook pattern with idempotent skip logic; documents precompile hook integration in bin/dev flow with environment variable propagation and version requirements
Precompile Hook Runtime Support
lib/react_on_rails/dev/server_manager.rb
Adds precompile hook invocation logic: run_precompile_hook_if_present method, version-checking via warn_if_shakapacker_version_too_old, SHAKAPACKER_SKIP_PRECOMPILE_HOOK environment variable management, early help-flag detection, and hook bypass for kill/help commands
Idempotent Locale Generation
lib/react_on_rails/locales/base.rb, lib/tasks/locale.rake
Adds optional force parameter to locale compilation workflow; implements timestamp-based skip logic to skip generation if locale files are current; updates rake task to parse force environment variable
Type Signatures
sig/react_on_rails/locales.rbs
Establishes Ruby type signatures for ReactOnRails::Locales module, including compile method, Base/ToJs/ToJson classes, and internal helper methods
Test Coverage
spec/react_on_rails/dev/server_manager_spec.rb, spec/react_on_rails/locales_spec.rb, spec/react_on_rails/locales_to_js_spec.rb
Adds comprehensive test suite for precompile hook execution scenarios, environment variable handling, version warnings, and force flag propagation; tests idempotent behavior with force override

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant bin_dev as bin/dev
    participant ServerManager as ServerManager<br/>(precompile_hook)
    participant Hook as Precompile Hook<br/>(rake locale)
    participant Spawner as Process Spawner<br/>(Overmind/Foreman)
    participant Webpack as Webpack<br/>(skips hook)
    
    User->>bin_dev: Start development server
    bin_dev->>ServerManager: run_from_command_line(ARGV)
    
    alt Precompile hook configured
        ServerManager->>Hook: Execute precompile_hook once
        activate Hook
        Hook->>Hook: Check if locales outdated
        alt Locales up-to-date
            Hook->>Hook: Skip (idempotent)
        else Locales outdated or force=true
            Hook->>Hook: Generate locale files
        end
        deactivate Hook
        Hook-->>ServerManager: ✓ Complete
    end
    
    ServerManager->>ServerManager: Set SHAKAPACKER_SKIP_PRECOMPILE_HOOK=true
    ServerManager->>Spawner: Spawn processes with env var set
    activate Spawner
    Spawner->>Webpack: Start webpack --watch
    activate Webpack
    Webpack->>Webpack: Detect SHAKAPACKER_SKIP_PRECOMPILE_HOOK
    Webpack->>Webpack: Skip running precompile_hook
    deactivate Webpack
    deactivate Spawner
    
    Spawner-->>User: ✓ Development environment ready
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • Server manager logic: Review run_precompile_hook_if_present and warn_if_shakapacker_version_too_old for correct environment variable handling and version detection
  • Idempotent locale generation: Verify timestamp checking logic in files_are_outdated correctly handles the "files absent" case and force flag behavior
  • Force parameter propagation: Ensure the force parameter flows correctly from rake task through compile methods to locale writers (ToJs/ToJson)
  • Test coverage: Review server_manager_spec comprehensively for precompile hook scenarios, version checks, and environment variable assertions

Possibly related issues

Possibly related PRs

Suggested labels

enhancement, documentation

Suggested reviewers

  • Judahmeek
  • alexeyr-ci
  • AbanoubGhadban

Poem

🐰 Hooks now hop in perfect time,
No more duplicate locale chimes,
Webpack skips with ENV so bright,
Idempotent—fast and right!
bin/dev coordination—
Dev server's cause for celebration!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 44.44% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and accurately summarizes the main change: implementing idempotent locale generation with a force flag option, which directly addresses the core objective of the PR.
Linked Issues check ✅ Passed The PR successfully implements all primary coding requirements from issue #2090: idempotent locale generation with timestamp checking, force flag support, precompile hook coordination with SHAKAPACKER_SKIP_PRECOMPILE_HOOK, updated rake task, test coverage, and documentation updates.
Out of Scope Changes check ✅ Passed All changes are directly aligned with issue #2090 objectives: locale generation idempotency, force flag, precompile hook coordination, bin/dev updates, documentation updates, and comprehensive test coverage. No unrelated changes detected.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch jg-/coordinate-precompile-hooks

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@claude
Copy link

claude bot commented Nov 22, 2025

Code Review

This PR successfully implements idempotent locale generation with a force flag. Overall the implementation is solid and well-tested. Here's my detailed feedback:


✅ Strengths

1. Clean Implementation

  • The force parameter is threaded cleanly through the call chain: rake task → Locales.compileBase.new
  • The logic properly inverts the condition: \!force && \!obsolete? correctly skips when files are up-to-date unless forced
  • User-facing messages are clear and helpful

2. Excellent Test Coverage

  • Tests verify the force parameter propagates to both ToJson and ToJs
  • The timestamp-based test in locales_to_js_spec.rb:50-70 properly validates the force flag bypasses up-to-date checks
  • Good use of sleep 0.01 to ensure timestamp differences in tests

3. Good Documentation

  • The i18n.md update provides a clear recommended pattern using Shakapacker's precompile_hook
  • Changelog entry is well-formatted and informative

🔍 Potential Issues & Suggestions

1. Missing RBS Type Signatures (Minor)

Per CLAUDE.md, this project uses RBS for type checking. The new force: parameter should have type signatures added:

# sig/react_on_rails/locales.rbs
module ReactOnRails
  module Locales
    def self.compile: (?force: bool) -> (ToJs | ToJson)
    
    class Base
      def initialize: (?force: bool) -> void
    end
  end
end

Action: Check if sig/react_on_rails/locales.rbs exists and update it, or create if needed. Run bundle exec rake rbs:validate after adding.

2. Race Condition in File Timestamp Checking (Low Severity)

The files_are_outdated method in base.rb:61-65 compares file timestamps:

def files_are_outdated
  latest_yml = locale_files.map { |file| File.mtime(file) }.max
  earliest = exist_files.map { |file| File.mtime(file) }.min
  latest_yml > earliest
end

Issue: If two processes run simultaneously, both might see obsolete? == true and regenerate files concurrently, potentially corrupting the output.

Suggested improvement: Consider using a lockfile pattern for production environments:

def with_generation_lock
  lockfile = "\#{i18n_dir}/.generation.lock"
  File.open(lockfile, File::CREAT | File::WRONLY) do |f|
    f.flock(File::LOCK_EX)
    yield
  end
ensure
  File.delete(lockfile) if File.exist?(lockfile)
end

Note: This might be overkill for typical usage. If Shakapacker's precompile_hook runs serially, it's fine. But worth considering for CI parallelism or dev servers with multiple workers.

3. Test Reliability: Time-Based Testing (Low Severity)

The test at locales_to_js_spec.rb:50-70 uses:

  • sleep 0.01 to ensure timestamp differences
  • Time.current + 1.minute to set future timestamps

Potential issue: On very slow/busy CI systems, sleep 0.01 might not be enough to guarantee different mtimes (filesystem timestamp resolution varies).

Suggested improvement:

# More robust approach
sleep 0.1  # Increase to 100ms for filesystem consistency
# Or use explicit mtime setting without relying on current time

4. Return Value Consistency (Minor)

The Base#initialize method has inconsistent return values:

  • Returns nil when i18n_dir.nil?
  • Returns nil when files are up-to-date (after the change)
  • Implicitly returns the result of puts when files are generated

Suggestion: This is fine for a constructor, but consider documenting the behavior or returning self consistently if the return value is ever used.

5. ENV Variable Parsing (Very Minor)

In locale.rake:

force = ENV["force"] == "true"

This only accepts force=true. Consider also accepting force=1, force=yes, etc., for flexibility:

force = %w[true 1 yes].include?(ENV["force"]&.downcase)

🔒 Security Considerations

No security concerns identified. The changes:

  • Don't introduce any new file I/O vulnerabilities
  • Don't parse untrusted input (ENV variable is simple boolean)
  • Don't change the YAML parsing logic (which already has safe load options)

⚡ Performance Considerations

Performance impact is positive:

  • Skipping regeneration when files are up-to-date saves significant time in development
  • The overhead of timestamp checking is negligible (just stat calls)
  • No N+1 queries or expensive operations added

📋 Recommendations

High Priority

  • Add RBS type signatures for the new force parameter (per CLAUDE.md requirements)
  • Run bundle exec rake rbs:validate to ensure no type errors

Medium Priority

  • Consider adding file locking if concurrent regeneration is a concern
  • Increase sleep duration in test to 0.1s for better reliability

Low Priority

  • Support more flexible ENV variable values (1, yes, etc.)
  • Document return value behavior of Base#initialize

🎯 Final Verdict

APPROVED ✅ - This is a well-implemented feature that solves a real problem (duplicate work in precompile hooks). The code is clean, well-tested, and the documentation is excellent.

The suggestions above are minor improvements, not blockers. The implementation is production-ready as-is.

Great work! 🚀

@claude
Copy link

claude bot commented Nov 22, 2025

Code Review - PR #2092

Overview

This PR successfully implements idempotent locale generation with a force flag, addressing issue #2090. The implementation is clean, well-tested, and follows the project's conventions. Overall, this is a high-quality contribution that improves developer experience.


✅ Strengths

1. Clean Architecture

  • The force parameter is properly threaded through the call chain: rake task → compile()ToJs/ToJson.new()
  • Early return pattern in Base#initialize keeps the logic simple and readable
  • Minimal changes to existing code, reducing risk of regression

2. Excellent Test Coverage

  • Unit tests verify force parameter propagation (locales_spec.rb:39-54)
  • Integration test validates the idempotent behavior with force override (locales_to_js_spec.rb:54-70)
  • Tests use proper timestamp manipulation to verify behavior
  • The new test at line 54-70 is particularly well-designed: it ensures files are up-to-date, forces regeneration, and validates the mtime changed

3. User Experience

  • Clear console messages inform users when skipping/generating
  • Helpful documentation in the rake task description
  • Example usage in both README and CHANGELOG

4. Documentation

  • Excellent addition of Shakapacker precompile_hook recommendation in i18n.md
  • Clear explanation of when to use force flag
  • CHANGELOG entry is properly formatted per project guidelines

🔍 Minor Observations & Suggestions

1. Edge Case: Empty locale_files Array (Low Priority)

In base.rb:84-92, if i18n_yml_dir is nil and Rails.application.config.i18n.load_path returns an empty array or nil, locale_files could be empty.

Current behavior:

def files_are_outdated
  latest_yml = locale_files.map { |file| File.mtime(file) }.max
  earliest = exist_files.map { |file| File.mtime(file) }.min
  latest_yml > earliest
end

If locale_files is empty, latest_yml would be nil, causing a NoMethodError when comparing nil > earliest.

Likelihood: Very low - the check_config_directory_exists validation should catch most misconfigurations.

Suggestion: Consider adding a guard:

def files_are_outdated
  return true if locale_files.empty?  # Force regeneration if no source files found
  
  latest_yml = locale_files.map { |file| File.mtime(file) }.max
  earliest = exist_files.map { |file| File.mtime(file) }.min
  latest_yml > earliest
end

This is optional and could be addressed in a follow-up if deemed necessary.

2. Test Improvement: sleep 0.01 (Nitpick)

spec/react_on_rails/locales_to_js_spec.rb:59:

sleep 0.01 # Ensure timestamp difference

The comment suggests this ensures timestamp difference, but the next line immediately sets future_time = Time.current + 1.minute which already guarantees a difference. The sleep appears unnecessary.

Impact: None - the test works correctly. This is purely for code clarity.

3. Console Output Uses puts (Style Question)

base.rb:43, 49 use puts for user feedback. This is fine for a Rake task, but worth noting:

  • puts output doesn't go through Rails logger
  • Won't be captured by test frameworks or structured logging
  • Direct stdout is appropriate for CLI tools, so this is likely intentional

Verdict: Current approach is correct for a Rake task. No change needed.

4. CHANGELOG PR Number Mismatch

The CHANGELOG entry references:

[PR 2091](https://github.com/shakacode/react_on_rails/pull/2091)

But this is PR #2092. Should be:

[PR 2092](https://github.com/shakacode/react_on_rails/pull/2092)

This needs to be fixed before merging.


🔒 Security Considerations

No security concerns identified:

  • No user input is processed beyond the boolean force flag
  • File operations are limited to configured directories
  • Proper error handling for YAML parsing (existing code)

⚡ Performance Considerations

Excellent performance improvements:

  • Idempotent behavior prevents unnecessary regeneration
  • Timestamp checking is O(n) where n = number of locale files (acceptable)
  • Safe for use in hot-reloading development environments

Recommendation: This is perfect for the Shakapacker precompile_hook use case.


🧪 Testing Recommendations

The test coverage is strong, but consider these additional manual tests before merging:

  1. Test with Shakapacker integration:

    # Add to spec/dummy/config/shakapacker.yml
    precompile_hook: 'bundle exec rake react_on_rails:locale'
    
    # Then run webpack compilation multiple times
    cd spec/dummy && yarn build
    # Should see "Locale files are up to date" message
  2. Test force flag:

    cd spec/dummy
    bundle exec rake react_on_rails:locale force=true
    # Should regenerate regardless of timestamps
  3. Verify CI passes - especially the linting checks mentioned in CLAUDE.md


📋 Action Items

Required Before Merge:

  • Fix CHANGELOG PR number: 2091 → 2092

Optional Improvements (can be follow-up PRs):

  • Consider adding guard for empty locale_files array (edge case protection)
  • Remove unnecessary sleep 0.01 from test for clarity

Final Verdict

LGTM with one required fix (CHANGELOG PR number).

This is a well-implemented feature that:

  • ✅ Solves a real developer pain point
  • ✅ Has excellent test coverage
  • ✅ Includes clear documentation
  • ✅ Follows project conventions
  • ✅ Maintains backward compatibility
  • ✅ Improves performance through smart caching

Great work! The idempotent behavior combined with the Shakapacker integration guidance will significantly improve the developer experience.


References:

@justin808
Copy link
Member Author

/run-skipped-ci

Changes:
- Fix CHANGELOG PR reference (2091 → 2092)
- Add safety guard for empty locale_files array to prevent nil comparison errors
- Improve ENV variable parsing to accept force=true|1|yes (more flexible)
- Remove unnecessary sleep in test (timestamp difference already guaranteed)
- Add RBS type signatures for Locales module and classes

All tests pass and RuboCop clean.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@github-actions
Copy link
Contributor

🚀 Full CI Mode Enabled

Triggered all workflows for full CI coverage

Note: Added the full-ci label to this PR. All future commits will run the full CI suite (including minimum dependency tests) until the label is removed.

To disable full CI mode, use the /stop-run-skipped-ci command.

View progress in the Actions tab.

@claude
Copy link

claude bot commented Nov 22, 2025

Code Review

Thanks for this PR! This is a solid implementation that adds idempotent behavior to locale generation. Here's my detailed feedback:


✅ Strengths

1. Clean Architecture

  • The force parameter is properly threaded through the call chain (rake task → compile() → ToJs/ToJson → Base#initialize)
  • The implementation correctly uses optional parameters with sensible defaults
  • Good separation of concerns between timestamp checking and forced regeneration

2. User Experience

  • Helpful console messages inform users when generation is skipped vs. when it occurs
  • Clear documentation in rake task description about the force option
  • Flexible ENV parsing accepts true, 1, or yes for better UX

3. Test Coverage

  • Tests verify force parameter propagation through the entire chain
  • Tests confirm idempotent behavior (skipping when up-to-date)
  • Tests verify force flag bypasses the timestamp check
  • Good use of RSpec shared examples for DRY testing

4. Documentation

  • Excellent addition to i18n.md recommending the Shakapacker precompile_hook pattern
  • Clear usage examples in both docs and rake task description
  • CHANGELOG entry properly formatted per project guidelines

5. Type Safety

  • New RBS signatures provide static type checking for the Locales module
  • Comprehensive method signatures cover all public and private methods

🔍 Issues & Suggestions

1. Critical: Empty Array Safety ⚠️

File: lib/react_on_rails/locales/base.rb:62

The check for empty locale_files is good, but there's a potential edge case:

def files_are_outdated
  return true if locale_files.empty?

  latest_yml = locale_files.map { |file| File.mtime(file) }.max
  earliest = exist_files.map { |file| File.mtime(file) }.min
  latest_yml > earliest
end

Issue: If exist_files is empty (no generated files exist yet), .min returns nil, causing nil > Time comparison error.

Recommendation:

def files_are_outdated
  return true if locale_files.empty?
  return true if exist_files.empty?  # No generated files yet

  latest_yml = locale_files.map { |file| File.mtime(file) }.max
  earliest = exist_files.map { |file| File.mtime(file) }.min
  latest_yml > earliest
end

However, looking at the code flow, obsolete? already checks exist_files.empty?, so this might be redundant. Consider whether this method should even be called when exist_files.empty?.

2. Minor: Inconsistent Return Values

File: lib/react_on_rails/locales/base.rb:39-45

def initialize(force: false)
  return if i18n_dir.nil?

  if \!force && \!obsolete?
    puts "Locale files are up to date, skipping generation. Use force=true to regenerate."
    return
  end

  @translations, @defaults = generate_translations
  convert
  puts "Generated locale files in #{i18n_dir}"
end

Issue: The method returns nil in early exit cases but doesn't have a consistent return value when generation succeeds. This is fine for Ruby, but the RBS signature shows -> void, which is correct.

Suggestion: Consider returning self for potential method chaining, though not critical for this use case.

3. RBS Type Accuracy

File: sig/react_on_rails/locales.rbs

Some observations:

def locale_files: () -> Array[String]

This should account for the memoization returning either an Array or the result from Rails.application.config.i18n.load_path:

def locale_files: () -> Array[String]

Actually, looking at the implementation, load_path could return various types. Consider:

def locale_files: () -> (Array[String] | nil)

But the current implementation wraps it with truthy_presence(...).presence, which would return nil if falsy. The type might need refinement based on actual behavior.

4. Test Improvement: Time-based Race Condition

File: spec/react_on_rails/locales_to_js_spec.rb:50-68

The test is good, but there's a minor improvement possible:

it "updates files when force is true" do
  initial_mtime = File.mtime(translations_path)

  future_time = Time.current + 1.minute
  FileUtils.touch(translations_path, mtime: future_time)
  FileUtils.touch(default_path, mtime: future_time)

  described_class.new(force: true)

  expect(File.mtime(translations_path)).not_to eq(future_time)
  expect(File.mtime(translations_path)).to be > initial_mtime
end

Suggestion: The second expectation (be > initial_mtime) could fail if the test runs extremely fast. Consider using be_within(1.second).of(Time.current) or just verifying it's different from future_time:

expect(File.mtime(translations_path)).not_to eq(future_time)
expect(File.mtime(default_path)).not_to eq(future_time)

5. Documentation: Edge Case Clarification

File: docs/building-features/i18n.md

The documentation is excellent, but could mention what happens if locale files don't exist yet:

The idempotent locale generation task can be safely called multiple times and will skip generation if files are already up-to-date. **On first run or when locale YAML files change, generation will occur automatically.**

🛡️ Security Considerations

No security concerns identified

  • The force parameter is read from ENV and properly validated
  • No SQL injection, XSS, or command injection vectors
  • File operations use safe paths from configuration

⚡ Performance Considerations

Performance improvements achieved

  • Idempotent behavior prevents unnecessary file regeneration
  • Timestamp checking is O(n) where n = number of locale files (acceptable)
  • No blocking operations or expensive computations

Minor optimization opportunity: The files_are_outdated method could short-circuit earlier if it finds any outdated file instead of computing all mtimes, but the current implementation is clear and performance is likely negligible for typical use cases.


📋 Checklist Items

Before merging, please verify:

  • Run bundle exec rubocop (per CLAUDE.md requirements) ✅ (assuming this passes)
  • Run bundle exec rake rbs:validate to validate new RBS signatures
  • Run bundle exec rake rbs:steep to type-check (may show existing errors per CLAUDE.md)
  • Consider adding a test for the empty exist_files edge case mentioned above
  • All CI checks pass (currently queued)

🎯 Overall Assessment

This is a well-implemented feature that solves a real problem. The code quality is high, tests are comprehensive, and documentation is excellent. The few suggestions above are minor improvements rather than blocking issues.

Recommendation: APPROVE with minor suggestions for consideration

Great work! 🚀

@claude
Copy link

claude bot commented Nov 22, 2025

Code Review - PR #2092

✅ Overall Assessment

This PR implements a well-designed solution for making the locale generation task idempotent. The implementation is clean, well-tested, and follows the project's conventions. Great work!


🎯 Strengths

  1. Clean API Design: The force parameter is intuitive and follows common CLI conventions (accepting 'true', '1', 'yes')

  2. Comprehensive Testing: Excellent test coverage including:

    • Force parameter propagation through the call chain
    • Idempotent behavior verification
    • Force flag override testing
  3. Good User Experience: Clear console messages inform users when files are skipped or generated

  4. Type Safety: RBS signatures properly updated to reflect the new optional parameter

  5. Documentation: The i18n docs now include the recommended Shakapacker precompile_hook pattern, which is exactly what users need


🔍 Code Quality Observations

lib/tasks/locale.rake

  • ✅ Clean implementation parsing the force flag from ENV
  • ✅ Good task description with usage examples

lib/react_on_rails/locales/base.rb

  • ✅ Proper early return pattern when i18n_dir is nil
  • ✅ Clear separation between idempotent check and force override
  • ✅ Helpful console output for both skip and success cases

Test Coverage

  • ✅ Tests verify parameter passing at all layers (compile → ToJs/ToJson → Base)
  • ✅ Shared examples ensure consistency across both JS and JSON output formats
  • ✅ Edge case testing with timestamp manipulation

💡 Minor Suggestions (Optional)

  1. Console Output Consistency: Consider using Rails logger instead of puts for better integration with Rails logging:

    # Instead of:
    puts "Locale files are up to date, skipping generation. Use force=true to regenerate."
    
    # Consider:
    Rails.logger.info "Locale files are up to date, skipping generation. Use force=true to regenerate."

    This would respect Rails log levels and integrate better with production logging.

  2. Test Robustness: In spec/react_on_rails/locales_to_js_spec.rb:54-69, the test uses Time.current + 1.minute which could theoretically cause flakiness if the test runs slowly. Consider using a more explicit time value:

    # Instead of relative time
    future_time = Time.current + 1.minute
    
    # Consider fixed future time
    future_time = Time.new(2030, 1, 1)
  3. Documentation Enhancement: In the docs, you might want to mention that the idempotent check is based on file modification times:

    The task compares the modification times of generated locale files against 
    the source YAML files. If all generated files are newer than all source files,
    generation is skipped.

🔒 Security Considerations

✅ No security concerns identified. The force flag is read from ENV and properly sanitized with a whitelist approach.


⚡ Performance Considerations

Excellent: The idempotent behavior prevents unnecessary file I/O and YAML parsing when files are up-to-date. This will significantly improve performance in development workflows and CI pipelines.

Timestamp checking overhead: The obsolete? check adds minimal overhead:

  • File existence checks: O(n) where n = number of generated files (typically 2)
  • mtime comparisons: Very fast system calls
  • Early return prevents expensive YAML parsing and file writing

📝 CHANGELOG Entry

✅ The CHANGELOG entry is well-written and follows the project conventions:

  • Includes PR number and author
  • Clearly explains the feature and its benefits
  • Placed in the "Improved" section (appropriate for enhancements)

✨ RBS Type Signatures

The RBS signatures in sig/react_on_rails/locales.rbs look correct:

  • compile method signature: (?force: bool) -> (ToJs | ToJson)
  • Base#initialize signature: (?force: bool) -> void

🧪 Testing Recommendations

Before merging, please verify:

  1. ✅ Run bundle exec rubocop (should pass based on project requirements)
  2. ✅ Run bundle exec rake rbs:validate to verify type signatures
  3. ✅ Test the actual workflow manually:
    # First run - should generate files
    bundle exec rake react_on_rails:locale
    
    # Second run - should skip
    bundle exec rake react_on_rails:locale
    
    # Force regeneration - should generate
    bundle exec rake react_on_rails:locale force=true

📚 Documentation Quality

The documentation changes in docs/building-features/i18n.md are excellent:

  • ✅ Clear recommendation for Shakapacker integration
  • ✅ Example configuration snippet
  • ✅ Explanation of when to use force flag
  • ✅ Maintains backward compatibility guidance

🎉 Conclusion

This is a solid, well-implemented PR that solves a real problem (issue #2090). The code is clean, well-tested, and properly documented. The idempotent behavior will make life easier for users who want to integrate locale generation into their build pipelines.

Recommendation: ✅ Approve and merge (pending CI passing)

Great work, @justin808! This is exactly the kind of improvement that makes React on Rails more pleasant to use.


Review generated with Claude Code - focusing on code quality, testing, security, and performance considerations.

@claude
Copy link

claude bot commented Nov 22, 2025

Code Review - PR #2092

Summary

This PR successfully makes the react_on_rails:locale rake task idempotent with force flag support. The implementation is clean and well-tested. Overall, this is high-quality work with just a few minor suggestions.


✅ Strengths

  1. Well-designed implementation

    • Clean separation of concerns: force parameter flows from rake task → compile → Base class
    • Backward compatible: no breaking changes, defaults to existing behavior
    • User-friendly messaging when skipping generation
  2. Excellent test coverage

    • Tests verify force parameter propagation through the call chain
    • Tests verify idempotent behavior (skips when up-to-date)
    • Tests verify force flag bypasses up-to-date check
    • Edge case handled: empty locale_files array
  3. Great documentation

    • Clear usage examples in rake task description
    • Recommended Shakapacker integration pattern documented
    • CHANGELOG entry follows project conventions
  4. Type safety

    • RBS signatures added for new force parameter
    • Comprehensive type definitions for all methods in Locales module

🔍 Minor Issues & Suggestions

1. Potential nil error in files_are_outdated (Low priority)

def files_are_outdated
  return true if locale_files.empty?

  latest_yml = locale_files.map { |file| File.mtime(file) }.max
  earliest = exist_files.map { |file| File.mtime(file) }.min
  latest_yml > earliest
end

Issue: If exist_files is empty, earliest will be nil, causing latest_yml > earliest to raise an error.

Suggestion: Add a guard:

def files_are_outdated
  return true if locale_files.empty?
  return true if exist_files.empty?  # Add this line

  latest_yml = locale_files.map { |file| File.mtime(file) }.max
  earliest = exist_files.map { |file| File.mtime(file) }.min
  latest_yml > earliest
end

Note: This should already be caught by obsolete? returning true when exist_files.empty?, but defensive programming is good practice.

2. ENV variable parsing could be more Ruby-idiomatic

force = %w[true 1 yes].include?(ENV["force"]&.downcase)

This is fine, but you could consider extracting to a helper if this pattern is used elsewhere:

def self.env_truthy?(key)
  %w[true 1 yes].include?(ENV[key]&.downcase)
end

Low priority - current implementation is clear and works well.

3. Test improvement opportunity

In spec/react_on_rails/locales_to_js_spec.rb, the force test uses:

future_time = Time.current + 1.minute
FileUtils.touch(translations_path, mtime: future_time)

Suggestion: Consider using Time.now instead of Time.current for consistency with File.mtime, or document why Time.current is preferred (ActiveSupport time zone awareness).

4. RBS signatures completeness

The RBS signatures look good, but verify they match the actual return types:

def self.compile: (?force: bool) -> (ToJs | ToJson)

This looks correct - compile returns an instance of either ToJs or ToJson.

Suggestion: Run bundle exec rake rbs:validate to ensure signatures are correct (per CLAUDE.md requirements).


🔒 Security

✅ No security concerns identified:

  • No user input directly used in file paths
  • ENV variable parsing is safe
  • Force flag only affects timestamp checking logic

⚡ Performance

✅ No performance concerns:

  • Idempotent behavior improves performance by skipping unnecessary work
  • File timestamp checking is O(n) on number of locale files (acceptable)
  • No changes to file parsing logic

🧪 Testing Recommendations

  1. Manual testing checklist (per CLAUDE.md requirements):

    # Test normal use (should skip if up-to-date)
    bundle exec rake react_on_rails:locale
    bundle exec rake react_on_rails:locale  # Should skip
    
    # Test force flag
    bundle exec rake react_on_rails:locale force=true
    
    # Test with Shakapacker precompile_hook
    # Add to config/shakapacker.yml:
    # default:
    #   precompile_hook: 'bundle exec rake react_on_rails:locale'
    bundle exec rake assets:precompile
  2. Verify RBS signatures:

    bundle exec rake rbs:validate
    bundle exec rake rbs:steep
  3. Run full test suite:

    bundle exec rake run_rspec

📋 Final Checklist

Before merging, ensure:

  • All tests pass locally
  • RuboCop passes: bundle exec rubocop
  • RBS validation passes: bundle exec rake rbs:validate
  • Manual testing completed (see above)
  • Files end with newline (per CLAUDE.md)

🎯 Verdict

APPROVE with minor suggestions. The implementation is solid, well-tested, and follows project conventions. The suggestions above are optional improvements that don't block merging.

Great work on solving the duplicate work issue in precompile hooks! This will be very helpful for users integrating with Shakapacker.


References:

The bin/dev command now automatically runs Shakapacker's precompile_hook
once before starting development processes and sets
SHAKAPACKER_SKIP_PRECOMPILE_HOOK=true to prevent duplicate execution in
spawned webpack processes.

Key improvements:
- Executes precompile_hook once before launching Procfile processes
- Sets SHAKAPACKER_SKIP_PRECOMPILE_HOOK environment variable
- All spawned processes inherit the variable to prevent re-execution
- Displays warning for Shakapacker versions below 9.4.0
- Skips hook execution for help and kill commands

This eliminates the need for:
- Manual coordination in Procfile.dev
- Sleep hacks to sequence tasks
- Duplicate task calls across processes
- Race conditions when multiple processes generate files

Users can now configure expensive build tasks (locale generation, ReScript
compilation, etc.) once in config/shakapacker.yml and bin/dev handles the
coordination automatically.

Changes:
- lib/react_on_rails/dev/server_manager.rb: Added hook execution logic
  with version warning for Shakapacker < 9.4.0
- spec/react_on_rails/dev/server_manager_spec.rb: Added comprehensive
  tests for hook execution, environment variable setting, and version
  warnings
- docs/building-features/process-managers.md: Updated documentation with
  precompile hook integration details and version requirements
- CHANGELOG.md: Added entry for automatic precompile hook coordination

Addresses #2091

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@claude
Copy link

claude bot commented Nov 22, 2025

Code Review: PR #2092 - Make locale generation idempotent with force flag support

👋 Great work on this PR! This is a well-thought-out solution that addresses the precompile hook coordination problem. Here's my detailed review:


✅ Strengths

1. Excellent Design - Idempotent by Default

The idempotent behavior is properly implemented in lib/react_on_rails/locales/base.rb:

  • Checks file timestamps to skip regeneration when up-to-date (lines 42-44, 66-72)
  • Provides clear user feedback with helpful messages
  • The force flag provides an escape hatch when needed

2. Comprehensive Test Coverage

  • Tests verify force parameter propagation through the call chain (locales_spec.rb)
  • Tests confirm idempotent behavior and force override (locales_to_js_spec.rb:54-69)
  • Extensive tests for bin/dev precompile hook integration with edge cases (server_manager_spec.rb)
  • Tests cover help flag detection, version warnings, and environment variable handling

3. Proper Error Handling

  • run_precompile_hook_if_present exits with clear error message on hook failure (lines 226-233)
  • Good user guidance in error messages
  • Graceful handling of missing hooks

4. Type Safety with RBS

  • Added comprehensive type signatures in sig/react_on_rails/locales.rbs
  • Follows project's type checking standards

5. Documentation Excellence

  • Updated i18n.md with recommended Shakapacker configuration pattern
  • Added detailed process-managers.md section explaining precompile hook integration
  • CHANGELOG entries are well-formatted and informative
  • Clear before/after examples for users

🔍 Observations & Minor Suggestions

1. Edge Case: Empty locale_files Array ✅ Already Handled

In lib/react_on_rails/locales/base.rb:67, you properly handle the case where locale_files.empty?:

def files_are_outdated
  return true if locale_files.empty?  # ✅ Good!
  # ...
end

This is correct - if there are no source files, we should still generate the locale files.

2. Force Flag Parsing 💡 Consider Enhancement

In lib/tasks/locale.rake:18:

force = %w[true 1 yes].include?(ENV["force"]&.downcase)

This is good, but consider if you want to handle the case where someone passes force=false explicitly (currently it would be falsy anyway, so this is fine). The current implementation is solid.

3. Help Flag Detection ✅ Well Done

The early help flag detection in server_manager.rb:154-159 is clever:

command = args.find { |arg| !arg.start_with?("--") && !arg.start_with?("-") }
help_requested = args.any? { |arg| HELP_FLAGS.include?(arg) }

This prevents running the precompile hook when the user just wants help. Smart optimization!

4. Version Warning Logic 💡 Potential Issue

In lib/react_on_rails/dev/server_manager.rb:241-256:

def warn_if_shakapacker_version_too_old
  return unless PackerUtils.shakapacker_version_requirement_met?("9.0.0")
  return if PackerUtils.shakapacker_version_requirement_met?("9.4.0")
  # ... warning
end

Question: What happens if Shakapacker version is < 9.0.0? The warning won't show. Is this intentional?

  • If Shakapacker < 9.0.0, the first return triggers and no warning is shown
  • If Shakapacker is 9.0.0-9.3.x, the warning shows ✅
  • If Shakapacker >= 9.4.0, no warning ✅

Recommendation: Consider adding a comment explaining why versions < 9.0.0 don't trigger a warning (perhaps they don't support precompile hooks at all?).

5. Environment Variable Always Set ⚠️ Design Question

In server_manager.rb:184-189:

unless %w[kill help].include?(command) || help_requested
  run_precompile_hook_if_present
  ENV["SHAKAPACKER_SKIP_PRECOMPILE_HOOK"] = "true"
end

The environment variable is set even when hook_value is nil (no hook configured). This is probably fine since Shakapacker would ignore it anyway, but you could alternatively only set it when a hook exists:

unless %w[kill help].include?(command) || help_requested
  if run_precompile_hook_if_present
    ENV["SHAKAPACKER_SKIP_PRECOMPILE_HOOK"] = "true"
  end
end

However, your current approach is safer as a defense-in-depth measure. ✅

6. Console Output for Up-to-Date Files 💭 UX Consideration

In lib/react_on_rails/locales/base.rb:43:

puts "Locale files are up to date, skipping generation. Use force=true to regenerate."

This is helpful, but consider:

  • Should this use Rails logger instead of puts for consistency?
  • In CI/production, this message will appear every time the rake task runs

Recommendation: Consider using a logger with appropriate log level, or keep as-is if you want visibility. This is a minor style preference.


🔒 Security

No security concerns identified

  • No shell injection risks (force flag is properly parsed as boolean)
  • No path traversal issues
  • Proper escaping in error messages

🎯 Performance

Excellent performance characteristics

  • Timestamp checking is fast (no file parsing unless needed)
  • Idempotent behavior prevents wasted work
  • No regression in build times

📋 Code Quality & Best Practices

Following CLAUDE.md Guidelines ✅

  • ✅ RuboCop compliance (no issues in diff)
  • ✅ Files end with newlines
  • ✅ Comprehensive tests added
  • ✅ CHANGELOG updated properly
  • ✅ RBS signatures added for type safety
  • ✅ User-facing documentation updated

Rails Engine Best Practices ✅

  • ✅ No duplicate rake task loading issues
  • ✅ Proper use of engine initializers
  • ✅ Configuration properly handled

🧪 Test Coverage Assessment

Excellent Coverage ✅

  • Unit tests for force parameter propagation
  • Integration tests for idempotent behavior
  • Edge case tests (empty files, force override)
  • Server manager tests for all command modes
  • Tests for version detection and warnings

Potential Additional Tests (Optional):

  1. Test locale generation with no YAML files - Verify behavior when locale_files is empty
  2. Test rake task force flag parsing - Verify force=TRUE (uppercase) works
  3. Test concurrent execution - Verify no race conditions if hook runs twice simultaneously (though this shouldn't happen with your design)

📚 Documentation Quality

Outstanding documentation

  • Clear before/after examples in docs/building-features/i18n.md
  • Comprehensive explanation in docs/building-features/process-managers.md
  • Good inline code comments
  • Helpful user-facing messages

🎉 Overall Assessment

Rating: Excellent ⭐⭐⭐⭐⭐

This PR demonstrates:

  • Strong software engineering principles (idempotency, separation of concerns)
  • Excellent test coverage and edge case handling
  • Clear documentation and user guidance
  • Proper type safety with RBS
  • No breaking changes

Recommendation: Approve with minor comments addressed

The only item worth considering is clarifying the version warning logic for Shakapacker < 9.0.0 (point #4 above). Everything else is either already excellent or minor style preferences.

Great work! 🚀

@claude
Copy link

claude bot commented Nov 22, 2025

Code Review - PR #2092

Thank you for this comprehensive PR! This is a well-thought-out solution to the precompile hook coordination problem. Here's my detailed review:


🟢 Strengths

1. Excellent Design - Idempotent Behavior

The idempotent locale generation is exactly the right approach. By checking timestamps and skipping when files are up-to-date, this prevents wasteful regeneration while still allowing force override when needed.

2. Great Test Coverage

  • Force parameter propagation tests ✅
  • Idempotent behavior tests ✅
  • Comprehensive bin/dev integration tests ✅
  • Tests for all command paths (dev, static, prod, kill, help) ✅
  • Version-specific warning tests ✅

3. Clear Documentation

The i18n.md and process-managers.md updates clearly explain the new pattern with before/after examples.

4. Good Error Handling

  • Exits cleanly when precompile hook fails
  • Provides helpful error message pointing to shakapacker.yml
  • Warns users on older Shakapacker versions

🟡 Potential Issues & Suggestions

1. Critical: Missing Edge Case in files_are_outdated Logic

Current code (lib/react_on_rails/locales/base.rb:64):

def files_are_outdated
  return true if locale_files.empty?  # <-- Added in PR
  
  latest_yml = locale_files.map { |file| File.mtime(file) }.max
  earliest = exist_files.map { |file| File.mtime(file) }.min
  latest_yml > earliest
end

Issue: What happens if exist_files is empty but locale_files is not?

The obsolete? method has:

def obsolete?
  return true if exist_files.empty?  # <-- Forces generation
  files_are_outdated
end

This is correct! But files_are_outdated will crash with NoMethodError if exist_files is empty (calling .min on empty array returns nil, then comparing with >).

This can never happen because obsolete? short-circuits, but it's fragile. If someone refactors later, this could break.

Recommendation: Add defensive check in files_are_outdated:

def files_are_outdated
  return true if locale_files.empty?
  return true if exist_files.empty?  # <-- Add this for safety
  
  latest_yml = locale_files.map { |file| File.mtime(file) }.max
  earliest = exist_files.map { |file| File.mtime(file) }.min
  latest_yml > earliest
end

2. Minor: Early Command Extraction Logic is Fragile

In lib/react_on_rails/dev/server_manager.rb:156:

# Get the command early to check for help/kill before running hooks
command = args.find { |arg| !arg.start_with?("--") && !arg.start_with?("-") }

Issue: This assumes commands never start with -, but what if someone runs:

bin/dev --verbose hmr

The .find will correctly get "hmr", which is good. But if someone runs:

bin/dev -v

Then command will be nil, which triggers the default HMR mode. This works, but the logic is subtle.

Current behavior is correct, but consider adding a comment explaining this:

# Get the command early (before OptionParser processes args)
# This finds the first non-flag argument, or nil for default mode
command = args.find { |arg| !arg.start_with?("--") && !arg.start_with?("-") }

3. Minor: Inconsistent Help Flag Handling

Two different approaches:

  1. OptionParser's -h flag calls show_help and exit (line 169-172)
  2. Case statement checks for "help", "--help", "-h" (line 187)

Issue: If someone passes -h, OptionParser will handle it and exit. The case statement's "-h" branch is unreachable.

Current code works correctly, but the case statement should only check:

when "help"  # Remove --help and -h since OptionParser handles them
  show_help

4. Security: Command Injection Risk in system() Call

In lib/react_on_rails/dev/server_manager.rb:218:

unless system(hook_value.to_s)

Issue: hook_value comes from config/shakapacker.yml, which is under user control. However, since this is:

  1. A development-only tool (bin/dev)
  2. Reading from local config file (not user input)
  3. The user already controls their codebase

This is acceptable. But consider documenting that precompile_hook should not contain untrusted input (though in this context, that's obvious).

5. Minor: Environment Variable Side Effect

ENV["SHAKAPACKER_SKIP_PRECOMPILE_HOOK"] = "true"

Current behavior: This modifies the global environment permanently.

Consideration: This is intentional and correct for bin/dev, but it means:

  • If bin/dev is required as a library (unusual), this persists
  • Tests need to clean up (✅ you already do this in spec)

This is fine, just noting it's a global side effect.

6. Missing Test Case: Empty locale_files Array

Test coverage gap: The new line in files_are_outdated:

return true if locale_files.empty?

This is not tested. Consider adding a test:

it "considers files outdated when no locale YAML files exist" do
  allow_any_instance_of(described_class).to receive(:locale_files).and_return([])
  
  described_class.new(force: true)
  
  # Should not crash, should generate files
  expect(File.exist?(translations_path)).to be true
end

🔒 Security Considerations

  1. Command injection via system(): Acceptable for dev-only tool reading from local config ✅
  2. Path traversal: Not applicable - paths are config-controlled ✅
  3. Environment variable pollution: Intentional and documented ✅

⚡ Performance Considerations

  1. Timestamp checking: File.mtime calls are fast, negligible overhead ✅
  2. Precompile hook runs once: This is the key performance win ✅
  3. No N+1 issues: File operations are batched appropriately ✅

📊 Code Quality

  1. RuboCop compliance: # rubocop:disable comments are justified ✅
  2. Method complexity: run_from_command_line is complex but well-tested ✅
  3. Naming: Clear and descriptive ✅
  4. Documentation: Excellent inline comments and docs ✅

🎯 Recommendations

Must Fix (Before Merge)

  • Add defensive check in files_are_outdated for empty exist_files

Nice to Have (Optional)

  • Add test for locale_files.empty? path
  • Clean up unreachable case branches in help flag handling
  • Add comment explaining early command extraction logic

Follow-up (Future PR)

  • Consider extracting precompile hook logic to separate class (current implementation is fine, but could be cleaner)

✅ Overall Assessment

Strong approval with minor suggestions. This PR:

  • ✅ Solves the stated problem effectively
  • ✅ Has excellent test coverage
  • ✅ Follows project conventions
  • ✅ Includes clear documentation
  • ✅ Handles edge cases thoughtfully

The issues I've identified are minor and mostly defensive programming suggestions. The core implementation is solid.

Recommendation: Merge after addressing the files_are_outdated defensive check.


Great work! This will significantly improve the developer experience for users with expensive precompile hooks.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
lib/react_on_rails/dev/server_manager.rb (1)

154-189: Consider more robust command detection to avoid edge cases.

The current command detection logic (line 156) finds the first non-flag argument before OptionParser.parse! executes. This can incorrectly identify option values as commands.

Example edge case:

bin/dev --route dashboard
# args = ["--route", "dashboard"]
# command = "dashboard" (incorrect - this is an option value, not a command)
# Precompile hook runs, then exits with "Unknown argument: dashboard"

While this doesn't cause incorrect behavior (the hook runs, then the command exits), it wastes time running the precompile hook before failing.

Suggested improvement:

-  # Get the command early to check for help/kill before running hooks
-  # We need to do this before OptionParser processes flags like -h/--help
-  command = args.find { |arg| !arg.start_with?("--") && !arg.start_with?("-") }
-
-  # Check if help flags are present in args (before OptionParser processes them)
-  help_requested = args.any? { |arg| HELP_FLAGS.include?(arg) }
+  # Check for help flags or help/kill commands before running hooks
+  help_requested = args.any? { |arg| HELP_FLAGS.include?(arg) }
+  skip_hook_commands = %w[help kill]
+  has_skip_command = args.any? { |arg| skip_hook_commands.include?(arg) }

Then update line 186:

-  unless %w[kill help].include?(command) || help_requested
+  unless has_skip_command || help_requested

And retrieve the command after parsing (line 182):

   end.parse!(args)

+  command = args.first

This avoids misidentifying option values as commands and is more explicit about intent.

🧹 Nitpick comments (4)
spec/react_on_rails/locales_to_js_spec.rb (1)

54-69: Spec: good coverage for forced regeneration; could also assert default.js mtime

This example accurately simulates “up-to-date” JS files and verifies that force: true triggers regeneration by checking translations.js mtime. For extra confidence, you could also assert a changed mtime for default.js, but that’s optional since the underlying code writes both files together.

spec/react_on_rails/dev/server_manager_spec.rb (1)

272-431: Specs: comprehensive coverage of precompile hook + skip env behavior

The new examples thoroughly exercise run_from_command_line across modes (dev/static/prod/kill/help/-h) with and without a precompile hook, and correctly validate SHAKAPACKER version gating and SHAKAPACKER_SKIP_PRECOMPILE_HOOK semantics.

One minor hygiene tweak: you already ENV.delete("SHAKAPACKER_SKIP_PRECOMPILE_HOOK") in a before; adding a matching after in this describe to restore or delete the var would further guard against leakage into any future examples that might run after this block.

Based on learnings

lib/react_on_rails/locales/base.rb (1)

7-21: Locales: idempotent compile + force flag design looks correct

The compile(force:) API cleanly forwards the flag to ToJs/ToJson, and Base#initialize(force:) now implements the expected behavior: skip when not forced and files aren’t obsolete, regenerate otherwise, with helpful console messages. The files_are_outdated tweak to treat an empty locale_files set as “outdated” also makes sense so that missing YAMLs trigger regeneration to an empty payload rather than silently skipping.

One small hardening you might consider: files_are_outdated currently calls locale_files.empty?. If locale_files were ever nil (for example, from an unexpected i18n.load_path state), this would raise; using locale_files.blank? (or return true if !locale_files || locale_files.empty?) would make this defensive without changing the intended behavior.

Also applies to: 39-50, 67-68

lib/react_on_rails/dev/server_manager.rb (1)

214-238: Consider using safer command execution to prevent potential shell injection.

The hook command is executed using system(hook_value.to_s) (line 226), which passes the command as a string to the shell. While shakapacker.yml is typically developer-controlled, using the array form of system() is a safer practice that avoids shell interpretation of metacharacters.

Apply this improvement:

+  require "shellwords"
+  
   def run_precompile_hook_if_present
     hook_value = PackerUtils.shakapacker_precompile_hook_value
     return unless hook_value

     # Warn if Shakapacker version doesn't support SHAKAPACKER_SKIP_PRECOMPILE_HOOK
     warn_if_shakapacker_version_too_old

     puts Rainbow("🔧 Running Shakapacker precompile hook...").cyan
     puts Rainbow("   Command: #{hook_value}").cyan
     puts ""

-    unless system(hook_value.to_s)
+    unless system(*Shellwords.split(hook_value.to_s))
       puts ""
       puts Rainbow("❌ Precompile hook failed!").red.bold
       puts Rainbow("   Command: #{hook_value}").red

This ensures that the command is parsed into separate arguments, preventing shell metacharacter interpretation and making the execution more predictable.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2306825 and b7ee06b.

📒 Files selected for processing (10)
  • CHANGELOG.md (1 hunks)
  • docs/building-features/i18n.md (1 hunks)
  • docs/building-features/process-managers.md (1 hunks)
  • lib/react_on_rails/dev/server_manager.rb (5 hunks)
  • lib/react_on_rails/locales/base.rb (4 hunks)
  • lib/tasks/locale.rake (1 hunks)
  • sig/react_on_rails/locales.rbs (1 hunks)
  • spec/react_on_rails/dev/server_manager_spec.rb (1 hunks)
  • spec/react_on_rails/locales_spec.rb (1 hunks)
  • spec/react_on_rails/locales_to_js_spec.rb (1 hunks)
🧰 Additional context used
🧠 Learnings (12)
📓 Common learnings
Learnt from: AbanoubGhadban
Repo: shakacode/react_on_rails PR: 1875
File: lib/react_on_rails/utils.rb:112-124
Timestamp: 2025-10-23T17:22:01.074Z
Learning: In React on Rails, when Pro is installed but not licensed, the intended behavior is to raise an error on boot. The `react_on_rails_pro?` method validates licenses and should raise errors early (including during path resolution in methods like `server_bundle?`) to enforce licensing requirements rather than failing later with obscure errors.
📚 Learning: 2025-02-12T16:38:06.537Z
Learnt from: Romex91
Repo: shakacode/react_on_rails PR: 1697
File: package-scripts.yml:28-28
Timestamp: 2025-02-12T16:38:06.537Z
Learning: The file `node_package/lib/ReactOnRails.full.js` is autogenerated during the build process and should not be present in the repository.

Applied to files:

  • spec/react_on_rails/locales_to_js_spec.rb
  • sig/react_on_rails/locales.rbs
  • docs/building-features/i18n.md
  • lib/react_on_rails/locales/base.rb
📚 Learning: 2024-12-12T13:07:09.929Z
Learnt from: alexeyr-ci
Repo: shakacode/react_on_rails PR: 1644
File: node_package/src/ReactOnRailsRSC.ts:87-87
Timestamp: 2024-12-12T13:07:09.929Z
Learning: When handling errors in 'node_package/src/ReactOnRailsRSC.ts', include the error stack in error messages in development and test environments to aid debugging.

Applied to files:

  • spec/react_on_rails/dev/server_manager_spec.rb
📚 Learning: 2025-10-23T17:22:01.074Z
Learnt from: AbanoubGhadban
Repo: shakacode/react_on_rails PR: 1875
File: lib/react_on_rails/utils.rb:112-124
Timestamp: 2025-10-23T17:22:01.074Z
Learning: In React on Rails, when Pro is installed but not licensed, the intended behavior is to raise an error on boot. The `react_on_rails_pro?` method validates licenses and should raise errors early (including during path resolution in methods like `server_bundle?`) to enforce licensing requirements rather than failing later with obscure errors.

Applied to files:

  • spec/react_on_rails/dev/server_manager_spec.rb
  • lib/react_on_rails/dev/server_manager.rb
📚 Learning: 2024-10-08T20:53:47.076Z
Learnt from: justin808
Repo: shakacode/react_on_rails PR: 1622
File: spec/dummy/spec/rake/assets_precompile_rake_spec.rb:12-12
Timestamp: 2024-10-08T20:53:47.076Z
Learning: When stubbing environment variables in RSpec tests, use `before` and `after` hooks to ensure that the original values are restored after the tests, preventing any side effects on other tests. Example provided by justin808:
```ruby
describe "My test" do
  before do
    original_value = ENV["VARIABLE_NAME"]
    allow(ENV).to receive(:[]).with("VARIABLE_NAME").and_return("stubbed_value")
  end

  after do
    allow(ENV).to receive(:[]).with("VARIABLE_NAME").and_call_original
    ENV["VARIABLE_NAME"] = original_value
  end

  it "tests something" do
    # Your test code here
  end
end
```
This practice ensures test isolation and reliability.

Applied to files:

  • spec/react_on_rails/dev/server_manager_spec.rb
📚 Learning: 2025-09-29T14:45:42.687Z
Learnt from: AbanoubGhadban
Repo: shakacode/react_on_rails PR: 1833
File: lib/react_on_rails/dev/process_manager.rb:72-83
Timestamp: 2025-09-29T14:45:42.687Z
Learning: In Ruby bundler contexts, when bundler intercepts system commands for executables not in the Gemfile, both version checks (like `system("foreman", "--version")`) and execution commands (like `system("foreman", "start", ...)`) fail equally, both returning false. This means availability checks using version flags accurately reflect whether execution commands will work in the current bundler context.

Applied to files:

  • spec/react_on_rails/dev/server_manager_spec.rb
📚 Learning: 2025-02-13T14:29:49.267Z
Learnt from: AbanoubGhadban
Repo: shakacode/react_on_rails PR: 1644
File: spec/react_on_rails/utils_spec.rb:218-218
Timestamp: 2025-02-13T14:29:49.267Z
Learning: In RSpec tests, prefer using local variables over constants within test blocks to avoid constant redefinition warnings and maintain better test isolation.

Applied to files:

  • spec/react_on_rails/dev/server_manager_spec.rb
📚 Learning: 2025-09-15T21:24:48.207Z
Learnt from: AbanoubGhadban
Repo: shakacode/react_on_rails PR: 1781
File: node_package/src/ClientSideRenderer.ts:82-95
Timestamp: 2025-09-15T21:24:48.207Z
Learning: In React on Rails, the force_load feature includes both explicit `data-force-load="true"` usage and the ability to hydrate components during the page loading state (`document.readyState === 'loading'`). Both capabilities require a Pro license, so the condition `!railsContext.rorPro && (isComponentForceLoaded || document.readyState === 'loading')` correctly gates both scenarios.

Applied to files:

  • spec/react_on_rails/locales_spec.rb
  • sig/react_on_rails/locales.rbs
  • lib/react_on_rails/locales/base.rb
📚 Learning: 2025-04-26T21:55:55.874Z
Learnt from: alexeyr-ci2
Repo: shakacode/react_on_rails PR: 1732
File: spec/dummy/client/app-react16/startup/ReduxSharedStoreApp.client.jsx:40-44
Timestamp: 2025-04-26T21:55:55.874Z
Learning: In the react_on_rails project, files under `app-react16` directories are copied/moved to corresponding `/app` directories during the conversion process (removing the `-react16` suffix), which affects their relative import paths at runtime.

Applied to files:

  • CHANGELOG.md
  • sig/react_on_rails/locales.rbs
  • docs/building-features/i18n.md
  • lib/react_on_rails/locales/base.rb
📚 Learning: 2025-07-08T05:57:29.630Z
Learnt from: AbanoubGhadban
Repo: shakacode/react_on_rails PR: 1745
File: node_package/src/RSCRequestTracker.ts:8-14
Timestamp: 2025-07-08T05:57:29.630Z
Learning: The global `generateRSCPayload` function in React on Rails Pro (RORP) is provided by the framework during rendering requests, not implemented in application code. The `declare global` statements are used to document the expected interface that RORP will inject at runtime.

Applied to files:

  • sig/react_on_rails/locales.rbs
  • lib/react_on_rails/locales/base.rb
📚 Learning: 2025-09-16T08:01:11.146Z
Learnt from: justin808
Repo: shakacode/react_on_rails PR: 1770
File: lib/generators/react_on_rails/templates/base/base/app/javascript/src/HelloWorld/ror_components/HelloWorld.client.jsx:2-2
Timestamp: 2025-09-16T08:01:11.146Z
Learning: React on Rails uses webpack CSS Modules configuration with namedExports: true, which requires the import syntax `import * as style from './file.module.css'` rather than the default export pattern. This configuration enables better tree shaking and bundle size optimization for CSS modules.

Applied to files:

  • sig/react_on_rails/locales.rbs
  • docs/building-features/i18n.md
  • lib/react_on_rails/locales/base.rb
📚 Learning: 2025-04-09T12:56:10.756Z
Learnt from: AbanoubGhadban
Repo: shakacode/react_on_rails PR: 1696
File: node_package/src/RSCPayloadContainer.ts:0-0
Timestamp: 2025-04-09T12:56:10.756Z
Learning: In the react_on_rails codebase, RSC payloads are already stringified using `JSON.stringify()` before being processed by the `escapeScript` function, which handles escaping of special characters. The function only needs to handle specific HTML markers like comments and closing script tags.

Applied to files:

  • sig/react_on_rails/locales.rbs
  • lib/react_on_rails/locales/base.rb
🧬 Code graph analysis (4)
spec/react_on_rails/dev/server_manager_spec.rb (1)
lib/react_on_rails/dev/server_manager.rb (1)
  • run_from_command_line (151-209)
spec/react_on_rails/locales_spec.rb (2)
lib/react_on_rails/configuration.rb (1)
  • configuration (33-71)
lib/react_on_rails/locales/base.rb (1)
  • compile (7-22)
lib/react_on_rails/dev/server_manager.rb (1)
lib/react_on_rails/packer_utils.rb (3)
  • shakapacker_precompile_hook_value (235-241)
  • shakapacker_version_requirement_met? (30-33)
  • shakapacker_version (15-19)
lib/react_on_rails/locales/base.rb (1)
lib/react_on_rails/configuration.rb (1)
  • initialize (134-190)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: examples (3.2, minimum)
  • GitHub Check: build-dummy-app-webpack-test-bundles
  • GitHub Check: rspec-package-tests (3.4, latest)
  • GitHub Check: pro-lint-js-and-ruby
  • GitHub Check: claude-review
🔇 Additional comments (9)
docs/building-features/i18n.md (1)

24-46: Docs: precompile_hook + force flag guidance is clear and consistent

The new recommended Shakapacker precompile_hook section and the force=true example line up with the rake task behavior and make the idempotent-flow story easy to follow. No changes needed here.

docs/building-features/process-managers.md (1)

19-58: Docs: bin/dev + precompile_hook integration matches implementation

The updated bin/dev flow, precompile hook integration note, SHAKAPACKER_SKIP_PRECOMPILE_HOOK version caveat, and Procfile/shakapacker.yml examples are all consistent with the tested server_manager behavior. Looks good.

lib/tasks/locale.rake (1)

13-20: Rake task: force flag wiring and docs look correct

The Options block clearly documents force=true, and the %w[true 1 yes].include?(ENV["force"]&.downcase) parsing plus Locales.compile(force: force) call cleanly hook into the new API. No changes needed.

CHANGELOG.md (1)

26-31: Changelog: entries accurately describe new behavior

The Unreleased “Improved” items succinctly capture the new bin/dev precompile_hook coordination and idempotent react_on_rails:locale behavior (including force=true). They match the implementation and docs.

spec/react_on_rails/locales_spec.rb (1)

40-54: Specs: correctly verify force propagation to writer classes

The added examples neatly confirm that Locales.compile(force: true) forwards force: true to the appropriate writer (ToJson vs ToJs) based on i18n_output_format, keeping configuration state restored via the existing before/after hooks. Looks solid.

lib/react_on_rails/dev/server_manager.rb (3)

11-12: LGTM!

The HELP_FLAGS constant is well-defined and appropriately frozen.


240-257: LGTM!

The version checking logic correctly identifies Shakapacker versions in the 9.0.0–9.3.x range that lack SHAKAPACKER_SKIP_PRECOMPILE_HOOK support, providing clear guidance to upgrade.


184-189: Effective coordination mechanism for preventing duplicate hook execution.

Setting SHAKAPACKER_SKIP_PRECOMPILE_HOOK after running the hook ensures spawned processes (via Procfile) will skip their hooks, preventing race conditions and duplicate work.

sig/react_on_rails/locales.rbs (1)

1-46: Well-structured type signatures for the locale compilation API.

The RBS signatures accurately reflect the idempotent locale generation feature:

  • compile(?force: bool) correctly accepts an optional force parameter
  • Base#initialize(?force: bool) properly propagates the force flag
  • Subclasses ToJs and ToJson appropriately override specific methods
  • Type signatures for complex hash operations (lines 26-27) accurately represent the nested translation structures

The nullable return type for Base#file_format (line 11) versus non-nullable in subclasses (lines 35, 41) is a valid type refinement pattern, indicating that Base may be abstract or have a default implementation.

@claude
Copy link

claude bot commented Nov 22, 2025

Code Review: Idempotent Locale Generation with Precompile Hook Integration

This is an excellent enhancement that addresses a real pain point in development workflows. The implementation is well-structured with good test coverage. Here's my detailed feedback:


Strengths

1. Solid Core Implementation

  • The idempotent check using file modification times (files_are_outdated) is a clean approach
  • The force parameter is properly threaded through the call chain (compile → ToJson/ToJs → Base)
  • Good user messaging when skipping regeneration vs. generating files

2. Comprehensive Test Coverage

  • Tests verify the force parameter propagation
  • Tests confirm idempotent behavior (skips when up-to-date)
  • Tests verify force flag bypasses the timestamp check
  • Extensive tests for bin/dev precompile hook integration

3. Excellent Documentation

  • Clear usage examples in the PR description
  • Updated i18n docs with recommended precompile_hook configuration
  • Process manager docs updated with detailed explanation of the integration

4. Type Safety

  • Added RBS signatures for the Locales module (commit 2)
  • This improves static type checking coverage

5. Smart Integration with bin/dev

  • Automatic precompile hook execution eliminates manual coordination
  • SHAKAPACKER_SKIP_PRECOMPILE_HOOK environment variable prevents duplicate execution
  • Helpful version warning for Shakapacker < 9.4.0

🔍 Issues & Concerns

Critical: Potential Nil Comparison Bug in files_are_outdated

Location: lib/react_on_rails/locales/base.rb:62-64

def files_are_outdated
  latest_yml = locale_files.map { |file| File.mtime(file) }.max
  earliest = exist_files.map { |file| File.mtime(file) }.min
  latest_yml > earliest
end

Problem: If locale_files is empty, latest_yml will be nil, causing a NoMethodError when comparing nil > earliest.

Fix Applied: Commit 2 adds:

return true if locale_files.empty?

But wait... This guard is placed in files_are_outdated, but there's a second issue:

Looking at the original code in the current master branch (before this PR), files_are_outdated is only called from obsolete?:

def obsolete?
  return true if exist_files.empty?
  files_are_outdated
end

Additional Issue: The new code in the PR changes the behavior in Base#initialize:

BEFORE (current master):

def initialize
  return if i18n_dir.nil?
  return unless obsolete?
  # ...
end

AFTER (PR commit 1):

def initialize(force: false)
  return if i18n_dir.nil?

  if !force && !obsolete?
    puts "Locale files are up to date, skipping generation. Use force=true to regenerate."
    return
  end

  @translations, @defaults = generate_translations
  convert
  puts "Generated locale files in #{i18n_dir}"
end

This is fine, BUT there's a subtle issue: The message "Locale files are up to date..." will print even when files don't exist yet. Let me trace through the logic:

  1. First run (no files exist):

    • exist_files.empty? → true
    • obsolete? returns true (early return in obsolete?)
    • !obsolete? → false
    • Condition !force && !obsolete? → false
    • Correctly generates files
  2. Second run (files exist and up-to-date):

    • exist_files.empty? → false
    • files_are_outdated → false (assuming files are newer)
    • obsolete? returns false
    • !obsolete? → true
    • Condition !force && !obsolete? → true
    • Correctly skips generation

Actually, the logic looks correct on closer inspection. Good!

Minor: ENV Variable Parsing Enhancement

Location: lib/tasks/locale.rake:18

BEFORE (commit 1):

force = ENV["force"] == "true"

AFTER (commit 2):

force = %w[true 1 yes].include?(ENV["force"]&.downcase)

Good improvement! This makes the interface more user-friendly by accepting common boolean representations. Consider documenting this in the task description.

Minor: Unnecessary sleep removal

Location: spec/react_on_rails/locales_to_js_spec.rb:56

Commit 2 removes sleep 0.01 from the test. The comment justifies this saying "timestamp difference already guaranteed", but looking at the test:

initial_mtime = File.mtime(translations_path)
# sleep 0.01 was here
future_time = Time.current + 1.minute
FileUtils.touch(translations_path, mtime: future_time)

The sleep was unnecessary since future_time = Time.current + 1.minute already creates a future timestamp. Good catch!


🚨 Potential Issues

1. Race Condition in bin/dev Hook Execution?

Location: lib/react_on_rails/dev/server_manager.rb:149-156

The code extracts the command early to check for help/kill before running hooks:

command = args.find { |arg| !arg.start_with?("--") && !arg.start_with?("-") }
help_requested = args.any? { |arg| HELP_FLAGS.include?(arg) }

Then later:

unless %w[kill help].include?(command) || help_requested
  run_precompile_hook_if_present
  ENV["SHAKAPACKER_SKIP_PRECOMPILE_HOOK"] = "true"
end

Question: What happens if a user runs bin/dev -h?

  1. command = args.find { |arg| !arg.start_with?("--") && !arg.start_with?("-") } → nil
  2. help_requested = args.any? { |arg| HELP_FLAGS.include?(arg) } → true
  3. Hook is skipped ✅
  4. OptionParser processes -h and calls show_help; exit

Looks good!

Question: What happens if a user runs bin/dev --help?

Same flow - help_requested will be true, hook is skipped. ✅

Question: What happens if a user runs just bin/dev help?

  1. command → "help"
  2. Hook is skipped via %w[kill help].include?(command)

Great defensive programming!

2. RBS Type Signatures - Minor Nitpick

Location: sig/react_on_rails/locales.rbs

The RBS signatures look comprehensive, but I notice:

def files_are_outdated: () -> bool

Given the guard added in commit 2 (return true if locale_files.empty?), this is correct. However, the signature for locale_files is:

def locale_files: () -> Array[String]

Question: Can locale_files actually return nil? Looking at the implementation:

def locale_files
  @locale_files ||= if i18n_yml_dir.present?
                      Dir["#{i18n_yml_dir}/**/*.yml"]
                    else
                      ReactOnRails::Utils.truthy_presence(
                        Rails.application && Rails.application.config.i18n.load_path
                      ).presence
                    end
end

The .presence call at the end could return nil. So the signature should probably be:

def locale_files: () -> Array[String]?

But this is a minor issue - the actual code handles it with the locale_files.empty? check in commit 2.


💡 Suggestions for Improvement

1. Consider Logging Instead of puts

Location: lib/react_on_rails/locales/base.rb:43-48

if !force && !obsolete?
  puts "Locale files are up to date, skipping generation. Use force=true to regenerate."
  return
end

@translations, @defaults = generate_translations
convert
puts "Generated locale files in #{i18n_dir}"

Suggestion: Consider using Rails logger instead of puts:

  • puts writes to stdout unconditionally
  • In some contexts (background jobs, test suites), this might create unwanted noise
  • Rails logger allows controlling verbosity via log levels

However, for a Rake task, puts is reasonable. Just something to consider.

2. Document ENV Variable Options in Rake Task Description

Location: lib/tasks/locale.rake:11-15

Options:
  force=true - Force regeneration even if files are up to date
               Example: rake react_on_rails:locale force=true

Suggestion: Document all accepted values:

Options:
  force=true|1|yes - Force regeneration even if files are up to date
                     Example: rake react_on_rails:locale force=true

3. RBS Validation Command in CI

The PR adds RBS signatures but I don't see them being validated in CI. The CLAUDE.md mentions:

Validate RBS signatures: bundle exec rake rbs:validate

Recommendation: Ensure this runs in CI to catch type signature drift.


🧪 Testing

Excellent test coverage overall! The tests verify:

✅ Force parameter propagation through the call chain
✅ Idempotent behavior (skipping when up-to-date)
✅ Force flag bypassing timestamp checks
✅ bin/dev hook execution
✅ Environment variable setting
✅ Version warnings for old Shakapacker
✅ Help/kill command handling

One suggestion: Consider adding a test for the edge case where locale_files returns an empty array or nil. The guard in commit 2 (return true if locale_files.empty?) should be tested explicitly.


📝 Documentation

Excellent documentation! The PR includes:

✅ Comprehensive PR description with usage examples
✅ Updated i18n.md with recommended precompile_hook pattern
✅ Updated process-managers.md with detailed integration explanation
✅ Proper CHANGELOG entries
✅ Clear commit messages

Minor suggestion: The CHANGELOG entry is quite long (3 lines). Consider condensing slightly while keeping the key information.


🏁 Overall Assessment

This is a high-quality PR that solves a real problem with a thoughtful implementation. The three-commit structure shows good iterative refinement:

  1. Commit 1: Core idempotent locale generation
  2. Commit 2: Refinements (nil guards, ENV parsing, RBS signatures)
  3. Commit 3: bin/dev integration for seamless developer experience

Recommendation: APPROVE with minor suggestions

The code is well-tested, well-documented, and follows the project's conventions. The minor issues I raised are mostly nitpicks and don't block merging.

Great work! 🎉

@claude
Copy link

claude bot commented Nov 22, 2025

Code Review - PR #2092: Make locale generation idempotent with force flag support

I've reviewed this PR and it looks excellent overall! The implementation is clean, well-tested, and addresses a real pain point with development workflows. Here's my detailed feedback:


Strengths

1. Clean Architecture

  • The force parameter flows cleanly through the call chain: rake task → Locales.compileBase#initializeToJs/ToJson
  • Proper separation of concerns between timestamp checking and forced regeneration
  • User-facing messages are clear and helpful

2. Excellent Test Coverage

  • Tests verify the force parameter is properly passed through to both ToJson and ToJs
  • Tests verify that force flag bypasses the up-to-date check
  • Tests verify the idempotent behavior (skips when up-to-date)
  • Integration with ServerManager has comprehensive tests covering all modes and edge cases

3. Documentation

  • Great documentation updates in i18n.md with recommended Shakapacker configuration
  • Clear examples in the rake task description
  • Well-documented changelog entries

4. Type Safety

  • RBS type signatures added for the new module (good practice!)
  • Proper type definitions for all methods including the new force parameter

🔍 Issues & Recommendations

Critical: Missing Feature in Base Implementation

Issue: The PR diff shows changes to lib/react_on_rails/locales/base.rb, but when I read the current master version, it doesn't have the force parameter implementation. Looking at the diff, the PR adds:

def initialize(force: false)
  return if i18n_dir.nil?

  if !force && !obsolete?
    puts "Locale files are up to date, skipping generation. Use force=true to regenerate."
    return
  end

  @translations, @defaults = generate_translations
  convert
  puts "Generated locale files in #{i18n_dir}"
end

However, I notice a potential issue:

Problem: The logic if !force && !obsolete? means:

  • When force=false AND files are up-to-date → Skip (✅ correct)
  • When force=false AND files are outdated → Generate (✅ correct)
  • When force=true AND files are up-to-date → Generate (✅ correct)
  • When force=true AND files are outdated → Generate (✅ correct)

Actually, the logic looks correct! The condition properly handles all cases.


Minor: User Message Could Be More Informative

Current:

puts "Locale files are up to date, skipping generation. Use force=true to regenerate."

Suggestion:

puts "✓ Locale files are up to date (skipping generation). Use force=true to regenerate."

Rationale: The checkmark emoji makes it clearer this is a success state, not an error. The parentheses clarify what's being skipped.


Minor: Consider Adding File Path Context to Messages

Current:

puts "Generated locale files in #{i18n_dir}"

Suggestion:

puts "✓ Generated locale files: #{files.join(', ')}"

Rationale: Users might want to know which specific files were generated, especially when troubleshooting.


Code Quality: Potential Edge Case with Empty Locale Files

In files_are_outdated:

def files_are_outdated
  return true if locale_files.empty?

  latest_yml = locale_files.map { |file| File.mtime(file) }.max
  earliest = exist_files.map { |file| File.mtime(file) }.min
  latest_yml > earliest
end

Observation: The PR adds return true if locale_files.empty? to the files_are_outdated method. This is good defensive coding!

Question: What happens if exist_files.empty?? The method will call .min on an empty array, which returns nil, then compare latest_yml > nil, which will raise a NoMethodError.

However, looking at the caller in obsolete?:

def obsolete?
  return true if exist_files.empty?
  files_are_outdated
end

Good! The code already handles this by returning early if exist_files.empty?. So this edge case is already covered. ✅


ServerManager Integration: Excellent Work!

The integration with bin/dev is really well done:

  1. Precompile hook runs before starting processes - prevents race conditions
  2. Sets SHAKAPACKER_SKIP_PRECOMPILE_HOOK=true - prevents duplicate execution
  3. Version checking - warns users on Shakapacker < 9.4.0
  4. Help flags handled correctly - doesn't run hooks for help/kill commands
  5. Proper error handling - exits with clear messages if hook fails

One minor suggestion: The logic for detecting help flags is slightly complex:

# Get the command early to check for help/kill before running hooks
command = args.find { |arg| !arg.start_with?("--") && !arg.start_with?("-") }

# Check if help flags are present in args
help_requested = args.any? { |arg| HELP_FLAGS.include?(arg) }

unless %w[kill help].include?(command) || help_requested
  run_precompile_hook_if_present
  ENV["SHAKAPACKER_SKIP_PRECOMPILE_HOOK"] = "true"
end

This works, but there's a subtle issue: if someone runs bin/dev -h static, the code will:

  1. Set command = "static" (first non-flag arg)
  2. Set help_requested = true (found -h)
  3. Skip precompile hook ✅

Actually, this is correct! The logic properly handles all edge cases. Great job! ✅


🔒 Security Review

Rake Task Parameter Parsing

force = %w[true 1 yes].include?(ENV["force"]&.downcase)

✅ Good: Safe parsing of the force parameter - no shell injection risk
✅ Good: Uses allowlist approach (only specific values are truthy)


🎯 Performance Considerations

  1. Timestamp checking is efficient - uses File.mtime which is fast
  2. Skips file generation entirely when up-to-date - excellent optimization
  3. No unnecessary file reads - only reads YAML when regeneration is needed

No performance concerns.


📋 Test Coverage Assessment

What's Tested:

  • ✅ Force parameter propagation to ToJson
  • ✅ Force parameter propagation to ToJs
  • ✅ Force flag bypasses up-to-date check
  • ✅ Precompile hook integration in all modes (dev, static, prod)
  • ✅ Hook skipped for kill/help commands
  • ✅ Version warning for Shakapacker < 9.4.0
  • ✅ Hook failure causes exit

Potential Gaps:

  • Missing: Test that verifies the skip message is shown when files are up-to-date
  • Missing: Test that verifies the generation message is shown after generation

Suggestion: Add a simple test in locales_to_js_spec.rb:

it "shows skip message when files are up-to-date" do
  # First generation
  described_class.new

  # Second call should skip and show message
  expect { described_class.new }.to output(/up to date.*skipping/i).to_stdout
end

it "shows generation message after generating" do
  expect { described_class.new }.to output(/Generated locale files/i).to_stdout
end

🎨 Style & Conventions

Follows Project Guidelines:

  • ✅ RuboCop-compliant (based on the code structure)
  • ✅ Proper use of keyword arguments (force: false)
  • ✅ Clear method names
  • ✅ Good comments where needed
  • ✅ Changelog updated appropriately

RBS Type Signatures:

The new sig/react_on_rails/locales.rbs file looks comprehensive and correct:

  • ✅ All public methods have signatures
  • ✅ Private methods included
  • ✅ Proper use of optional parameters (?force: bool)
  • ✅ Return types specified

📖 Documentation Review

i18n.md Updates:

  • ✅ Clear explanation of idempotent behavior
  • ✅ Recommended Shakapacker configuration shown
  • ✅ Force flag usage documented
  • ✅ Alternatives provided for manual coordination

process-managers.md Updates:

  • ✅ Excellent explanation of precompile hook integration
  • ✅ Before/after examples are very helpful
  • ✅ Version compatibility warnings included

🎯 Summary & Recommendation

Overall Assessment: This is a high-quality PR that solves a real problem with a clean, well-tested solution.

Required Changes:

None - the code is production-ready as-is.

Nice-to-Have Improvements:

  1. Add tests for stdout messages (skip message, generation message)
  2. Consider adding emoji to success messages for consistency with other parts of the codebase
  3. Consider showing which specific files were generated in the success message

Verdict:APPROVE

This PR is ready to merge. The implementation is solid, well-tested, and follows all project conventions. The nice-to-have improvements can be addressed in a follow-up PR if desired.


🙏 Great Work!

Special kudos for:

  • Comprehensive test coverage across multiple test files
  • Excellent documentation updates with real-world examples
  • Proper RBS type signatures
  • Clean integration with the existing bin/dev workflow
  • Version compatibility handling

This will make the development workflow much smoother for React on Rails users! 🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Coordinate assets:precompile with Shakapacker Precompile Hooks

2 participants