Skip to content

Commit b8e3ba0

Browse files
justin808claude
andcommitted
Make locale generation idempotent with force flag support (#2090)
## 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]>
1 parent 2306825 commit b8e3ba0

File tree

6 files changed

+76
-9
lines changed

6 files changed

+76
-9
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ After a release, please make sure to run `bundle exec rake update_changelog`. Th
2323

2424
Changes since the last non-beta release.
2525

26+
#### Improved
27+
28+
- **Idempotent Locale Generation**: The `react_on_rails:locale` rake task is now idempotent, automatically skipping generation when locale files are already up-to-date. This makes it safe to call multiple times (e.g., in Shakapacker's `precompile_hook`) without duplicate work. Added `force=true` option to override timestamp checking. [PR 2091](https://github.com/shakacode/react_on_rails/pull/2091) by [justin808](https://github.com/justin808).
29+
2630
### [v16.2.0.beta.12] - 2025-11-20
2731

2832
#### Added

docs/building-features/i18n.md

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,29 @@ You can use [Rails internationalization (i18n)](https://guides.rubyonrails.org/i
2121

2222
3. The locale files must be generated before `yarn build` using `rake react_on_rails:locale`.
2323

24-
For development, you should adjust your startup scripts (`Procfile`s) so that they run `bundle exec rake react_on_rails:locale` before running any Webpack watch process (`yarn run build:development`).
24+
**Recommended: Use Shakapacker's precompile_hook** (Shakapacker 9.3+)
2525

26-
If you are not using the React on Rails test helper,
27-
you may need to configure your CI to run `bundle exec rake react_on_rails:locale` before any Webpack process as well.
26+
The idempotent locale generation task can be safely called multiple times and will skip generation if files are already up-to-date. This makes it ideal for use with Shakapacker's `precompile_hook` feature:
27+
28+
```yaml
29+
# config/shakapacker.yml
30+
default: &default
31+
# Run locale generation before webpack compilation
32+
# Safe to run multiple times - will skip if already built
33+
precompile_hook: 'bundle exec rake react_on_rails:locale'
34+
```
35+
36+
This ensures locale files are always generated before webpack runs, whether in development, production, or CI. To force regeneration:
37+
38+
```bash
39+
bundle exec rake react_on_rails:locale force=true
40+
```
41+
42+
**Alternative: Manual coordination**
43+
44+
For development, you can adjust your startup scripts (`Procfile`s) to run `bundle exec rake react_on_rails:locale` before running any Webpack watch process (`yarn run build:development`).
45+
46+
If you are not using the React on Rails test helper, you may need to configure your CI to run `bundle exec rake react_on_rails:locale` before any Webpack process as well.
2847

2948
> [!NOTE]
3049
> If you try to lint before running tests, and you depend on the test helper to build your locales, linting will fail because the translations won't be built yet.

lib/react_on_rails/locales/base.rb

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
module ReactOnRails
66
module Locales
7-
def self.compile
7+
def self.compile(force: false)
88
config = ReactOnRails.configuration
99
check_config_directory_exists(
1010
directory: config.i18n_dir, key_name: "config.i18n_dir",
@@ -15,9 +15,9 @@ def self.compile
1515
remove_if: "not using this i18n with React on Rails, or if you want to use all translation files"
1616
)
1717
if config.i18n_output_format&.downcase == "js"
18-
ReactOnRails::Locales::ToJs.new
18+
ReactOnRails::Locales::ToJs.new(force: force)
1919
else
20-
ReactOnRails::Locales::ToJson.new
20+
ReactOnRails::Locales::ToJson.new(force: force)
2121
end
2222
end
2323

@@ -36,12 +36,17 @@ def self.check_config_directory_exists(directory:, key_name:, remove_if:)
3636
private_class_method :check_config_directory_exists
3737

3838
class Base
39-
def initialize
39+
def initialize(force: false)
4040
return if i18n_dir.nil?
41-
return unless obsolete?
41+
42+
if !force && !obsolete?
43+
puts "Locale files are up to date, skipping generation. Use force=true to regenerate."
44+
return
45+
end
4246

4347
@translations, @defaults = generate_translations
4448
convert
49+
puts "Generated locale files in #{i18n_dir}"
4550
end
4651

4752
private

lib/tasks/locale.rake

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,13 @@ namespace :react_on_rails do
99
Generate i18n javascript files
1010
This task generates javascript locale files: `translations.js` & `default.js` and places them in
1111
the "ReactOnRails.configuration.i18n_dir".
12+
13+
Options:
14+
force=true - Force regeneration even if files are up to date
15+
Example: rake react_on_rails:locale force=true
1216
DESC
1317
task locale: :environment do
14-
ReactOnRails::Locales.compile
18+
force = ENV["force"] == "true"
19+
ReactOnRails::Locales.compile(force: force)
1520
end
1621
end

spec/react_on_rails/locales_spec.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,22 @@ module ReactOnRails
3636

3737
described_class.compile
3838
end
39+
40+
it "passes force parameter to ToJson" do
41+
ReactOnRails.configuration.i18n_output_format = nil
42+
43+
expect(ReactOnRails::Locales::ToJson).to receive(:new).with(force: true)
44+
45+
described_class.compile(force: true)
46+
end
47+
48+
it "passes force parameter to ToJs" do
49+
ReactOnRails.configuration.i18n_output_format = "js"
50+
51+
expect(ReactOnRails::Locales::ToJs).to receive(:new).with(force: true)
52+
53+
described_class.compile(force: true)
54+
end
3955
end
4056
end
4157
end

spec/react_on_rails/locales_to_js_spec.rb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,24 @@ module ReactOnRails
5050
described_class.new
5151
expect(File.mtime(translations_path)).to eq(ref_time)
5252
end
53+
54+
it "updates files when force is true" do
55+
# Get initial mtime after first generation
56+
initial_mtime = File.mtime(translations_path)
57+
58+
# Touch files to make them newer than YAML (up-to-date)
59+
sleep 0.01 # Ensure timestamp difference
60+
future_time = Time.current + 1.minute
61+
FileUtils.touch(translations_path, mtime: future_time)
62+
FileUtils.touch(default_path, mtime: future_time)
63+
64+
# Force regeneration even though files are up-to-date
65+
described_class.new(force: true)
66+
67+
# New mtime should be different from the future_time we set
68+
expect(File.mtime(translations_path)).not_to eq(future_time)
69+
expect(File.mtime(translations_path)).to be > initial_mtime
70+
end
5371
end
5472
end
5573

0 commit comments

Comments
 (0)