Skip to content

Commit 0090f5e

Browse files
JasonYCHuangjustin808
authored andcommitted
Automatically generate i18n javascript files for react-intl when the serve starts up (#642)
1 parent e5216c7 commit 0090f5e

File tree

11 files changed

+297
-9
lines changed

11 files changed

+297
-9
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ Contributors: please follow the recommendations outlined at [keepachangelog.com]
88
### Fixed
99
- Removed foreman as a dependency. [#678](https://github.com/shakacode/react_on_rails/pull/678) by [x2es](https://github.com/x2es).
1010

11+
### Added
12+
- Automatically generate __i18n__ javascript files for `react-intl` when the serve starts up. [#642](https://github.com/shakacode/react_on_rails/pull/642) by [JasonYCHuang](https://github.com/JasonYCHuang).
13+
1114
## [6.3.5] - 2017-1-6
1215
### Fixed
1316
- The redux generator now creates a HelloWorld component that uses redux rather than local state. [#669](https://github.com/shakacode/react_on_rails/issues/669) by [justin808](https://github.com/justin808).

README.md

+10-9
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ React on Rails integrates Facebook's [React](https://github.com/facebook/react)
5050
- [Installation Summary](#installation-summary)
5151
- [Initializer Configuration: config/initializers/react_on_rails.rb](#initializer-configuration)
5252
- [Including your React Component in your Rails Views](#including-your-react-component-in-your-rails-views)
53+
- [I18n](#i18n)
5354
+ [How it Works](#how-it-works)
5455
- [Client-Side Rendering vs. Server-Side Rendering](#client-side-rendering-vs-server-side-rendering)
5556
- [Building the Bundles](#building-the-bundles)
@@ -170,7 +171,15 @@ Configure the `config/initializers/react_on_rails.rb`. You can adjust some neces
170171
// inside your React component
171172
this.props.name // "Stranger"
172173
```
173-
174+
175+
### I18n
176+
177+
You can enable the i18n functionality with [react-intl](https://github.com/yahoo/react-intl).
178+
179+
React on Rails provides an option for automatic conversions of Rails `*.yml` locale files into `*.js` files for `react-intl`.
180+
181+
See the [How to add I18n](docs/basics/i18n.md) for a summary of adding I18n.
182+
174183
## NPM
175184
All JavaScript in React On Rails is loaded from npm: [react-on-rails](https://www.npmjs.com/package/react-on-rails). To manually install this (you did not use the generator), assuming you have a standard configuration, run this command:
176185
@@ -245,11 +254,6 @@ The `railsContext` has: (see implementation in file [react_on_rails_helper.rb](a
245254
pathname: uri.path, # /posts
246255
search: uri.query, # id=30&limit=5
247256

248-
# Locale settings
249-
i18nLocale: I18n.locale,
250-
i18nDefaultLocale: I18n.default_locale,
251-
httpAcceptLanguage: request.env["HTTP_ACCEPT_LANGUAGE"],
252-
253257
# Other
254258
serverSide: boolean # Are we being called on the server or client? NOTE, if you conditionally
255259
# render something different on the server than the client, then React will only show the
@@ -261,9 +265,6 @@ The `railsContext` has: (see implementation in file [react_on_rails_helper.rb](a
261265
##### Needing the current url path for server rendering
262266
Suppose you want to display a nav bar with the current navigation link highlighted by the URL. When you server render the code, you will need to know the current URL/path if that is what you want your logic to be based on. The new `railsContext` has this information so the application of an "active" class can be done server side.
263267

264-
##### Needing the I18n.locale
265-
Suppose you want to server render your react components with localization applied given the current Rails locale. The `railsContext` contains the I18n.locale.
266-
267268
##### Configuring different code for server side rendering
268269
Suppose you want to turn off animation when doing server side rendering. The `serverSide` value is just what you need.
269270

docs/basics/i18n.md

+90
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# How to add I18n
2+
3+
Here's a summary of adding the I18n functionality.
4+
5+
You can refer to [react-webpack-rails-tutorial](https://github.com/shakacode/react-webpack-rails-tutorial) for a complete example.
6+
7+
1. Add `react-intl` & `intl` to `client/package.json`, and remember to `bundle && npm install`.
8+
9+
```js
10+
"dependencies": {
11+
...
12+
"intl": "^1.2.5",
13+
"react-intl": "^2.1.5",
14+
...
15+
}
16+
```
17+
18+
2. In `client/webpack.client.base.config.js`, set `react-intl` as an entry point.
19+
20+
```js
21+
module.exports = {
22+
...
23+
entry: {
24+
...
25+
vendor: [
26+
...
27+
'react-intl',
28+
],
29+
...
30+
```
31+
32+
3. `react-intl` requires locale files in json format. React on Rails will help you to generate or update `translations.js` & `default.js` automatically after you configured the following settings.
33+
34+
> `translations.js`: All your locales in json format.
35+
>
36+
> `default.js`: Default settings in json format.
37+
>
38+
> You can add them to `.gitignore` and `.eslintignore`.
39+
40+
Update settings in `config/initializers/react_on_rails.rb` to what you need:
41+
42+
```ruby
43+
# Replace the following line to the location where you keep translation.js & default.js.
44+
config.i18n_dir = Rails.root.join("PATH_TO", "YOUR_JS_I18N_FOLDER")
45+
```
46+
47+
Add following lines to `config/application.rb`, this will help you to generate `translations.js` & `default.js` automatically when you starts the server.
48+
49+
```js
50+
module YourModule
51+
class Application < Rails::Application
52+
...
53+
config.after_initialize do
54+
ReactOnRails::LocalesToJs.new
55+
end
56+
end
57+
end
58+
```
59+
60+
5. In React, you need to initialize `react-intl`, and set parameters for it.
61+
62+
```js
63+
...
64+
import { addLocaleData } from 'react-intl';
65+
import en from 'react-intl/locale-data/en';
66+
import de from 'react-intl/locale-data/de';
67+
import { translations } from 'path_to/i18n/translations';
68+
import { defaultLocale } from 'path_to/i18n/default';
69+
...
70+
// Initizalize all locales for react-intl.
71+
addLocaleData([...en, ...de]);
72+
...
73+
// set locale and messages for IntlProvider.
74+
const locale = method_to_get_current_locale() || defaultLocale;
75+
const messages = translations[locale];
76+
...
77+
return (
78+
<IntlProvider locale={locale} key={locale} messages={messages}>
79+
<CommentScreen {...{ actions, data }} />
80+
</IntlProvider>
81+
)
82+
```
83+
```js
84+
// In your component.
85+
import { defaultMessages } from 'path_to/i18n/default';
86+
...
87+
return (
88+
{ formatMessage(defaultMessages.yourLocaleKeyInCamelCase) }
89+
)
90+
```

lib/generators/react_on_rails/templates/base/base/config/initializers/react_on_rails.rb.tt

+6
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,12 @@ ReactOnRails.configure do |config|
5959
config.server_renderer_pool_size = 1 # increase if you're on JRuby
6060
config.server_renderer_timeout = 20 # seconds
6161

62+
################################################################################
63+
# I18N OPTIONS
64+
################################################################################
65+
# Replace the following line to the location where you keep translation.js & default.js.
66+
config.i18n_dir = Rails.root.join("client", "app", "libs", "i18n")
67+
6268
################################################################################
6369
# MISCELLANEOUS OPTIONS
6470
################################################################################

lib/react_on_rails.rb

+1
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,4 @@
1616
require "react_on_rails/test_helper/webpack_assets_status_checker"
1717
require "react_on_rails/test_helper/ensure_assets_compiled"
1818
require "react_on_rails/test_helper/node_process_launcher"
19+
require "react_on_rails/locales_to_js"

lib/react_on_rails/configuration.rb

+4
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ def self.configuration
7272
server_render_method: "",
7373
symlink_non_digested_assets_regex: /\.(png|jpg|jpeg|gif|tiff|woff|ttf|eot|svg|map)/,
7474
npm_build_test_command: "",
75+
i18n_dir: "",
7576
npm_build_production_command: ""
7677
)
7778
end
@@ -84,6 +85,7 @@ class Configuration
8485
:skip_display_none, :generated_assets_dirs, :generated_assets_dir,
8586
:webpack_generated_files, :rendering_extension, :npm_build_test_command,
8687
:npm_build_production_command,
88+
:i18n_dir,
8789
:server_render_method, :symlink_non_digested_assets_regex
8890

8991
def initialize(server_bundle_js_file: nil, prerender: nil, replay_console: nil,
@@ -94,12 +96,14 @@ def initialize(server_bundle_js_file: nil, prerender: nil, replay_console: nil,
9496
generated_assets_dir: nil, webpack_generated_files: nil,
9597
rendering_extension: nil, npm_build_test_command: nil,
9698
npm_build_production_command: nil,
99+
i18n_dir: nil,
97100
server_render_method: nil, symlink_non_digested_assets_regex: nil)
98101
self.server_bundle_js_file = server_bundle_js_file
99102
self.generated_assets_dirs = generated_assets_dirs
100103
self.generated_assets_dir = generated_assets_dir
101104
self.npm_build_test_command = npm_build_test_command
102105
self.npm_build_production_command = npm_build_production_command
106+
self.i18n_dir = i18n_dir
103107

104108
self.prerender = prerender
105109
self.replay_console = replay_console

lib/react_on_rails/locales_to_js.rb

+119
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
require "erb"
2+
3+
module ReactOnRails
4+
class LocalesToJs
5+
def initialize
6+
return unless obsolete?
7+
@translations, @defaults = generate_translations
8+
convert
9+
end
10+
11+
private
12+
13+
def obsolete?
14+
return true if exist_js_files.empty?
15+
js_files_are_outdated
16+
end
17+
18+
def exist_js_files
19+
@exist_js_files ||= js_files.select(&File.method(:exist?))
20+
end
21+
22+
def js_files_are_outdated
23+
latest_yml = locale_files.map(&File.method(:mtime)).max
24+
earliest_js = exist_js_files.map(&File.method(:mtime)).min
25+
latest_yml > earliest_js
26+
end
27+
28+
def js_file_names
29+
%w(translations default)
30+
end
31+
32+
def js_files
33+
@js_files ||= js_file_names.map { |n| js_file(n) }
34+
end
35+
36+
def js_file(name)
37+
"#{i18n_dir}/#{name}.js"
38+
end
39+
40+
def locale_files
41+
@locale_files ||= Rails.application.config.i18n.load_path
42+
end
43+
44+
def i18n_dir
45+
@i18n_dir ||= ReactOnRails.configuration.i18n_dir
46+
end
47+
48+
def default_locale
49+
@default_locale ||= I18n.default_locale.to_s || "en"
50+
end
51+
52+
def convert
53+
js_file_names.each do |name|
54+
template = send("template_#{name}")
55+
path = js_file(name)
56+
generate_js_file(template, path)
57+
end
58+
end
59+
60+
def generate_js_file(template, path)
61+
result = ERB.new(template).result()
62+
File.open(path, "w") do |f|
63+
f.write(result)
64+
end
65+
end
66+
67+
def generate_translations
68+
translations = {}
69+
defaults = {}
70+
locale_files.each do |f|
71+
translation = YAML.load(File.open(f))
72+
key = translation.keys[0]
73+
val = flatten(translation[key])
74+
translations = translations.deep_merge(key => val)
75+
defaults = defaults.deep_merge(flatten_defaults(val)) if key == default_locale
76+
end
77+
[translations.to_json, defaults.to_json]
78+
end
79+
80+
def format(input)
81+
input.to_s.tr(".", "_").camelize(:lower).to_sym
82+
end
83+
84+
def flatten_defaults(val)
85+
flatten(val).each_with_object({}) do |(k, v), h|
86+
key = format(k)
87+
h[key] = { id: k, defaultMessage: v }
88+
end
89+
end
90+
91+
def flatten(translations)
92+
translations.each_with_object({}) do |(k, v), h|
93+
if v.is_a? Hash
94+
flatten(v).map { |hk, hv| h["#{k}.#{hk}".to_sym] = hv }
95+
else
96+
h[k] = v
97+
end
98+
end
99+
end
100+
101+
def template_translations
102+
<<-JS
103+
export const translations = #{@translations};
104+
JS
105+
end
106+
107+
def template_default
108+
<<-JS
109+
import { defineMessages } from 'react-intl';
110+
111+
const defaultLocale = \'#{default_locale}\';
112+
113+
const defaultMessages = defineMessages(#{@defaults});
114+
115+
export { defaultMessages, defaultLocale };
116+
JS
117+
end
118+
end
119+
end

spec/dummy/config/initializers/react_on_rails.rb

+6
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,12 @@ def self.custom_context(view_context)
7171
config.server_renderer_pool_size = 1 # increase if you're on JRuby
7272
config.server_renderer_timeout = 20 # seconds
7373

74+
################################################################################
75+
# I18N OPTIONS
76+
################################################################################
77+
# Replace the following line to the location where you keep translation.js & default.js.
78+
config.i18n_dir = Rails.root.join("client", "app", "libs", "i18n")
79+
7480
################################################################################
7581
# MISCELLANEOUS OPTIONS
7682
################################################################################
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
de:
2+
hello: "Hallo welt"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
en:
2+
hello: "Hello world"
+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
require_relative "spec_helper"
2+
require "tmpdir"
3+
4+
module ReactOnRails
5+
RSpec.describe LocalesToJs do
6+
let(:i18n_dir) { Pathname.new(Dir.mktmpdir) }
7+
let(:locale_dir) { File.expand_path("../fixtures/i18n/locales", __FILE__) }
8+
let(:translations_path) { "#{i18n_dir}/translations.js" }
9+
let(:default_path) { "#{i18n_dir}/default.js" }
10+
let(:en_path) { "#{locale_dir}/en.yml" }
11+
12+
before do
13+
allow_any_instance_of(ReactOnRails::LocalesToJs).to receive(:locale_files).and_return(Dir["#{locale_dir}/*"])
14+
ReactOnRails.configure do |config|
15+
config.i18n_dir = i18n_dir
16+
end
17+
end
18+
19+
context "with obsolete js files" do
20+
before do
21+
FileUtils.touch(translations_path, mtime: Time.now - 1.year)
22+
FileUtils.touch(en_path, mtime: Time.now - 1.month)
23+
end
24+
25+
it "updates files" do
26+
ReactOnRails::LocalesToJs.new
27+
28+
translations = File.read(translations_path)
29+
default = File.read(default_path)
30+
expect(translations).to include("{\"hello\":\"Hello world\"")
31+
expect(translations).to include("{\"hello\":\"Hallo welt\"")
32+
expect(default).to include("const defaultLocale = 'en';")
33+
expect(default).to include("{\"hello\":{\"id\":\"hello\",\"defaultMessage\":\"Hello world\"}}")
34+
35+
expect(File.mtime(translations_path)).to be >= File.mtime(en_path)
36+
end
37+
end
38+
39+
context "with up-to-date js files" do
40+
before do
41+
ReactOnRails::LocalesToJs.new
42+
end
43+
44+
it "doesn't update files" do
45+
ref_time = Time.now - 1.minute
46+
FileUtils.touch(translations_path, mtime: ref_time)
47+
48+
update_time = Time.now
49+
ReactOnRails::LocalesToJs.new
50+
expect(update_time).to be > File.mtime(translations_path)
51+
end
52+
end
53+
end
54+
end

0 commit comments

Comments
 (0)