Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automatically generate i18n javascript files for react-intl when the serve starts up. #642

Merged
merged 1 commit into from
Jan 12, 2017
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 @@ -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).

### Added
- 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 conversions 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 the I18n functionality.

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")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JasonYCHuang We added this here, with an invalid directory, and later we abort deploying or testing when this directory does not exist.


################################################################################
# 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"
54 changes: 54 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,54 @@
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 - 1.month)
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
ref_time = Time.now - 1.minute
FileUtils.touch(translations_path, mtime: ref_time)

update_time = Time.now
ReactOnRails::LocalesToJs.new
expect(update_time).to be > File.mtime(translations_path)
end
end
end
end