*This is the second part of The Maintanable Spree series. [In the first article we are focusing on Command Pattern, read it here.*
Most e-commerce solutions, if they exist long enough, reach a point when their makers start considering expansion beyond their native, single-country market. These thoughts may find their origin in many different stages of shop's existence and under multiple circumstances. In some cases single country customers' needs prove to be insufficient to fuel company's growth anymore. In others, the store could be designed from the ground up with multiple languages and currencies in mind. One day however, the decision is made. And it's usually: "We're going international".
The international market is a great opportunity. It can supply booming e-commerce solution with masses upon masses of eager customers. Nowadays this is especially true, since conducting business across borders has never been easier, cheaper or faster. Shipping companies offer competitively priced services to deliver your packages to anywhere in the world.
However, international market is also a challenge. With so many online stores available, it takes a superior user experience to attract customers to a particular one and keep them there. Many factors come to play in that experience, but one of the most important ones is availability in the customer's preferred language. When user is not able to understand the interface, the confusion ensues, which is an obstacle for a pleasant experience.
Supporting multiple languages is non-optional for international stores that aim for the best user experience. Fortunately, modern e-commerce solutions, such as Spree, allow us to do just that, while also not introducing too much overhead to the store's administrators. Here's how.
As you can read in its [documentation](https://guides.spreecommerce.org/developer/customization/i18n.html), Spree comes with English as a default language, but you can easily extend it to support more languages using extensions. To be specific, `spree_i18n` and `spree_globalize`. Let's have a look at them now.
Extension based on [rails-i18n](https://github.com/svenfuchs/rails-i18n) project, aiming to provide translations based on YAML config files. Out-of-the-box it comes with a large repository of transatable terms that find use in every online store, like like address information, products information and interface elements. The list of translations can be very easily extended, by simply adding YAML key-value pairs in the translation files. The usage is also extremely straight-forward, and very familiar if you've ever translated any Rails application.
Let's say we have two locales enabled, `en` for English and `pl` for Polish. In that situation we'd also have two YAML config files: `config/locales/en.yml` and `config/locales/pl.yml`. Their content would look a lot like this:
We can access those translations in the view by using just calling `Spree.t(:product)`. From the documentation:
> The `Spree.t()` helper method looks up the currently configured locale and retrieves the translated value from the relevant locale YAML file. Assuming a default locale, this translation would be fetched from the en translations collated from the application, `spree_i18n` and `rails-i18n`.
Extension based on [Globalize](https://github.com/globalize/globalize) gem for Rails. It enables translations of model data, especially Spree built-ins, like `Product` or `OptionType`. It requires very little interaction to activate. Add it to your Gemfile, configure supported locales and you're good to go: Spree models are enriched with a convenient link for their translations UI, fully manageable by the store administrator.
These two extensions that I mentioned can, and usually are, used simultaneously, since they serve similar, but ultimately different purposes.
Spree I18n works great for translating interface elements that are not part of any model. It does not require any modifications to the database, since it keeps translations in config files. But it also means that, for every new translation, source files need to be updated, which is not comfortable for things that are updated often and with no interaction from the developer, like products. Things like buttons, notices, popups are great examples where it shines.
Spree Globalize, on the other hand, is perfect for translating models and their attributes. It offers store administrators a handy UI for managing translations and does not require changing the source files to do that. But, for every entity it serves, it requires a separate database table. This is a big overhead when you want to translate things like buttons or popups.
One limitation of Spree Globalize is that it was meant first and foremost for built-in Spree resources. However, with a little bit of work we can extend it to serve custom models that might come into existence over the course of application development, that were not foreseen by Spree creators.
This story is actually based on my experience from some time ago, when I was tasked with adding a new, translatable model to our application. For maximal clarity I set up another Spree instance and replicated the whole process, while also noting down my actions here.
To replicate these steps you need to install and enable Spree Globalize extension. Follow the [documentation](https://github.com/spree-contrib/spree_globalize) to do that.
Let's say that we want to add a new, translatable entity to our application. A notification, that can be displayed for customers in their respective languages. Store administrators should be able to manage notifications with the same set of CRUD operations that they use for managing products.
We start by creating the Rails model. The notification will have title and content and both these attributes will be translatable.
The first code decision that needs to be made is whether to put your new model inside the Spree module, ending up with `Spree::Notification` or not, which gives us just `Notification`. As you can see I went with the latter. It's really a matter of convention. Pick one and stick with it for all models.
I mention that because both of these options resulted in some different technical problems for me. I will mention the both as we go, so you'll have a solid base to build on in case you face them yourself.
We can create the main model and its translations in one migration.
Take notice that we don't put title and content attributes inside the `notifications` table and instead we delegate that to translations table. After running the migration these lines have been added to the database schema:
We can confirm that notification model has been created together with its translations using the Rails console:
If you put your model inside the Spree module you might stumble upon the following behavior when you try to create a new notification:
Notification's translation should reference its parent by `spree_notification_id` attribute, but here it tries to do this by just `notification_id`. Obviously, there's no such attribute in `Spree::Notification::Translation`, hence the exception.
I spent some time to find the root cause of this behavior, but ultimately I was unable to. I managed, however, to work around this issue with a simple hack. I simply aliased `notification_id`, that's assigned to during the translation's creation to `spree_notification_id`, which is an actual translation attribute name. I put a small monkey-patch in `app/models/spree/notification/translation_decorator.rb`:
After that it works just fine. You can validate it by creating a notification in the Rails console:
If you decided to put model outside of Spree module you should not have this problem.
Once the model is ready we need to create the controller to manage our new translatables. Fortunately, Spree does most of the work here for us. It offers programmers `Spree::Admin::ResourceController` which, with slight customizations, can serve pretty much any RESTful resources. In most cases all we need to do is define the attributes that are available for mass assignment (by default all of them are, so not very safe) and write views for the new resource. This is the controller part:
Most actions, except `index` are defined in the `ResourceController`. It also takes care of creating or finding appropriate resources before performing any operations on them. Check out its [source code](https://github.com/spree/spree/blob/3-7-stable/backend/app/controllers/spree/admin/resource_controller.rb), it's really clever and saves a lot of work. Because we keep Notification outside of Spree module we also need to explicitly tell the controller what is it's resource model class.
## Router
Now that we have controller set up, we can define routes leading to it, so that users' requests are actually able to reach it. I put this in my routes file:
Since Spree routes are mounted at the root of the application, this entry will hand requests to `/admin/notifications` over to `Spree::Admin::NotificationsController`. Which is exatcly what we want.
If you, like me, didn't wrap your model class in Spree module you need to patch Spree's translations controller a bit. When you take a look at its [source](https://github.com/spree-contrib/spree_globalize/blob/master/app/ controllers/spree/admin/translations_controller.rb) you'll notice that it's tailored to work with Spree models. Even though we can reference translatables inside translation views via names like `@option_type`, for their model class names the controller transforms them to something like `OptionType` and prefixes with `Spree::`. We can make an exception in our case:
I put that in `app/controllers/spree/admin/translations_controller_decorator.rb`. Now the translation views that we'll write in a moment will work just fine.
Again, we're mostly basing on pre-existing Spree structures. For the index view we'll supply our own content for page title, page actions and our custom notifications listing.
Links for editing, deleting and translating are placed next to every notification entry. This, of course, is a very simple, proof-of-concept view that can be extended and prettified in real world use cases. It is fully functional though, so we'll just roll with it here.
In a similar fashion we create views for creating and editing notifications.
The central point in both of these views is a form in which we can define notification's attributes. I extracted it to a partial:
This set of views is enough to create, edit and delete a simple resource. We still need a way to manage its translations though. Fortunately, Spree takes care of most of that as well. But before that, let's go ahead and create a notification. Just go to `/admin/notifications` and click on a `New` button in the top right corner. You should be taken to a form like this:
Fill in the fields and create the notification. Now we can move on to translations.
A part of work left for us to do in addition to that performed by Spree consists of supplying our translation page with title and action buttons. I put that in `app/views/spree/admin/translations/notification.html.erb` file:
That's actually it. From now on, when you click a globe button next to the notification entry you'll be taken to the translation page where you can configure all translatable attributes for every enabled locale. The result, for the amount of code that we wrote, is pretty compelling:
I added a simple div on the store's home page to demonstrate what we've accomplished. It's something along these lines:
Notice how we don't care about locale at all. Despite that, when we navigate to the site's root with different language settings we get expected results:
With relatively little work we've managed to bring a comfortable way of managing translatable resources to our application. Customers are happy to see the store in their preferred language. Administrators are happy that in a few simple steps they can add translations to new and existing models. We, as developers, are happy that everything works and the translation process does not require further changes to the source code.
I wouldn't go as far as to say that internationalization is a cure for every e-commerce problems. But working internationalization sure makes everybody involved happier.
Leave your information below: