DEV Community

Ryan Feigenbaum for Ghost

Posted on

Hacktoberfest with Ghost: Help bring i18n to our app

Ghost is an industry-leading, open-source publishing platform built on Node. Creators (Ali Abdaal, Scatchy, DESK), publishers (The Lever, The Atlantic, The Browser), and companies (Unsplash, DuckDuckGo, OpenAI, Mozilla) use our platform to publish, share, and grow a business around their content.

Because our users are global, better translation support (i18n) has become our most-requested feature. Hacktoberfest seemed like the perfect opportunity to ask the dev community for help in shipping this much-wanted feature.

While developers have contributed to translating individual components in the past, we believe that i18n needs to be solved with a single solution. Therefore, we put together a plan that outlines the requirements for this solution.

i18n Milestone 2: Framework and build tools #15502

This issue represents a single milestone within our i18n project. The full project is documented on the Ghost forum.


Expected structure

  • A new ghost/locales folder in the monorepo contains namespaced JSON files e.g. locales/es/common.json, /ocales/es/portal.json, locales/es/email.json
  • For Node.js/email-templates we do one of:
    • (Preferred) The correct path is loaded from config based on environment
    • ghost/locales gets copied into ghost/core somehow
  • Then for react:
    • Use code-splitting to generate individual versions of portal for each supported locale.

Goals:

  • [ ] Get the basic pieces of i18next in place & configured according to the tech spec
  • [ ] Implement the expected structure (see above)
  • [ ] Figure out if we need to switch to vitejs in Portal to achieve the desired build (may need help from Ghost team)
  • [ ] Have 1 interpolated string in each of email and Portal
  • [ ] Tooling for extracting strings & generating files

Tasks:

  • [ ] Move Portal into the monorepo (Ghost team to complete soon)
  • [ ] Demo/spike of the build tool for Portal - I think this is the hardest part so we should do it first
  • [ ] Demo/spike of how locale files get pulled into Node.js for email templates
  • [ ] Demo/spike of i18next wiring so that there's one interpolated string in Portal and one in an email template
  • [ ] Demo/spike of a tooling that can grab the 2 interpolated strings e.g. yarn translate es would create /locales/es/portal.js and /locales/es/email.js each containing one string.

I'm hoping to find people interested in the different parts to do initial spike implementations - then we can figure out the tasks more clearly.


Notes on participating & Hacktoberfest:

We aren't going to merge half-baked spikes or wiring that doesn't match the spec yet - so if you're just trying to get through Hacktoberfest as fast as possible, this is not the issue for you! (There are plenty of small bugs though!)

However, if you're using Hacktoberfest as a vehicle to find a cool & meaningful project to work on, this should be well up your street :)

We'll do our best to ensure that those who actively contribute value to this project get something merged and appropriately labelled by the end of Hacktoberfest 🎃

👉 If you're interested, drop a note to say Hi and which part you're interested in before getting stuck in, just in case we end up with multiple people trying to get the same things merged.

Problem

At present, only our theme layer is translatable using a custom-built i18n framework (that we do not want to build on). Our native membership (Portal), email, comment, and search systems, however, are not translatable.

Since these systems only have one set of strings that need translating into each language, it makes sense to have these translations be part of Ghost core (that is, built into the product itself).

Solution

We want to be able to serve translated emails and widgets (membership, comments, search), based on the language defined by the user in general settings. Technically, the solution boils down to answering two key questions:

  1. Where do the translated strings come from?
  2. How do the replacement strings get interpolated?

The answer to (1) is “a JSON file” of some description. We do not want to manage translations in a 3rd party tool like transifex, but we’d like to keep all of the JSON files in GitHub. It needs to be possible to load the strings and pass them to all the places they’re needed.

The answer to (2) is a {{t}} helper that needs to exist in both React and email templates. We want to keep our existing pattern in themes of using the default string as the key. This means that when a default string is changed, it needs to be re-translated in all languages, which is desirable as changes are usually more than cosmetic.

We will also need a way to collect all the default strings and compile them into a new JSON file, ready to be translated.

For email templates, it makes sense for the interpolation to be dynamic, done at the time of sending as this is a server-side system. For our React widgets, we would like to create a custom build for each translation, so it becomes possible to load language-specific scripts, e.g. portal-es.min.js.

Several aspects of i18n will be made significantly easier by ensuring that all translatable components and the translations themselves live together in the Ghost monorepo. This was the first part of the process, which is already complete 🎉

Technical plan

Our basic requirements are:

  • Load a single set of translations + (maybe) a set of custom strings into React widgets
  • Load a single set of translations into Ghost core for emails
  • Future: Load translations for Admin
  • Interpolation tools for Handlebars and React
  • Be able to use the default string as the key
  • Be able to generate a new language file with all available keys
  • Not have to load all translations, e.g., all of Portal’s translations into comments
  • Have a single place, in GitHub, where locales are defined
  • Ideally: generate a build per locale for each JS widget, e.g., portal-es.min.js

That last requirement is the hardest, as we will need to figure out a build step for our react projects… but this should be a one-off piece.

The i18next 4 framework appears to support every single one of our requirements, including fallback-based keysnamespacing, and having tools for extracting translation strings. It’s also extensible with plugins and has links to tooling for managing the translation process—so we could have a yarn check-translations command to view what’s not translated.

Expected structure:

  • A new ghost/locales folder in the monorepo that contains namespaced JSON files, e.g., locales/es/common.jsonlocales/es/portal.json , locales/es/email.json
  • For Node.js/email-templates we do one of:
    • (Preferred) The correct path is loaded from config based on environment
    • ghost/locales gets copied into ghost/core somehow
  • Then for React:
    • Use code-splitting to generate individual versions of portal, comments, and search for each supported locale.

Here are some details of using i18next with Webpack to create a a build step using “code splitting,” but it doesn’t quite achieve what we want. We are currently investigating switching to Vite for our build tool—and this may end up being an additional prerequisite.

Milestones

This project has prerequisites, is fairly complex, and has lots of different phases in which different community members may wish to get involved. Therefore, we’ve broken the project down into milestones and will keep this thread up to date with where we are.

Milestone 1: Monorepo

  • Portal needs to be merged into the monorepo ✅
  • Comments and search will follow later, but the project can continue without them.

Who: This milestone has been completed by the Ghost team.

Milestone 2: Framework and build tools

  • Implement the “expected structure” from above
  • Get the basic pieces of i18next in place
  • Figure out if we need to switch to Vite in Portal (may need help from Ghost team)
  • Have one interpolated string in email and Portal
  • Tools for extracting strings & generating files

Who: Looking for interested contributors, Ghost team will help and/or kick this off if needed.

Milestone 3: Interpolated Strings

  • All strings in Portal and Member emails (signup, signin, subscribe) are interpolated using the {{t}} helper and the default, existing US English string as the key.
  • comments-ui and SodoSearch should also be in the monorepo by now, and so these strings can also be interpolated.

Who: Looking for interested contributors

Milestone 4: Translations

  • Tooling for extracting strings into new locale files should be perfected
  • Getting all strings translated for additional languages.

Who: Looking for interested contributors


Check out the issue on GitHub for more info:

i18n Milestone 2: Framework and build tools #15502

This issue represents a single milestone within our i18n project. The full project is documented on the Ghost forum.


Expected structure

  • A new ghost/locales folder in the monorepo contains namespaced JSON files e.g. locales/es/common.json, /ocales/es/portal.json, locales/es/email.json
  • For Node.js/email-templates we do one of:
    • (Preferred) The correct path is loaded from config based on environment
    • ghost/locales gets copied into ghost/core somehow
  • Then for react:
    • Use code-splitting to generate individual versions of portal for each supported locale.

Goals:

  • [ ] Get the basic pieces of i18next in place & configured according to the tech spec
  • [ ] Implement the expected structure (see above)
  • [ ] Figure out if we need to switch to vitejs in Portal to achieve the desired build (may need help from Ghost team)
  • [ ] Have 1 interpolated string in each of email and Portal
  • [ ] Tooling for extracting strings & generating files

Tasks:

  • [ ] Move Portal into the monorepo (Ghost team to complete soon)
  • [ ] Demo/spike of the build tool for Portal - I think this is the hardest part so we should do it first
  • [ ] Demo/spike of how locale files get pulled into Node.js for email templates
  • [ ] Demo/spike of i18next wiring so that there's one interpolated string in Portal and one in an email template
  • [ ] Demo/spike of a tooling that can grab the 2 interpolated strings e.g. yarn translate es would create /locales/es/portal.js and /locales/es/email.js each containing one string.

I'm hoping to find people interested in the different parts to do initial spike implementations - then we can figure out the tasks more clearly.


Notes on participating & Hacktoberfest:

We aren't going to merge half-baked spikes or wiring that doesn't match the spec yet - so if you're just trying to get through Hacktoberfest as fast as possible, this is not the issue for you! (There are plenty of small bugs though!)

However, if you're using Hacktoberfest as a vehicle to find a cool & meaningful project to work on, this should be well up your street :)

We'll do our best to ensure that those who actively contribute value to this project get something merged and appropriately labelled by the end of Hacktoberfest 🎃

👉 If you're interested, drop a note to say Hi and which part you're interested in before getting stuck in, just in case we end up with multiple people trying to get the same things merged.

Find more info in our Forum

If you’re interested in helping with milestones 2-4 of this project, we’d love to hear from you in the comments below 👇

Top comments (1)

Collapse
 
vladi160 profile image
vladi160

Add and editable default language + a language option in the page/post.
I had bad days to configure this (GhostCMS + NextJS). At the end the URL for the default language is '/path' and for English (the second language) 'path-en'.
The same for navigation - made it with Json {"en": "Label text en", "bg": "Label text bg", "url": "/path"} - using Link in NextJS, even with dropdown .... "dropdown": {bg: string, en: string, url: string}[].
Some built-in grid will be good, too. I did it with shortcodes like in WP:
[row]
[col]
content
[/col]
[col]
Content
[/col]
[/row] - a row with 2 columns (col-12, col-md-6) with Bootstrap grid