Lot’s of people, myself included, have been extolling the virtues of using a JavaScript MVC framework, instead of a traditional PHP templating system as a WordPress front-end. While I’m pretty down on how WordPress traditionally handles templating, the one thing it does well is translation strings. WordPress is full of great functions for making text translatable.
While angular-translate and other modules are available for localization, they all feel clunky and redundant when WordPress already has great localization built in. In this article, I want to share a simple way to use WordPress to manage localization and avoid using untranslatable strings in your Angular templates. It’s based on how we solved the problem in Ingot.
In Practice
For Ingot, my A/B testing plugin, we used a single page web app, powered by AngularJS and the WordPress REST API for the admin page. I’m very happy with this way of working and will use it in the future. Roy Sivan, who worked with me on the UI for Ingot has a great post on Torque about using Angular and the REST API for plugin admin screens that you should check out if this sort of idea excites you.
The one thing that started out as a bit of a pain was getting translation strings, managed by WordPress, into Angular templates. In our first version, I’m ashamed to admit we had a lot of untranslatable strings.
Setting Up
As a proof of concept, I made a simple plugin that adds a shortcode that used Angular and the REST API to show 10 posts. In the template file, I used a mix of content from the posts and string like “Title” and “Author” that can be translated by WordPress.
The setup is fairly simple. My plugin has a function to enqueue the JavaScript files for Angular and my Angular app. The file for the app, uses wp_localize_script() to print a JavaScript variable to the screen with data I need in the app, including translations, as well as the REST API URL and nonce.
The function wp_localize_script() was created for the purpose of passing translation strings into JavaScript. It’s also been used a lot for getting other dynamic data, like URLs for API endpoints into the DOM. Here is how I used it:
define( 'ANGTREX_URL', plugin_dir_url( __FILE__ ) ); define( 'ANGTREX_DIR', dirname( __FILE__ ) ); add_action( 'wp_enqueue_scripts', function(){ wp_enqueue_script( 'angular', '//cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.8/angular.min.js' ); wp_enqueue_script( 'angular-resource', '//ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular-resource.js', [ 'angular' ] ); wp_enqueue_script( 'angular-trans-exp', ANGTREX_URL . 'app.js', [ 'angular' ] ); wp_localize_script('angular-trans-exp', 'ANGTREX', [ 'api_url' => esc_url_raw( rest_url() ), 'rest_nonce' => wp_create_nonce( 'rest_api' ), 'translations' => [ 'title' => esc_html__( 'Title', 'angtrex' ), 'author' => esc_html__( 'Author', 'angtrex' ), 'view' => esc_html__( 'Read More', 'angtrex' ) ] ] ); });
As you can see the object being localized is called ANGTREX. I have an index called translations, with three strings. These strings are escaped, and made translation ready using the function esc_html__().
Nothing about this is unique or new, which is what I like about it. Everything is very standard.
I then created the HTML for a very simple Angular app inside of a standard shortcode function:
add_shortcode( 'angtrex', 'angtrex' ); function angtrex(){ ob_start(); ?> <div ng-app="angtrex"> <div ng-controller="ListController"> <div ng-repeater="post in posts track by $index"> <h1> {{translations.title}} : {{post.title.rendered}} </h1> <p> {{translations.author}} : {{post.author}} </p> <p> {{post.content.rendered}} </p> </div> </div> </div> <?php return ob_get_clean(); }
As you can see, the template uses tags that call the post and data from “translations.” Getting from the localized variable to the template and making it universally available, to all templates, and all controllers was the challenge.
Alternative Solutions
In my example code, I only have one controller, so the objections I’m about to state to the two options I considered are not very relevant. But in a real-world scenario, with lots of controllers, which may be changing over time, I think my objections stand.
Before settling on my solution, I first considered making translations an Angular service. That made sense at first. But then I would have to inject that service into every single controller. Not that, I’d have to then set $scope.translations equal to that service.
That’s a pain. It would be much easier, in each controller, to just do:
$scope.translations = ANGTREX.translations;
Again that works, but it’s not very DRY. It’s prone to errors. You could forget to do so in a controller or someone could remove that line later on. Also, if I change the source for translations later, it’s a big refactor.
The rootScope Solution
In every Angular controller, the $scope variable is unique to the scope of that controller. That’s the point. But, Angular also has $rootScope, which is a global scope. I’m injecting it into all of my modules already, so I decided to use that.
Yes, it’s important not to pollute rootScope with lots of extra stuff. But in this case, we are talking about something that is needed in every controller and is already in a variable with window scope anyway.
To make this work, I used .run() when I created my main app module. As you can see here:
angtrex.app = angular.module( 'angtrex', [ 'ngResource' ] ).run(function($rootScope) { //add translations to route scope $rootScope.translations = ANGTREX.translations; });
In the callback for .run(), I added translations to $rootScope. It’s now available in all of my controllers.
Go JavaScript MVC, But Stay Accessible
There is no doubt that Angular and other JavaScript MVC frameworks make a great user experience. But creating a great experience for English speakers with normal sight should not be done at the expense of others.
Translation-friendliness is an important step in accessibility, but it’s not the only one. Highly dynamic interfaces can be very difficult for those using screen readers unless ARIA tags are used properly.
I’m all for the sudden popularity of highly dynamic, JavaScript-powered WordPress interfaces. But, we need to remember the accessibility concerns. I hope that this article has helped you avoid allowing internationalization to be a barrier.
No Comments