I18n in Expo and React Native

Apparently, the whole world doesn’t speak english! Here’s how to add i18n support to your react native project. This article specifically addresses expo, since we’ll be working around i18n tools requiring react-native link.

We’re working on WooToApp, which is a native app builder for WooCommerce Stores. It now features internationalisation and language switching (of course!).

Add the i18n package.

The package is at https://github.com/AlexanderZaytsev/react-native-i18n if you’d like a read. There’s not much to it.

yarn add react-native-i18n or npm install react-native-i18n –save

In App.js we need two things done – load the translations, and load the user device locale.

Load the locale and the translations.

If you’re caching assets during app load (a common pattern for expo), add a promise to grab the device locale and store it in I18n.locale to your load routine.

While we’re here, we’ll define a few basic translations for our testing (down the track, you probably want to fetch() these from your server…). I’ve used Google Translate for this, which will do fine for the initial work of internationalisation.

const localeSet = Expo.Util.getCurrentLocaleAsync().then(function (r) {
  I18n.fallbacks = true;
  I18n.translations = {
    en: {
      pay: 'Pay eng',
      choosePaymentMethod: "Choose a payment method."
    },
    fr_FR: {
      pay: 'Payer',
      choosePaymentMethod: "Choisissez une méthode de paiement."
    },
  };
  I18n.locale = r;
  console.log(r);
  return r;
});

// load fonts, load business rules, load locale now.

Promise.all([...fonts, ...[business, localeSet]])

If you don’t have a preload routine like the above, you should make sure you’ve read and set the locale before outputting any strings.

Note that we’ve made use of a built in expo function to grab the users’ current locale. Expo.Util.getCurrentLocaleAsync() returns the users current locale, e.g. en_AU, fr_FR.
This function was previously Exponent.Util.getCurrentLocaleAsync().

Now we’ve worked out the users’ current locale, assigned it to the I18n library, and added some default translations. Note we’re also logging out the locale so you can check it out while debugging.

The next step is to translate a string in a component in your app – then you can continue internationalising the rest!

Translate strings in a component.

Open your component and import the I18n library.

import I18n from 'react-native-i18n';

Use a string translation you added above. Reload your app, and enjoy the translated goodness.

<Text>{I18n.t('choosePaymentMethod')}</Text>

Switching device locales (Android)

I used a french locale for my local device testing while writing this article.

To get to the locale switcher, I had to navigate:

Settings > General Management > Language & Input > Language. I added French (France) as a language and set that to my current.

It’ll help to keep that settings page open in your app drawer, or you’ll be navigating through a French menu to change it back.

React Native – using Flow

I’m backporting Flow into a large React Native project of mine. It’s unclear sometimes how to do things ‘the right way’ with Flow.

This is what’s working so far:

  • Any type that is used across multiple components should exist in it’s own JS file, and be imported as a type. Here’s how that looks in the component (e.g. follow the Order type from import. We expect to store an array of Order in this.state.orders.
  • Occasionally, rigorously defining a type just isn’t worth stuffing around with. For example, the navigation object passed around with react-navigation (via this.props.navigation. etc).
    I receive no real benefit from having this correctly typed. It’s not increasing visibility of my code or re-use, it’s not cleaning up bugs. After a quick google showed it’d be a battle, I settled that props.navigation is going to be of type any. *This is the only case of this. I might circle back and fix it later* 

    Here’s what that little cheat looks like:

    I should emphasise again, if you find yourself doing this often, you’re missing the point.

  • Passing functions in as props is a common pattern in React. Strongly typing these will quickly shrink your surface area for bugs (It’s amazing the bugs you find when backporting Flow into an existing project :D)

React Native: Testing with Jest

It’s a painful road getting an existing project set up with tests. An existing React Native app of ours, out in the wild, is getting retrofitted with unit tests.

The great part is, I know what to test. I know what type of bugs have been introduced in the past and what we’re likely to break again (exactly what the tests are for…)

“it renders without crashing”.

The out of the box test ensures your app renders without crashing. It’s also where you get to find out about any of your libraries that are incompatible with jest! For me, this was ‘sentry-expo’.

Simply updating to the most recent expo along the way had me on my way again.

 

import React from 'react';
import App from './App';

import renderer from 'react-test-renderer';

it('renders without crashing', () => {
  const rendered = renderer.create().toJSON();
  expect(rendered).toBeTruthy();
  expect(rendered).toMatchSnapshot();
});

Note we’ve added an extra test (shown in bold) to take a DOM snapshot and check it against previous snapshots.

If the initial DOM tree changes in any way to the stored snapshot, the test will fail. This is a great way to capture unintended side-effects and loading issues. Once you’ve investigated the change (or pre-empted it..) you can tell the testrunner to update the snapshot for the future.

We keep “npm run test” alive in the background to see the tests constantly run as I make code changes, add new tests, and modify existing tests.

Testing a real component

Our retail price component accepts either a product or a price prop, and decides a price to show the user based on this. We also show the currency code decided by the app owner.

The component is in “components/Price.js”, so if we create a new file called “components/Price.test.js”, jest will find it and add it to the test suite automatically.

 <Price price='9.99'/>

or

 <Price product={{price: 101}}/>

Our first test creates a component with price of 9.99 and checks the state has been set correctly.

 

import React from 'react';
import renderer from 'react-test-renderer';

// the component in test
import Price from './Price';
// the global business settings (like currency symbol)
import Business from "../skin/business";


it('can-show-prices', () => {
 let pricetag = renderer.create(<Price price='9.99'/>);
 
// check the Text component (with class 'actualPrice') contains $9.99
 let pricetagroot = pricetag.root;
 expect(pricetagroot.findByProps({className: "actualPrice"}).props.children).toEqual(["$", "9.99"]);
 
// check the snapshot matches, and make sure the price makes its way to state properly
 let pricetagTree = pricetag.toTree()
 expect(pricetagTree).toMatchSnapshot();
 expect(pricetagTree.props.price).toBe('9.99');
 expect(pricetagTree.instance.state.price).toBe('9.99');

// Let's change the currency symbol for the app, and check it propagates to a component.
 Business.business.currency_symbol = "@";
 
// Pass in an object instead of a value.
 pricetag = renderer.create(<Price product={{price: 101}}/>);
 
// Make sure the new currency symbol and the price finds its way to the display (also, number formatting!)
 pricetagroot = pricetag.root;
 expect(pricetagroot.findByProps({className: "actualPrice"}).props.children).toEqual(["@", "101.00"]);
 
// Check the snapshot and that the price propagated correctly.
 pricetagTree = pricetag.toTree();
 expect(pricetagTree).toMatchSnapshot();
 expect(pricetagTree.instance.state.product.price).toBe(101);

});

Testing an external HTTP request

Testing an external HTTP request is strictly an integration test, not a unit test. It’s generally frowned upon to include ajax requests in your test suite, because you need to test your current code is working (the rest of the pipeline is a different problem, strictly).

However, an integration test that includes some end to end components holds a huge amount of value – knowing an API endpoint is still up and responding, authenticating properly, and that little used areas of your app are running as they should.

In our React Native context, calling an endpoint using our api.js file ensures that the current API adapter, as is in the codebase, can authenticate a user or initiate a payment.

To get ajax working correctly in jest, we need to require xhr2 first. Our underlying API adapter uses fetch(), which isn’t defined in jest.

const XMLHttpRequest = require('xhr2');

global.XMLHttpRequest = XMLHttpRequest;

Next up, we call the endpoint and investigate the response. This test calls the endpoint with invalid data, and ensures that the request fails (just as important as making sure a valid request passes!)

it("Should throw errors on incomplete request.", (done) => {
    return Woo.customers.authenticate("[email protected]").then( data => {
        expect(data).toEqual(expect.objectContaining({result:false,user:null,errors:expect.anything()}));
        expect(data.errors).toEqual(expect.objectContaining({invalid_username: expect.anything()}));
        done();
    }).catch(data => {
        console.log(data);
        done.fail();
    });
});

Woo.customers.authenticate is the API request exactly as we make it in the React Native app, using the same included JS file.

We interrogate the JSON response – the first expect ensures we receive an object with result set to false, user set to null, and an errors object (with anything in it).

The second expect ensures that the errors object has a property “invalid_username“.

We call ‘done()’ to let the test container know we’re finished (since we’re buried in a promise) – done() is a callback passed in to it().

More Jest & React Native testing snippets

It can be helpful to see some real world examples of tests – especially in jest I find the documentation a bit light on ‘real world’ tests. Here’s some Jest snippets from a React Native project:

Make sure string matches in JSON response.

expect(data.email).toEqual("[email protected]");

Make sure the Header component rendered with these props matches the last time it passed successfully.

let header = renderer.create(<Header title='test 234' showHamburger={true} isSearch={true} showCart={true} />);
expect(header).toMatchSnapshot();

Make sure the data object returned has a ‘liveMode’ property (which can be anything)

expect(data).toEqual(expect.objectContaining({liveMode: expect.anything()}));

Render a component. Then pass new props to component, and check the props rendered correctly as per last time:

let headerRenderer = renderer.create(<Header showHamburger={true} isSearch={false} showBackButton={true} showCart={true}/>);
headerRenderer.update(<Header title={"RHYS"} showHamburger={true} isSearch={false} showBackButton={true} showCart={true}/>);


let headerJSON = headerRenderer.toJSON();
expect(headerJSON).toMatchSnapshot();

Good luck!

 

WooCommerce: To perform the requested action, WordPress needs to access your web server

Seeing this error? “WooCommerce: To perform the requested action, WordPress needs to access your web server”

This permissions issue haunts me after every new WordPress install. There’s a one-liner fix that doesn’t involve 777’ing your whole server.

 

Here’s the magic cure:

  1. Open wp-config.php
  2. Add
    define( 'FS_METHOD', 'direct' );

    at the end

  3. Save and close, install plugins!

Thanks to https://wordpress.stackexchange.com/questions/228591/to-perform-the-requested-action-wordpress-needs-to-access-your-web-server-pleas

Fixing react native header bar (and status bar) on android and iOS.

The header bar in a new react native or expo project will either show as plain white in the background or eat up a good chunk of your app header area. This easy solution will pad out the header component by the height of the status bar, so it integrates nicely up top.

First up, import this helpful library

import {getStatusBarHeight} from 'react-native-status-bar-height';

Set up a stylesheet for the container and the header

const styles = StyleSheet.create({
    container: {
        flex: 1,
    },
    header: {
        paddingTop: getStatusBarHeight(),
        height: 54 + getStatusBarHeight(),
    },
});

Now bind it to your Container and Header elements.

render() {

        return (<Container style={styles.container}>
            <Header hasTabs style={styles.header}>    
/* snip */

GuzzleClientException truncated

By default, a Guzzle error thrown in Laravel will truncate the response body. When you’re deep in an API integration, this is a productivity killer.

Here’s an easy way to error log the full response body from the API.

try {

$response = $client->request( "POST", $this::$resource, [ 'json' => $item ] );

$body = json_decode( $response->getBody()->getContents() );

return $body;
} catch ( \GuzzleHttp\Exception\ClientException $e) {
// here's the good stuff
Log::error($e->getResponse()->getBody()->getContents());
throw $e;

}

Code Workshop is locking in ReactJS.

There’s a common complaint among the javascript ecosystem, from new developers and seasoned developers alike. We’re all exhausted from learning, building with, and then maintaining the new hot framework of the day. Below, we’ve detailed how we decided to lock in ReactJS.

New JS Frameworks.. We're locking in ReactJS.
From https://www.exceptionnotfound.net/wait-pick-learn-ignore-dealing-with-javascript-framework-fatigue/

Who are we?

I’m Rhys Williams. I run Code Workshop, a Sydney app development company. We’re small sized – I run the business by myself from day to day and do the lion’s share of development and client relations. We’ve got tight relationships with a tiny army of Sydney based contractors who look after our UI/UX, WordPress development and QA process.

We take on 2-3 decent sized client projects per year which are either completely written in javascript or some sprinkling on an otherwise laravel back-end application. In the last 5 years, this has been jQuery, AngularJS, VueJS, and most recently ReactJS.

For mobile app development we’ve had similar framework churn to follow the times. Some jQuery, PhoneGap, Ionic, and recently ReactNative.

We’re locking in ReactJS and React Native until 2023.

Now, in 2018, we’re making a call. JS framework churn is over. The toolset has matured and ReactJS won. The toolchain is still evolving (somewhat frustratingly.. Like removing propTypes) but for the next 5 years all of our client side development will be ReactJS/React Native.

We’ve worked on successful ReactJS projects for our eCommerce and traditional business clients in the last 12 months. Supporting the apps is easy, maintaining them is easy, and working on old code is easy.

ReactJS and React Native are well supported and actively developed by huge communities. I suspect it’s a breath of fresh air for developers who’ve followed the same path of us through jQuery, Angular, and a few minor players along the way.

Who uses React Native or ReactJS?

I had a chat with a client last week about who uses React Native, and I was shocked to see just how big the ecosystem has really become.

It’s important to know that some big names use a platform because it means they’re solving huge problems with it successfully. Experimental, toy technologies will often fall to pieces quickly when you use them in anger.

Big apps/projects build using React Native:

Apps using ReactNative

Big websites using ReactJS:

yeah, Netflix uses ReactJS.

This is a much bigger list – ReactJS has been around a few years longer than React Native and has reached something of a saturation point. Here’s a few notable standouts:

  • Yahoo
  • Atlassian
  • KhanAcademy
  • NetFlix
  • New York Times
  • WhatsApp
  • Tesla
  • Visa
  • Spotify
  • Dropbox

Why is it cheaper to build with ReactJS?

Popular frameworks spin out enormously useful and technically amazing resources for developers. React is an open source ecosystem by default, so most of the huge companies listed above also give back to the community.

This typically means large pieces of ReactJS custom code we would otherwise need to write for your app project are written, debugged, QA tested, already in production and open sourced for us to use on your project.

It also means there’s a wealth of technical knowledge and know-how for nearly any problem in the mobile app/web app space. Need a mapping library? Here’s a good one. Images won’t cache properly? Here’s why. There’s a new iOS release? Here’s the permissions updates. The depth of knowledge means problems are solved faster and we’re back to the app build quicker.

React Native is A LOT cheaper.

On the native front, React Native does away with two teams and replaces it with one. It wasn’t so long ago you needed native Android developers and native iOS developers to bring your app to market (unless you used Phonegap, shudder). We develop your app once in React Native. Only building and physical testing is carried out on the two types of devices, which speeds up development time significantly.

React Native also benefits from Expo, a free and open source toolchain to bring your app to market cheaper. Common functionality that is useful for any app development (push notifications, automated build toolchain, filesystem access, social authentication, location awareness, live debugging, sending apps to external parties) is provided out of the box with Expo. Again, this ensures we’re not charging clients to build the same functionality over and over! It’s commoditized code that we don’t need to re-write.

Expo for cross platform native apps

It’d be dishonest to pretend the codebase is 100% out of the box the same between the two platforms with no effort. However, in a recent app build we finished, the code lives in a single repository and differences are handled inline in the code. The future is here, your iOS and Android app can live in one code base. And… it works fine.

The App Store and Google Play both accept React Native apps without a problem, requiring only the typical reviews any app needs.

Talk to us about ReactJS and React Native

We’re locking our business into the React ecosystem for new projects for the next 5 years.

Support and ongoing maintenance are an important part of our business and client relationships, so it’s important to vocally commit and make it clear we’ll be around to support React based projects well into the future.

If you’ve got an app or website project coming up, let us know. We can help you put together a budget, spec out the project, and dive in and build the project when you’re ready. Most importantly, down the track we’ll be around to help with ongoing support, maintenance, and keeping your app or site fresh with the times.

 

 

React Native: Example of full expo app.json

When I first started spending time with React Native, it was often a battle to work out just where and how things were supposed to be formatted in the app.json.

The base file doesn’t include the splash screen, app icon, notification icons, google signin nodes, and nodes required to build for the Play Store and the App Store.

Note the two bundle identifiers should always match (across android and ios). You should increment the 3 version identifiers in a uniform way unless you’re working on separate dedicated teams.

Here’s a lightly modified version of an app.json from a production app we’re working on. I’ll keep adding pieces to this script so it can become a kitchen sink file.

{
  "expo": {
    "sdkVersion": "26.0.0",
    "scheme": "foodorderingapprn",
    "name": "Food Ordering App",
    "version": "0.0.1",
    "orientation": "portrait",
    "slug": "food-ordering-app-rn",
    "icon": "./assets/logo-1024.png",
    "splash": {
      "image": "./assets/splash.png",
      "resizeMode": "cover",
      "backgroundColor": "#deff00"
    },
    "notification": {
      "icon": "./assets/notification-48.png",
      "color": "#deff00"
    },
    "android": {
      "config": {
        "googleSignIn": {
          "apiKey": "AIzaSyDCXXXXXXXPBTDw9-22222vvYQJiE11111",
          "certificateHash": "FFFFFFFFFFFFAA8BA9C47FF23A4E8FBAC5962675"
        },
        "branch": {
          "apiKey": "key_live_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
        }
      },
      "package": "au.com.codeworkshop.food",
      "versionCode": 1,
      "permissions": []
    },
    "ios": {
      "config": {
        "branch": {
          "apiKey": "key_live_hdFKWmm3Yuv7LVjtujrx8knarvj1Yyla"
        }
      },
      "bundleIdentifier": "au.com.codeworkshop.food",
      "buildNumber": "1.0.1"
    },
    "extra": {
      "app_code": "ABCD"
    }
  }
}