diff --git a/CHANGELOG.md b/CHANGELOG.md index be4832f7ce..d29a314920 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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). diff --git a/README.md b/README.md index ad1111350d..2749380b76 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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: @@ -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 @@ -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. diff --git a/docs/basics/i18n.md b/docs/basics/i18n.md new file mode 100644 index 0000000000..fb004621a4 --- /dev/null +++ b/docs/basics/i18n.md @@ -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 ( + + + + ) + ``` + ```js + // In your component. + import { defaultMessages } from 'path_to/i18n/default'; + ... + return ( + { formatMessage(defaultMessages.yourLocaleKeyInCamelCase) } + ) + ``` diff --git a/lib/generators/react_on_rails/templates/base/base/config/initializers/react_on_rails.rb.tt b/lib/generators/react_on_rails/templates/base/base/config/initializers/react_on_rails.rb.tt index 2e967a7405..20cf015c35 100644 --- a/lib/generators/react_on_rails/templates/base/base/config/initializers/react_on_rails.rb.tt +++ b/lib/generators/react_on_rails/templates/base/base/config/initializers/react_on_rails.rb.tt @@ -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 ################################################################################ diff --git a/lib/react_on_rails.rb b/lib/react_on_rails.rb index e8bc066f33..72c9786990 100644 --- a/lib/react_on_rails.rb +++ b/lib/react_on_rails.rb @@ -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" diff --git a/lib/react_on_rails/configuration.rb b/lib/react_on_rails/configuration.rb index a967038eec..9e8e74c801 100644 --- a/lib/react_on_rails/configuration.rb +++ b/lib/react_on_rails/configuration.rb @@ -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 @@ -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, @@ -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 diff --git a/lib/react_on_rails/locales_to_js.rb b/lib/react_on_rails/locales_to_js.rb new file mode 100644 index 0000000000..c5237ff80c --- /dev/null +++ b/lib/react_on_rails/locales_to_js.rb @@ -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 diff --git a/spec/dummy/config/initializers/react_on_rails.rb b/spec/dummy/config/initializers/react_on_rails.rb index d6fec053c3..940b4a86bd 100644 --- a/spec/dummy/config/initializers/react_on_rails.rb +++ b/spec/dummy/config/initializers/react_on_rails.rb @@ -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 ################################################################################ diff --git a/spec/react_on_rails/fixtures/i18n/locales/de.yml b/spec/react_on_rails/fixtures/i18n/locales/de.yml new file mode 100644 index 0000000000..3f02f787ec --- /dev/null +++ b/spec/react_on_rails/fixtures/i18n/locales/de.yml @@ -0,0 +1,2 @@ +de: + hello: "Hallo welt" diff --git a/spec/react_on_rails/fixtures/i18n/locales/en.yml b/spec/react_on_rails/fixtures/i18n/locales/en.yml new file mode 100644 index 0000000000..a9f72ecc77 --- /dev/null +++ b/spec/react_on_rails/fixtures/i18n/locales/en.yml @@ -0,0 +1,2 @@ +en: + hello: "Hello world" diff --git a/spec/react_on_rails/locales_to_js_spec.rb b/spec/react_on_rails/locales_to_js_spec.rb new file mode 100644 index 0000000000..68bac85f2f --- /dev/null +++ b/spec/react_on_rails/locales_to_js_spec.rb @@ -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