Skip to content

Commit

Permalink
Automatically generate i18n javascript files for react-intl when the …
Browse files Browse the repository at this point in the history
…serve starts up
  • Loading branch information
JasonYCHuang committed Jan 11, 2017
1 parent a97e209 commit d277795
Show file tree
Hide file tree
Showing 11 changed files with 296 additions and 9 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ Contributors: please follow the recommendations outlined at [keepachangelog.com]
### Fixed
- Removed foreman as a dependency. [#678](https://github.com/shakacode/react_on_rails/pull/678) by [x2es](https://github.com/x2es).

### Changed
- 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).

## [6.3.5] - 2017-1-6
### Fixed
- 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).
Expand Down
19 changes: 10 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ React on Rails integrates Facebook's [React](https://github.com/facebook/react)
- [Installation Summary](#installation-summary)
- [Initializer Configuration: config/initializers/react_on_rails.rb](#initializer-configuration)
- [Including your React Component in your Rails Views](#including-your-react-component-in-your-rails-views)
- [I18n](#i18n)
+ [How it Works](#how-it-works)
- [Client-Side Rendering vs. Server-Side Rendering](#client-side-rendering-vs-server-side-rendering)
- [Building the Bundles](#building-the-bundles)
Expand Down Expand Up @@ -170,7 +171,15 @@ Configure the `config/initializers/react_on_rails.rb`. You can adjust some neces
// inside your React component
this.props.name // "Stranger"
```
### I18n
You can enable the i18n functionality with [react-intl](https://github.com/yahoo/react-intl).
React on Rails provides an option for automatic conversion of Rails `*.yml` locale files into `*.js` files for `react-intl`.
See the [How to add I18n](docs/basics/i18n.md) for a summary of adding I18n.
## NPM
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:
Expand Down Expand Up @@ -245,11 +254,6 @@ The `railsContext` has: (see implementation in file [react_on_rails_helper.rb](a
pathname: uri.path, # /posts
search: uri.query, # id=30&limit=5

# Locale settings
i18nLocale: I18n.locale,
i18nDefaultLocale: I18n.default_locale,
httpAcceptLanguage: request.env["HTTP_ACCEPT_LANGUAGE"],

# Other
serverSide: boolean # Are we being called on the server or client? NOTE, if you conditionally
# render something different on the server than the client, then React will only show the
Expand All @@ -261,9 +265,6 @@ The `railsContext` has: (see implementation in file [react_on_rails_helper.rb](a
##### Needing the current url path for server rendering
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.

##### Needing the I18n.locale
Suppose you want to server render your react components with localization applied given the current Rails locale. The `railsContext` contains the I18n.locale.

##### Configuring different code for server side rendering
Suppose you want to turn off animation when doing server side rendering. The `serverSide` value is just what you need.

Expand Down
90 changes: 90 additions & 0 deletions docs/basics/i18n.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# How to add I18n

Here's a summary of adding I18n functionality with React on Rails.

You can refer to [react-webpack-rails-tutorial](https://github.com/shakacode/react-webpack-rails-tutorial) for a complete example.

1. Add `react-intl` & `intl` to `client/package.json`, and remember to `bundle && npm install`.

```js
"dependencies": {
...
"intl": "^1.2.5",
"react-intl": "^2.1.5",
...
}
```

2. In `client/webpack.client.base.config.js`, set `react-intl` as an entry point.

```js
module.exports = {
...
entry: {
...
vendor: [
...
'react-intl',
],
...
```
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.
> `translations.js`: All your locales in json format.
>
> `default.js`: Default settings in json format.
>
> You can add them to `.gitignore` and `.eslintignore`.
Update settings in `config/initializers/react_on_rails.rb` to what you need:
```ruby
# Replace the following line to the location where you keep translation.js & default.js.
config.i18n_dir = Rails.root.join("PATH_TO", "YOUR_JS_I18N_FOLDER")
```
Add following lines to `config/application.rb`, this will help you to generate `translations.js` & `default.js` automatically when you starts the server.
```js
module YourModule
class Application < Rails::Application
...
config.after_initialize do
ReactOnRails::LocalesToJs.new
end
end
end
```
5. In React, you need to initialize `react-intl`, and set parameters for it.
```js
...
import { addLocaleData } from 'react-intl';
import en from 'react-intl/locale-data/en';
import de from 'react-intl/locale-data/de';
import { translations } from 'path_to/i18n/translations';
import { defaultLocale } from 'path_to/i18n/default';
...
// Initizalize all locales for react-intl.
addLocaleData([...en, ...de]);
...
// set locale and messages for IntlProvider.
const locale = method_to_get_current_locale() || defaultLocale;
const messages = translations[locale];
...
return (
<IntlProvider locale={locale} key={locale} messages={messages}>
<CommentScreen {...{ actions, data }} />
</IntlProvider>
)
```
```js
// In your component.
import { defaultMessages } from 'path_to/i18n/default';
...
return (
{ formatMessage(defaultMessages.yourLocaleKeyInCamelCase) }
)
```
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ ReactOnRails.configure do |config|
config.server_renderer_pool_size = 1 # increase if you're on JRuby
config.server_renderer_timeout = 20 # seconds

################################################################################
# I18N OPTIONS
################################################################################
# Replace the following line to the location where you keep translation.js & default.js.
config.i18n_dir = Rails.root.join("client", "app", "libs", "i18n")

################################################################################
# MISCELLANEOUS OPTIONS
################################################################################
Expand Down
1 change: 1 addition & 0 deletions lib/react_on_rails.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@
require "react_on_rails/test_helper/webpack_assets_status_checker"
require "react_on_rails/test_helper/ensure_assets_compiled"
require "react_on_rails/test_helper/node_process_launcher"
require "react_on_rails/locales_to_js"
4 changes: 4 additions & 0 deletions lib/react_on_rails/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ def self.configuration
server_render_method: "",
symlink_non_digested_assets_regex: /\.(png|jpg|jpeg|gif|tiff|woff|ttf|eot|svg|map)/,
npm_build_test_command: "",
i18n_dir: "",
npm_build_production_command: ""
)
end
Expand All @@ -84,6 +85,7 @@ class Configuration
:skip_display_none, :generated_assets_dirs, :generated_assets_dir,
:webpack_generated_files, :rendering_extension, :npm_build_test_command,
:npm_build_production_command,
:i18n_dir,
:server_render_method, :symlink_non_digested_assets_regex

def initialize(server_bundle_js_file: nil, prerender: nil, replay_console: nil,
Expand All @@ -94,12 +96,14 @@ def initialize(server_bundle_js_file: nil, prerender: nil, replay_console: nil,
generated_assets_dir: nil, webpack_generated_files: nil,
rendering_extension: nil, npm_build_test_command: nil,
npm_build_production_command: nil,
i18n_dir: nil,
server_render_method: nil, symlink_non_digested_assets_regex: nil)
self.server_bundle_js_file = server_bundle_js_file
self.generated_assets_dirs = generated_assets_dirs
self.generated_assets_dir = generated_assets_dir
self.npm_build_test_command = npm_build_test_command
self.npm_build_production_command = npm_build_production_command
self.i18n_dir = i18n_dir

self.prerender = prerender
self.replay_console = replay_console
Expand Down
119 changes: 119 additions & 0 deletions lib/react_on_rails/locales_to_js.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
require "erb"

module ReactOnRails
class LocalesToJs
def initialize
return unless obsolete?
@translations, @defaults = generate_translations
convert
end

private

def obsolete?
return true if exist_js_files.empty?
js_files_are_outdated
end

def exist_js_files
@exist_js_files ||= js_files.select(&File.method(:exist?))
end

def js_files_are_outdated
latest_yml = locale_files.map(&File.method(:mtime)).max
earliest_js = exist_js_files.map(&File.method(:mtime)).min
latest_yml > earliest_js
end

def js_file_names
%w(translations default)
end

def js_files
@js_files ||= js_file_names.map { |n| js_file(n) }
end

def js_file(name)
"#{i18n_dir}/#{name}.js"
end

def locale_files
@locale_files ||= Rails.application.config.i18n.load_path
end

def i18n_dir
@i18n_dir ||= ReactOnRails.configuration.i18n_dir
end

def default_locale
@default_locale ||= I18n.default_locale.to_s || "en"
end

def convert
js_file_names.each do |name|
template = send("template_#{name}")
path = js_file(name)
generate_js_file(template, path)
end
end

def generate_js_file(template, path)
result = ERB.new(template).result()
File.open(path, "w") do |f|
f.write(result)
end
end

def generate_translations
translations = {}
defaults = {}
locale_files.each do |f|
translation = YAML.load(File.open(f))
key = translation.keys[0]
val = flatten(translation[key])
translations = translations.deep_merge(key => val)
defaults = defaults.deep_merge(flatten_defaults(val)) if key == default_locale
end
[translations.to_json, defaults.to_json]
end

def format(input)
input.to_s.tr(".", "_").camelize(:lower).to_sym
end

def flatten_defaults(val)
flatten(val).each_with_object({}) do |(k, v), h|
key = format(k)
h[key] = { id: k, defaultMessage: v }
end
end

def flatten(translations)
translations.each_with_object({}) do |(k, v), h|
if v.is_a? Hash
flatten(v).map { |hk, hv| h["#{k}.#{hk}".to_sym] = hv }
else
h[k] = v
end
end
end

def template_translations
<<-JS
export const translations = #{@translations};
JS
end

def template_default
<<-JS
import { defineMessages } from 'react-intl';
const defaultLocale = \'#{default_locale}\';
const defaultMessages = defineMessages(#{@defaults});
export { defaultMessages, defaultLocale };
JS
end
end
end
6 changes: 6 additions & 0 deletions spec/dummy/config/initializers/react_on_rails.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@ def self.custom_context(view_context)
config.server_renderer_pool_size = 1 # increase if you're on JRuby
config.server_renderer_timeout = 20 # seconds

################################################################################
# I18N OPTIONS
################################################################################
# Replace the following line to the location where you keep translation.js & default.js.
config.i18n_dir = Rails.root.join("client", "app", "libs", "i18n")

################################################################################
# MISCELLANEOUS OPTIONS
################################################################################
Expand Down
2 changes: 2 additions & 0 deletions spec/react_on_rails/fixtures/i18n/locales/de.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
de:
hello: "Hallo welt"
2 changes: 2 additions & 0 deletions spec/react_on_rails/fixtures/i18n/locales/en.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
en:
hello: "Hello world"
53 changes: 53 additions & 0 deletions spec/react_on_rails/locales_to_js_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
require_relative "spec_helper"
require "tmpdir"

module ReactOnRails
RSpec.describe LocalesToJs do
let(:i18n_dir) { Pathname.new(Dir.mktmpdir) }
let(:locale_dir) { File.expand_path("../fixtures/i18n/locales", __FILE__) }
let(:translations_path) { "#{i18n_dir}/translations.js" }
let(:default_path) { "#{i18n_dir}/default.js" }
let(:en_path) { "#{locale_dir}/en.yml" }

before do
allow_any_instance_of(ReactOnRails::LocalesToJs).to receive(:locale_files).and_return(Dir["#{locale_dir}/*"])
ReactOnRails.configure do |config|
config.i18n_dir = i18n_dir
end
end

context "with obsolete js files" do
before do
FileUtils.touch(translations_path, mtime: Time.now - 1.year)
FileUtils.touch(en_path, mtime: Time.now)
end

it "updates files" do
ReactOnRails::LocalesToJs.new

translations = File.read(translations_path)
default = File.read(default_path)
expect(translations).to include("{\"hello\":\"Hello world\"")
expect(translations).to include("{\"hello\":\"Hallo welt\"")
expect(default).to include("const defaultLocale = 'en';")
expect(default).to include("{\"hello\":{\"id\":\"hello\",\"defaultMessage\":\"Hello world\"}}")

expect(File.mtime(translations_path)).to be >= File.mtime(en_path)
end
end

context "with up-to-date js files" do
before do
ReactOnRails::LocalesToJs.new
end

it "doesn't update files" do
mt = File.mtime(translations_path)
sleep 1

ReactOnRails::LocalesToJs.new
expect(File.mtime(translations_path)).to eq(mt)
end
end
end
end

0 comments on commit d277795

Please sign in to comment.