Skip to content

Commit 093a996

Browse files
committed
Automatically generate i18n javascript files for react-intl when the serve starts up
1 parent f929478 commit 093a996

File tree

11 files changed

+267
-9
lines changed

11 files changed

+267
-9
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ Contributors: please follow the recommendations outlined at [keepachangelog.com]
66
## [Unreleased]
77
*Please add entries here for your pull requests.*
88

9+
##### Changed
10+
- 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).
11+
912
## [6.3.5] - 2016-1-6
1013
### Fixed
1114
- 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

+8-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)
@@ -168,7 +169,13 @@ Configure the `config/initializers/react_on_rails.rb`. You can adjust some neces
168169
// inside your React component
169170
this.props.name // "Stranger"
170171
```
171-
172+
173+
### I18n
174+
175+
You can enable the i18n functionality with [react-intl](https://github.com/yahoo/react-intl). ReactOnRails also converts traditional Rails locale files, `*.yml`, to required javascript files, `translations.js` & `default.js`, automatically.
176+
177+
See the [How to add I18n](docs/basics/i18n.md) for a summary of adding I18n functionality with ReactOnRails.
178+
172179
## NPM
173180
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:
174181
@@ -243,11 +250,6 @@ The `railsContext` has: (see implementation in file [react_on_rails_helper.rb](a
243250
pathname: uri.path, # /posts
244251
search: uri.query, # id=30&limit=5
245252

246-
# Locale settings
247-
i18nLocale: I18n.locale,
248-
i18nDefaultLocale: I18n.default_locale,
249-
httpAcceptLanguage: request.env["HTTP_ACCEPT_LANGUAGE"],
250-
251253
# Other
252254
serverSide: boolean # Are we being called on the server or client? NOTE, if you conditionally
253255
# render something different on the server than the client, then React will only show the
@@ -259,9 +261,6 @@ The `railsContext` has: (see implementation in file [react_on_rails_helper.rb](a
259261
##### Needing the current url path for server rendering
260262
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.
261263

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

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 I18n functionality with ReactOnRails.
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. ReactOnRails will create or update this transformation automatically after you finished the following settings.
33+
34+
Update settings in `config/initializers/react_on_rails.rb` to what you need:
35+
36+
```ruby
37+
# Replace the following line to the location where you keep translation.js & default.js.
38+
config.i18n_dir = Rails.root.join("PATH_TO", "YOUR_JS_I18N_FOLDER")
39+
```
40+
41+
Add following lines to `config/application.rb`, this will help you to generate `translations.js` & `default.js` automatically when you starts the server.
42+
43+
```js
44+
module YourModule
45+
class Application < Rails::Application
46+
...
47+
config.after_initialize do
48+
ReactOnRails::LocalesToJs.new
49+
end
50+
end
51+
end
52+
```
53+
54+
4. In React, you need to initialize `react-intl`, and set parameters for it.
55+
56+
> `translations.js`: All your locales in json format.
57+
>
58+
> `default.js`: [1] `defaultLocale` is your default locale, like "en". [2] `defaultMessages` is the place where you can get your local values with localeKeyInCamelForm, and it also contains fallback when something went wrong.
59+
>
60+
> There is no need to track and lint `translations.js` & `default.js`, and you can add them to `.gitignore` and `.eslintignore`.
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

+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
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+
@obsolete ||= (latest_file_names - i18n_js_files.map { |f| f.tr("_", ".") }).present?
15+
end
16+
17+
def latest_file_names
18+
files = locale_files + i18n_js_files.map { |f| send("path_#{f}").to_s }
19+
.select { |f| File.exist?(f) }
20+
files = files.sort_by { |f| File.mtime(f) }
21+
files.last(2).map { |f| File.basename(f) }
22+
end
23+
24+
def convert
25+
i18n_js_files.each do |f|
26+
template = send("template_#{f}")
27+
path = send("path_#{f}")
28+
create_js_file(template, path)
29+
end
30+
end
31+
32+
def i18n_js_files
33+
%w(translations_js default_js)
34+
end
35+
36+
def create_js_file(template, path)
37+
result = ERB.new(template).result()
38+
File.open(path, "w") do |f|
39+
f.write(result)
40+
end
41+
end
42+
43+
def generate_translations
44+
translations = {}
45+
defaults = {}
46+
locale_files.each do |f|
47+
translation = YAML.load(File.open(f))
48+
key = translation.keys[0]
49+
val = flatten(translation[key])
50+
translations = translations.deep_merge(key => val)
51+
defaults = defaults.deep_merge(flatten_defaults(val)) if key == default_locale
52+
end
53+
[translations.to_json, defaults.to_json]
54+
end
55+
56+
def format(input)
57+
input.to_s.tr(".", "_").camelize(:lower).to_sym
58+
end
59+
60+
def flatten_defaults(val)
61+
flatten(val).each_with_object({}) do |(k, v), h|
62+
key = format(k)
63+
h[key] = { id: k, defaultMessage: v }
64+
end
65+
end
66+
67+
def flatten(translations)
68+
translations.each_with_object({}) do |(k, v), h|
69+
if v.is_a? Hash
70+
flatten(v).map { |hk, hv| h["#{k}.#{hk}".to_sym] = hv }
71+
else
72+
h[k] = v
73+
end
74+
end
75+
end
76+
77+
def i18n_dir
78+
@i18n_dir ||= ReactOnRails.configuration.i18n_dir
79+
end
80+
81+
def locale_files
82+
@locale_files ||= Rails.application.config.i18n.load_path
83+
end
84+
85+
def default_locale
86+
@default_locale ||= I18n.default_locale.to_s || "en"
87+
end
88+
89+
def path_translations_js
90+
i18n_dir + "translations.js"
91+
end
92+
93+
def path_default_js
94+
i18n_dir + "default.js"
95+
end
96+
97+
def template_translations_js
98+
<<-JS
99+
export const translations = #{@translations};
100+
JS
101+
end
102+
103+
def template_default_js
104+
<<-JS
105+
import { defineMessages } from 'react-intl';
106+
107+
const defaultLocale = \'#{default_locale}\';
108+
109+
const defaultMessages = defineMessages(#{@defaults});
110+
111+
export { defaultMessages, defaultLocale };
112+
JS
113+
end
114+
end
115+
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"
+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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+
9+
before do
10+
ReactOnRails::LocalesToJs.any_instance.stub(:locale_files).and_return(Dir["#{locale_dir}/*"])
11+
ReactOnRails.configure do |config|
12+
config.i18n_dir = i18n_dir
13+
end
14+
end
15+
16+
it "generates translations.js & default.js" do
17+
ReactOnRails::LocalesToJs.new
18+
19+
files = Dir["#{i18n_dir.to_path}/*"].map { |p| Pathname.new(p).basename.to_s }
20+
expect(files).to include("translations.js", "default.js")
21+
22+
result_translations = File.read("#{i18n_dir.to_path}/translations.js")
23+
result_default = File.read("#{i18n_dir.to_path}/default.js")
24+
expect(result_translations).to include("{\"hello\":\"Hello world\"")
25+
expect(result_translations).to include("{\"hello\":\"Hallo welt\"")
26+
expect(result_default).to include("const defaultLocale = 'en';")
27+
expect(result_default).to include("{\"hello\":{\"id\":\"hello\",\"defaultMessage\":\"Hello world\"}}")
28+
end
29+
end
30+
end

0 commit comments

Comments
 (0)