Now we’re entering the home stretch. The new order page is next:
| <section class="depot_form"> |
» | <h1><%= t('.legend') %></h1> |
| <%= render 'form', order: @order %> |
| </section> |
| |
| <%= javascript_pack_tag("pay_type") %> |
Here’s the form that’s used by this page:
| <%= form_with(model: order, local: true) do |form| %> |
| <% if order.errors.any? %> |
| <div id="error_explanation"> |
| <h2><%= pluralize(order.errors.count, "error") %> |
| prohibited this order from being saved:</h2> |
| |
| <ul> |
| <% order.errors.full_messages.each do |message| %> |
| <li><%= message %></li> |
| <% end %> |
| </ul> |
| </div> |
| <% end %> |
| |
| <div class="field"> |
» | <%= form.label :name, t('.name') %> |
| <%= form.text_field :name, size: 40 %> |
| </div> |
| |
| <div class="field"> |
» | <%= form.label :address, t('.address_html') %> |
| <%= form.text_area :address, rows: 3, cols: 40 %> |
| </div> |
| |
| <div class="field"> |
» | <%= form.label :email, t('.email') %> |
| <%= form.email_field :email, size: 40 %> |
| </div> |
| |
| <div id='pay-type-component'></div> |
| |
| <div class="actions"> |
» | <%= form.submit t('.submit') %> |
| </div> |
| <% end %> |
That covers the form elements that Rails is rendering, but what about the React-rendered payment details we added in Iteration H1: Adding Fields Dynamically to a Form? If you recall, we had to create the HTML form elements inside React components, mimicking what Rails form helpers would do.
Since React is rendering our payment details components—--not Rails—--we need to make our translations available to React, meaning they must be available in JavaScript. The i18n-js library will do just that.[80]
This library will make a copy of our translations as a JavaScript object and provide an object called I18n that allows us to access them. Our React components will use that to provide localized strings for the dynamic form we created earlier. First, we’ll add it to our Gemfile.
| gem 'i18n-js' |
Install it with bundle install. Getting i18n-js to work requires a bit of configuration, so let’s do that before we start using it in our React components.
First, we’ll configure i18n-js to convert our translations. This is done by a middleware that the gem provides.[81] A middleware is a way to add behavior to all requests served by a Rails app by manipulating an internal data structure. In the case of i18n-js, its middleware makes sure that the JavaScript copy of our translations is in sync with those in config/locales.
We can set this up by adding a line of code to config/application.rb:
| config.middleware.use I18n::JS::Middleware |
This requires restarting our server, so if you are currently running it, go ahead and restart it now.
Next, we need to tell Rails to serve up the translations that i18n-js provides. We also need to make the I18n object available. We can do that by adding two require directives to app/assets/javascripts/application.js. These directives tell Rails to include the referenced JavaScript libraries when serving up pages. Since the JavaScript files that come with i18n-js are inside a gem, we have to do this explicitly.
| window.I18n = require("../../../public/javascripts/i18n") |
| require("../../../public/javascripts/translations") |
The last bit of configuration we need for i18n-js is to tell it what the currently chosen locale is. We can do that by rendering a dynamic script tag in our application layout in app/views/layouts/application.html.erb.
| <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %> |
» | <script type="text/javascript"> |
» | I18n.defaultLocale = "<%= I18n.default_locale %>"; |
» | I18n.locale = "<%= I18n.locale %>"; |
» | </script> |
Note that we want this tag to appear after the call to javascript_pack_tag so that I18n will have been defined.
With this in place, we need to add calls to I18n.t inside the JSX of our React components. This is straightforward to do using the curly brace syntax we’ve seen before. Let’s start with the main component in app/javascript/PayTypeSelector/index.jsx. Here’s the entire class with the render method fully localized.
| import React from 'react' |
| |
| import NoPayType from './NoPayType'; |
| import CreditCardPayType from './CreditCardPayType'; |
| import CheckPayType from './CheckPayType'; |
| import PurchaseOrderPayType from './PurchaseOrderPayType'; |
| |
| class PayTypeSelector extends React.Component { |
| constructor(props) { |
| super(props); |
| this.onPayTypeSelected = this.onPayTypeSelected.bind(this); |
| this.state = { selectedPayType: null }; |
| } |
| |
| onPayTypeSelected(event) { |
| this.setState({ selectedPayType: event.target.value }); |
| } |
| |
| render() { |
| let PayTypeCustomComponent = NoPayType; |
| if (this.state.selectedPayType == "Credit card") { |
| PayTypeCustomComponent = CreditCardPayType; |
| } else if (this.state.selectedPayType == "Check") { |
| PayTypeCustomComponent = CheckPayType; |
| } else if (this.state.selectedPayType == "Purchase order") { |
| PayTypeCustomComponent = PurchaseOrderPayType; |
| } |
» | return ( |
» | <div> |
» | <div className="field"> |
» | <label htmlFor="order_pay_type"> |
» | {I18n.t("orders.form.pay_type")} |
» | </label> |
» | |
» | <select id="order_pay_type" onChange={this.onPayTypeSelected} |
» | name="order[pay_type]"> |
» | <option value=""> |
» | {I18n.t("orders.form.pay_prompt_html")} |
» | </option> |
» | |
» | <option value="Check"> |
» | {I18n.t("orders.form.pay_types.check")} |
» | </option> |
» | |
» | <option value="Credit card"> |
» | {I18n.t("orders.form.pay_types.credit_card")} |
» | </option> |
» | |
» | <option value="Purchase order"> |
» | {I18n.t("orders.form.pay_types.purchase_order")} |
» | </option> |
» | |
» | </select> |
» | </div> |
» | <PayTypeCustomComponent /> |
» | </div> |
» | ); |
| } |
| } |
| export default PayTypeSelector |
Although I18n.t is similar to Rails’ t, note the subtle difference in the argument to the method. In our Rails view, we can simply use t(".pay_type") which, as we learned in Iteration K2: Translating the Storefront, allows Rails to figure out from the template name where the strings are in the locale YAML files. We can’t take advantage of this with i18n-js, so we must specify the complete path to the translation in the YAML file.
Next, let’s do this to the three components that make up our payment details view. First up is app/javascript/PayTypeSelector/CheckPayType.jsx (note we aren’t highlighting code here because we are essentially changing the entire render method on each component):
| import React from 'react' |
| |
| class CheckPayType extends React.Component { |
| render() { |
| return ( |
| <div> |
| <div className="field"> |
| <label htmlFor="order_routing_number"> |
| {I18n.t("orders.form.check_pay_type.routing_number")} |
| </label> |
| <input type="password" |
| name="order[routing_number]" |
| id="order_routing_number" /> |
| </div> |
| <div className="field"> |
| <label htmlFor="order_acount_number"> |
| {I18n.t("orders.form.check_pay_type.account_number")} |
| </label> |
| |
| <input type="text" |
| name="order[account_number]" |
| id="order_account_number" /> |
| </div> |
| </div> |
| ); |
| } |
| } |
| export default CheckPayType |
Now, CreditCardPayType.jsx:
| import React from 'react' |
| |
| class CreditCardPayType extends React.Component { |
| render() { |
| return ( |
| <div> |
| <div className="field"> |
| <label htmlFor="order_credit_card_number"> |
| {I18n.t("orders.form.credit_card_pay_type.cc_number")} |
| </label> |
| |
| <input type="password" |
| name="order[credit_card_number]" |
| id="order_credit_card_number" /> |
| </div> |
| <div className="field"> |
| <label htmlFor="order_expiration_date"> |
| {I18n.t("orders.form.credit_card_pay_type.expiration_date")} |
| </label> |
| |
| <input type="text" |
| name="order[expiration_date]" |
| id="order_expiration_date" |
| size="9" |
| placeholder="e.g. 03/19" /> |
| </div> |
| </div> |
| ); |
| } |
| } |
| export default CreditCardPayType |
And finally PurchaseOrderPayType.jsx:
| import React from 'react' |
| |
| class PurchaseOrderPayType extends React.Component { |
| render() { |
| return ( |
| <div> |
| <div className="field"> |
| <label htmlFor="order_po_number"> |
| {I18n.t("orders.form.purchase_order_pay_type.po_number")} |
| </label> |
| |
| <input type="password" |
| name="order[po_number]" |
| id="order_po_number" /> |
| </div> |
| </div> |
| ); |
| } |
| } |
| export default PurchaseOrderPayType |
With those done, here are the corresponding locale definitions:
| en: |
| |
| orders: |
| new: |
| legend: "Please Enter Your Details" |
| form: |
| name: "Name" |
| address_html: "Address" |
| email: "E-mail" |
| pay_type: "Pay with" |
| pay_prompt_html: "Select a payment method" |
| submit: "Place Order" |
| pay_types: |
| check: "Check" |
| credit_card: "Credit Card" |
| purchase_order: "Purchase Order" |
| check_pay_type: |
| routing_number: "Routing #" |
| account_number: "Account #" |
| credit_card_pay_type: |
| cc_number: "CC #" |
| expiration_date: "Expiry" |
| purchase_order_pay_type: |
| po_number: "PO #" |
| es: |
| |
| orders: |
| new: |
| legend: "Por favor, introduzca sus datos" |
| form: |
| name: "Nombre" |
| address_html: "Dirección" |
| email: "E-mail" |
| pay_type: "Forma de pago" |
| pay_prompt_html: "Seleccione un método de pago" |
| submit: "Realizar Pedido" |
| pay_types: |
| check: "Cheque" |
| credit_card: "Tarjeta de Crédito" |
| purchase_order: "Orden de Compra" |
| check_pay_type: |
| routing_number: "# de Enrutamiento" |
| account_number: "# de Cuenta" |
| credit_card_pay_type: |
| cc_number: "Número" |
| expiration_date: "Expiración" |
| purchase_order_pay_type: |
| po_number: "Número" |
See the following screenshot for the completed form.
All looks good until we click the Realizar Pedido button prematurely and see the results shown in the screenshot. The error messages that Active Record produces can also be translated; what we need to do is supply the translations:
| es: |
| |
| activerecord: |
| errors: |
| messages: |
| inclusion: "no está incluido en la lista" |
| blank: "no puede quedar en blanco" |
| errors: |
| template: |
| body: "Hay problemas con los siguientes campos:" |
| header: |
| one: "1 error ha impedido que este %{model} se guarde" |
| other: "%{count} errores han impedido que este %{model} se guarde" |
Although you can create these with many trips to Google Translate, the Rails i18n gem’s GitHub repo contains a lot of translations for common strings in many languages.[82]
Note that messages with counts typically have two forms: errors.template.header.one is the message that’s produced when there’s one error, and errors.template.header.other is produced otherwise. This gives the translators the opportunity to provide the correct pluralization of nouns and to match verbs with the nouns.
Since we once again made use of HTML entities, we want these error messages to be displayed as is (or in Rails parlance, raw). We also need to translate the error messages. So, again, we modify the form:
| <%= form_with(model: order, local: true) do |form| %> |
| <% if order.errors.any? %> |
| <div id="error_explanation"> |
» | <h2><%=raw t('errors.template.header', count: @order.errors.count, |
» | model: t('activerecord.models.order')) %>.</h2> |
» | <p><%= t('errors.template.body') %></p> |
| |
| <ul> |
| <% order.errors.full_messages.each do |message| %> |
» | <li><%=raw message %></li> |
| <% end %> |
| </ul> |
| </div> |
| <% end %> |
| <!-- ... --> |
Note that we’re passing the count and model name (which is, itself, enabled for translation) on the translate call for the error template header. With these changes in place, we try again and see improvement, as shown in the following screenshot.
That’s better, but the names of the model and the attributes bleed through the interface. This is OK in English, because the names we picked work for English. We need to provide translations for each model. This, too, goes into the YAML file:
| es: |
| |
| activerecord: |
| models: |
| order: "pedido" |
| attributes: |
| order: |
| address: "Dirección" |
| name: "Nombre" |
| email: "E-mail" |
| pay_type: "Forma de pago" |
Note that there’s no need to provide English equivalents for this, because those messages are built into Rails.
We’re pleased to see the model and attribute names translated in the following screenshot; we fill out the form, we submit the order, and we get a “Thank you for your order” message.
We need to update the flash messages and add the locale to the store_index_url:
| def create |
| @order = Order.new(order_params) |
| @order.add_line_items_from_cart(@cart) |
| |
| respond_to do |format| |
| if @order.save |
| Cart.destroy(session[:cart_id]) |
| session[:cart_id] = nil |
| ChargeOrderJob.perform_later(@order,pay_type_params.to_h) |
» | format.html { redirect_to store_index_url(locale: I18n.locale), |
» | notice: I18n.t('.thanks') } |
| format.json { render :show, status: :created, |
| location: @order } |
| else |
| format.html { render :new } |
| format.json { render json: @order.errors, |
| status: :unprocessable_entity } |
| end |
| end |
| end |
Next, we adjust the test to match:
| test "should create order" do |
| assert_difference('Order.count') do |
| post orders_url, params: { order: { address: @order.address, |
| email: @order.email, name: @order.name, |
| pay_type: @order.pay_type } } |
| end |
| |
» | assert_redirected_to store_index_url(locale: 'en') |
| end |
Finally, we provide the translations:
| en: |
| |
| thanks: "Thank you for your order" |
| es: |
| |
| thanks: "Gracias por su pedido" |
See the cheery message in the next screenshot.
3.16.29.209