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 8, 2017
1 parent f929478 commit 37d1a2a
Show file tree
Hide file tree
Showing 11 changed files with 269 additions and 9 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ Contributors: please follow the recommendations outlined at [keepachangelog.com]
## [Unreleased]
*Please add entries here for your pull requests.*

##### 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] - 2016-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
17 changes: 8 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 @@ -168,7 +169,13 @@ 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). ReactOnRails also converts traditional Rails locale files, `*.yml`, to required javascript files, `translations.js` & `default.js`, automatically.
See the [How to add I18n](docs/basics/i18n.md) for a summary of adding I18n functionality with ReactOnRails.
## 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 @@ -243,11 +250,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 @@ -259,9 +261,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 ReactOnRails.

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. ReactOnRails will create or update this transformation automatically after you finished the following settings.
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.convert
end
end
end
```
4. In React, you need to initialize `react-intl`, and set parameters for it.
> `translations.js`: All your locales in json format.
>
> `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.
>
> There is no need to track and lint `translations.js` & `default.js`, and you can add them to `.gitignore` and `.eslintignore`.
```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
117 changes: 117 additions & 0 deletions lib/react_on_rails/locales_to_js.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
require "erb"

module ReactOnRails
class LocalesToJs
def initialize
@translations, @defaults = generate_translations if obsolete?
end

def convert
convert_to_js if obsolete?
end

private

def obsolete?
@obsolete ||= (latest_file_names - i18n_js_files.map { |f| f.tr("_", ".") }).present?
end

def latest_file_names
files = locale_files + i18n_js_files.map { |f| send("path_#{f}").to_s }
.select { |f| File.exist?(f) }
files = files.sort_by { |f| File.mtime(f) }
files.last(2).map { |f| File.basename(f) }
end

def convert_to_js
i18n_js_files.each do |f|
template = send("template_#{f}")
path = send("path_#{f}")
create_js_file(template, path)
end
end

def i18n_js_files
%w(translations_js default_js)
end

def create_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 i18n_dir
@i18n_dir ||= ReactOnRails.configuration.i18n_dir
end

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

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

def path_translations_js
i18n_dir + "translations.js"
end

def path_default_js
i18n_dir + "default.js"
end

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

def template_default_js
<<-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"
30 changes: 30 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,30 @@
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__) }

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

it "generates translations.js & default.js" do
ReactOnRails::LocalesToJs.new.convert

files = Dir["#{i18n_dir.to_path}/*"].map { |p| Pathname.new(p).basename.to_s }
expect(files).to include("translations.js", "default.js")

result_translations = File.read("#{i18n_dir.to_path}/translations.js")
result_default = File.read("#{i18n_dir.to_path}/default.js")
expect(result_translations).to include("{\"hello\":\"Hello world\"")
expect(result_translations).to include("{\"hello\":\"Hallo welt\"")
expect(result_default).to include("const defaultLocale = 'en';")
expect(result_default).to include("{\"hello\":{\"id\":\"hello\",\"defaultMessage\":\"Hello world\"}}")
end
end
end

0 comments on commit 37d1a2a

Please sign in to comment.