When we started working on our community translation project for Discover Meteor (our book about the Meteor JavaScript framework) a few weeks ago, we never expected we would get such a huge response.

Barely 24 hours after the initial announcement, we already had 25 people contributing to 14 different translations.

The first 14 translation projects.

Managing 14 ongoing translation projects – with more coming soon – is not an easy task. So it’s a good thing we put in some time beforehand to try and automate as much of the process as we could.

Of course, we could’ve just built an app just to manage the translation efforts, or maybe modified the one we’re already using to host the book itself. But that would take time, and at that point we didn’t even know if anybody would show up.

We’re big believers in not reinventing the wheel if you can avoid it. So rather than start a long development process from scratch, we decided to reuse what we already had and get by with a minimum amount of coding.

Defining Our Requirements

The workflow we came up with involves GitHub, Middleman, Codeship, and Heroku, and fits our three requirements:

  • Scalable: having more languages should not require more of our time.
  • Automated: things should run fine even without our direct involvement.
  • Free: adding languages should not add any extra costs to our setup.

It’s actually pretty amazing to think that we now have tools that are not only powerful on their own, but can also interact with one another to form complex workflows… and all that for free!

First, here’s an overview of what the whole thing looks like:

The completed workflow.

Read on to learn how exactly it all fits together.

Collaborating With GitHub

The first step was finding a way for people to actually collaborate on the translations.

This was easy enough: since our book uses Markdown files as sources and is versioned with Git, we simply decided to use GitHub to manage the translations.

Not only does GitHub have a great user interface and online editing features (perfect for those quick typo fixes), but it also has good team and permissions management.

Managing members and permissions for the Italian team.

Of course, the problem is that while Markdown is great for writing and editing, it doesn’t make for very compelling reading. This is where Middleman comes in. Middleman is a powerful Ruby static site generator, and it’s also the app we’re using to generate this very site (I previously covered three Middleman hacks used here).

With our Middleman wrapper, we were now able to generate a nice HTML site for each of our languages. There was just two problems:

  • We didn’t want to have one Middleman repo per language. Even using Git, it would be a pain to keep all repos updated.
  • We wanted each language’s repo to contain only Markdown files; not templates, stylesheets, or JavaScript files.

More Middleman Magic

We ended up doing a lot more cool stuff with Middleman, such as:

  • Internationalizing our UI strings.
  • Calculating each translation’s progress.
  • Creating static APIs for each language.

I’ll cover all those plus a few other nifty static site generators tricks in an upcoming blog post. If you’re interested, sign up to our newsletter to make sure you don’t miss it!

Introducing Git Submodules

Git Submodules have a bad rap, and maybe deservedly so. They’re confusing at first, and it takes a bit of practice to get them to behave.

But in our case, they ended up being just what we needed. We decided to use a single Middleman repo, and include each translation repo as a submodule of that central repo.

Listing submodules in GitHub.

With a single repo, we didn’t have to worry about keeping things in sync anymore.

Thinking About The Environment

We had decided each translation would be available at http://es.discovermeteor.com, http://zh.discovermeteor.com, http://ru.discovermeteor.com, and so on.

So now that we were including every language in a single repo, we needed a way to tell Middleman to use the Markdown files corresponding to the right language.

If this was a classic PHP or Rails app, we could’ve simply passed the app a URL parameter. But of course, this isn’t possible with our static site’s dumb HTML files. This means we had to tell Middleman which language to pick before it generated each site.

We did this by setting an environment variable on each server. With Heroku, it’s as simple as:

heroku config:set LANGUAGE=ru --app ru-discover-meteor

We could now access this environment variable from within Middleman’s config.rb file to tell it which language to use as source for the current instance:

activate :blog do |blog|
  blog.sources = "chapters/"+ENV['LANG']+"/:title.html"
  blog.permalink = "chapters/{slug}"

Heroku To The Rescue

All that was left was finding a place to host all these translations. We chose Heroku for two big reasons:

  • It has a free plan that’s more than enough to host plain old HTML files.
  • It has a command line utility, which means the whole process of creating a new instance can be automated with a simple bash script.
Creating Heroku instances for each translations.

Here’s what that script looks like, using GitHub’s hub command line utility to automate creating a new language repo:


# cd into working directory
cd ~/Dev

# clone the outline of the book into a local repo
git clone git@github.com:DiscoverMeteor/DiscoverMeteor_Outline.git DiscoverMeteor_$1
cd DiscoverMeteor_$1

# use hub to create a new GitHub repo and push to it
hub create DiscoverMeteor/DiscoverMeteor_$1
git push origin master

# cd into Middleman app directory
cd ~/Dev/DiscoverMeteorStatic

# Create new Heroku app and add Git remote
heroku create $1-discover-meteor
git remote add $1 git@heroku.com:discover-meteor-$1.git

# Add Git submodule for the new language
git submodule add git://github.com/DiscoverMeteor/DiscoverMeteor_$1.git source/chapters/$1

# Add new submodule, commit, and push
git add -A
git commit -m 'added language: $1'
git push $1 master

# Set domain and LANGUAGE environment variable
heroku domains:add $1.discovermeteor.com --app $1-discover-meteor
heroku config:set LANGUAGE=$1 --app $1-discover-meteor

So adding (for example) Arabic as a new language is as easy as sh create_lang.sh ar.


But there was a sticking point in our workflow: we still needed to push every single update to each of our 14 instances manually.

Wouldn’t it be great if there was an app that could watch a GitHub repo, and redeploy all instances every time that repo changed?

Well it turns out that’s exactly what Codeship does. Through the Heroku API, Codeship lets you specify any number of Heroku apps to which to push to when a repo receives new commits. And the best part is that they have a free Heroku add-on that makes it very easy to get started.

The Codeship build and deploy process.

This is not only a huge time-saver, but Codeship’s email alerts and logs make it easy to see when a build fails for a specific language.

Tying It All Together With Webhooks

There was just one more thing we could do better: because of the way Git submodules work, we still had to manually git pull any submodule changes and commit them into the DiscoverMeteorStatic repo before we could push it back up to GitHub and trigger the Codeship build process.

So we built a custom Rack app to receive git push webhooks from each of the translation repositories on GitHub, and then in turn create a new commit into the DiscoverMeteorStatic repo.

Making It Work

So here’s what adding a new language typically involves:

  1. Run sh create_language.sh xy
  2. Add people to the “xy” GitHub Team.
  3. ?
  4. Profit!

Thanks to a bit of upfront automation, we’ve reduced a fairly complex workflow to two steps!

The point of this article is not so much teaching to replicate our workflow (although you’re of course welcome to copy it), but showing how powerful basic Git/GitHub/Heroku literacy can be, even without writing full-fledged apps.

So whether or not you consider yourself a programmer, I would definitely encourage you to play around with any new services you come across, and to keep thinking of new ways of making them work together!