Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,15 @@ Changes since the last non-beta release.
#### Improved

- **Automatic Precompile Hook Coordination in bin/dev**: 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.

- Eliminates the need for manual coordination, sleep hacks, and duplicate task calls in Procfile.dev
- Users can configure expensive build tasks (like locale generation or ReScript compilation) once in `config/shakapacker.yml` and `bin/dev` handles coordination automatically
- Includes warning for Shakapacker versions below 9.4.0 (the `SHAKAPACKER_SKIP_PRECOMPILE_HOOK` environment variable is only supported in 9.4.0+)
- The `SHAKAPACKER_SKIP_PRECOMPILE_HOOK` environment variable is set for all spawned processes, making it available for custom scripts that need to detect when `bin/dev` is managing the precompile hook
- Addresses [2091](https://github.com/shakacode/react_on_rails/issues/2091) by [justin808](https://github.com/justin808)

- **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 2090](https://github.com/shakacode/react_on_rails/pull/2090) by [justin808](https://github.com/justin808).

### [v16.2.0.beta.12] - 2025-11-20

#### Added
Expand Down
17 changes: 14 additions & 3 deletions docs/building-features/i18n.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,26 @@ You can use [Rails internationalization (i18n)](https://guides.rubyonrails.org/i

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

The locale generation task is idempotent - it will skip generation if files are already up-to-date. This makes it safe to call multiple times without duplicate work:

```bash
bundle exec rake react_on_rails:locale
# Subsequent calls will skip if already up-to-date
```

To force regeneration:

```bash
bundle exec rake react_on_rails:locale force=true
```

**Recommended: Use Shakapacker's precompile_hook with bin/dev** (React on Rails 16.2+, Shakapacker 9.3+)

The locale generation task is idempotent and can be safely called multiple times. Configure it in Shakapacker's `precompile_hook` and `bin/dev` will handle coordination automatically:
Configure the idempotent task in `config/shakapacker.yml` to run automatically before webpack:

```yaml
# config/shakapacker.yml
default: &default
# Run locale generation before webpack compilation
# Safe to run multiple times - will skip if already built
precompile_hook: 'bundle exec rake react_on_rails:locale'
```

Expand Down
22 changes: 17 additions & 5 deletions lib/react_on_rails/locales/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

module ReactOnRails
module Locales
def self.compile
def self.compile(force: false)
config = ReactOnRails.configuration
check_config_directory_exists(
directory: config.i18n_dir, key_name: "config.i18n_dir",
Expand All @@ -15,9 +15,9 @@ def self.compile
remove_if: "not using this i18n with React on Rails, or if you want to use all translation files"
)
if config.i18n_output_format&.downcase == "js"
ReactOnRails::Locales::ToJs.new
ReactOnRails::Locales::ToJs.new(force: force)
else
ReactOnRails::Locales::ToJson.new
ReactOnRails::Locales::ToJson.new(force: force)
end
end

Expand All @@ -36,19 +36,31 @@ def self.check_config_directory_exists(directory:, key_name:, remove_if:)
private_class_method :check_config_directory_exists

class Base
def initialize
def initialize(force: false)
return if i18n_dir.nil?
return unless obsolete?

if locale_files.empty?
puts "Warning: No locale files found in #{i18n_yml_dir || 'Rails i18n load path'}"
return
end

if !force && !obsolete?
puts "Locale files are up to date, skipping generation. " \
"Use 'rake react_on_rails:locale force=true' to force regeneration."
return
end

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

private

def file_format; end

def obsolete?
return true if exist_files.length != files.length # Some files missing
return true if exist_files.empty?

files_are_outdated
Expand Down
7 changes: 6 additions & 1 deletion lib/tasks/locale.rake
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,13 @@ namespace :react_on_rails do
Generate i18n javascript files
This task generates javascript locale files: `translations.js` & `default.js` and places them in
the "ReactOnRails.configuration.i18n_dir".

Options:
force=true - Force regeneration even if files are up to date
Example: rake react_on_rails:locale force=true
DESC
task locale: :environment do
ReactOnRails::Locales.compile
force = %w[true 1 yes].include?(ENV["force"]&.downcase)
ReactOnRails::Locales.compile(force: force)
end
end
46 changes: 46 additions & 0 deletions sig/react_on_rails/locales.rbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
module ReactOnRails
module Locales
def self.compile: (?force: bool) -> (ToJs | ToJson)
def self.check_config_directory_exists: (directory: String?, key_name: String, remove_if: String) -> void

class Base
def initialize: (?force: bool) -> void

private

def file_format: () -> String?
def obsolete?: () -> bool
def exist_files: () -> Array[String]
def files_are_outdated: () -> bool
def file_names: () -> Array[String]
def files: () -> Array[String]
def file: (String name) -> String
def locale_files: () -> Array[String]
def i18n_dir: () -> String?
def i18n_yml_dir: () -> String?
def default_locale: () -> String
def convert: () -> void
def generate_file: (String template, String path) -> void
def generate_translations: () -> [String, String]
def format: (untyped input) -> Symbol
def flatten_defaults: (Hash[untyped, untyped] val) -> Hash[Symbol, Hash[Symbol, untyped]]
def flatten: (Hash[untyped, untyped] translations) -> Hash[Symbol, untyped]
def template_translations: () -> String
def template_default: () -> String
end

class ToJs < Base
private

def file_format: () -> String
end

class ToJson < Base
private

def file_format: () -> String
def template_translations: () -> String
def template_default: () -> String
end
end
end
16 changes: 16 additions & 0 deletions spec/react_on_rails/locales_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,22 @@ module ReactOnRails

described_class.compile
end

it "passes force parameter to ToJson" do
ReactOnRails.configuration.i18n_output_format = nil

expect(ReactOnRails::Locales::ToJson).to receive(:new).with(force: true)

described_class.compile(force: true)
end

it "passes force parameter to ToJs" do
ReactOnRails.configuration.i18n_output_format = "js"

expect(ReactOnRails::Locales::ToJs).to receive(:new).with(force: true)

described_class.compile(force: true)
end
end
end
end
20 changes: 20 additions & 0 deletions spec/react_on_rails/locales_to_js_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,26 @@ module ReactOnRails
described_class.new
expect(File.mtime(translations_path)).to eq(ref_time)
end

it "updates files when force is true" do
# Get initial mtime after first generation
initial_mtime = File.mtime(translations_path)

# Sleep to ensure different timestamp on fast filesystems
sleep 0.01

# Touch files to make them newer than YAML (up-to-date)
future_time = Time.current + 1.minute
FileUtils.touch(translations_path, mtime: future_time)
FileUtils.touch(default_path, mtime: future_time)

# Force regeneration even though files are up-to-date
described_class.new(force: true)

# New mtime should be different from the future_time we set
expect(File.mtime(translations_path)).not_to eq(future_time)
expect(File.mtime(translations_path)).to be > initial_mtime
end
end
end

Expand Down
Loading