Iteration K3: Translating Checkout

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&oacute;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.

images/u_1_checkout_translated.png

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:

images/u_2_checkout_errors.png
 es:
 
  activerecord:
  errors:
  messages:
  inclusion: ​"​​no​ ​est&aacute;​ ​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.

images/u_3_checkout_errors_better.png

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&oacute;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.

images/u_4_checkout_errors_fixed.png

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.

images/u_5_gracias.png
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.16.29.209