Progressive Ember - Web Manifest

A walk through the real effort to transition an enterprise level Ember application to a progressive web application.

Part two: Adding a web manifest

What is a web manifest?

It’s metadata that describes your web application when it’s installed by your users. There’s a great overview here.

ember-web-app

There is a great addon in the Ember ecosystem, ember-web-app that can take care of this for you.

At the time of this post, this addon does not support localization, which my application requires. Meaning I don’t want to deliver an English manifest to my Italian speaking users. I opened a feature request issue; if anyone would like to collaborate on building that functionality, let me know. But as we’ll see, it’s a little more involved.

Localization

In the case of my application, there are four attributes that I need localized:

Users of my application also have the ability to switch languages, and a very few of them switch languages frequently.

I use ember-intl as my localization tool of choice, but ember-i18n is also a good option and well maintained. However do be mindful that ember-i18n may deprecate in favor of ember-intl.

The end objective is that in the <head> of my application, I have the following:

<link rel="manifest" href="path/to/manifest.json">

Let’s break this down a bit. I need a manifest that can be localized for 5 languages. The web app manifest specification doesn’t allow for this, so I will either need the text to change at run-time as users switch languages or 5 different manifest files. The latter is much simpler, so let’s start there:

<link rel="manifest" href="path/to/en-us/manifest.json">
<link rel="manifest" href="path/to/es-es/manifest.json">
<link rel="manifest" href="path/to/pt-br/manifest.json">
<link rel="manifest" href="path/to/it-it/manifest.json">
<link rel="manifest" href="path/to/fr-fr/manifest.json">

We can only provide one of these links on the page at a time (I believe Chrome would pick the first one and discard the rest). Luckily I’m already using ember-cli-ifa to resolve localized assets from my asset map already. So my result is now:

<link rel="manifest" href={{asset-map (concat "manifest/" user.locale "/manifest.json")}}>

The problem here is that index.html does not know what asset-map is nor does it know about my user’s language. So what I might need here is ember-cli-head, which provides a way to inject content into the <head> at my application route and at other run time events that I might care about. What I end up with is something like:

import Route from '@ember/routing/route';
import { get } from '@ember/object';
import { inject as service } from '@ember/service';

export default Route.extend({
  intl: service(),
  headData: service(),

  afterModel() {
    set(this, 'headData.locale', get(this, 'intl.locale'));
    set(this, 'headData.description', get(this, 'intl').t('manifest.description'));
  }

  ...

I move my <link> in to ember-cli-head’s head.hbs template (note the model change):

<link rel="manifest" href={{asset-map (concat "manifest/" model.locale "/manifest.json")}}>

So on boot, after my user’s language is know, I can set the locale of my user and the correct localized asset will resolve. When my user’s switch their language, they’ll get the updated manifest.

This is going to come back to haunt me when I add a service worker, but more on that later.

Translation workflow

What further complicates this is that I want the content of my web manifest to live in the context of my existing translation workflow. Our application has a lot of content in it, that syncs with Crowdin.

I have the manifest content that I want translated in our ember-intl translation files and it gets translated by our translation team. But I don’t want developers to have to copy and paste content if it changes into 5 different web manifest files. So to try and help facilitate this, we have an in-repo addon that hooks in to our translation workflow. The gist of which is:

function localizeWebManifest(locale, path) {
  var translations = fs.readFileSync(path);
  var sourceJson = JSON.parse(translations);

  var manifestPath = 'manifest/' + locale + '/manifest.json';
  var manifest = fs.readFileSync(manifestPath);
  var manifestJson = JSON.parse(manifest);

  manifestJson.name = sourceJson.manifest.name;
  manifestJson.short_name = sourceJson.manifest.short_name;
  manifestJson.description = sourceJson.manifest.description;

  fs.writeFileSync(manifestPath, JSON.stringify(manifestJson, null, 2), function (err) {
    if (err) {
      return console.log(err);
    }
  });
}

For each locale we support, we update our manifest files with the content from our ember-intl JSON files. Like I said earlier, there are a lot of moving parts here and it would be great if there was a way to contribute this back to ember-web-app.

Results

Before: Baseline Lighthouse

After: Web Manifest Lighthouse

note: the big dip in performance is related to Lighthouse switching major versions, not adding a web manifest.

Our scores went up across the board, which is terrific. That performance number is still terrible though, so let’s see what it takes to slim down our build size.

Keep reading