{"id":18338836,"url":"https://github.com/tableflip/i18n-browserify","last_synced_at":"2025-10-08T15:04:30.638Z","repository":{"id":30706748,"uuid":"34262825","full_name":"tableflip/i18n-browserify","owner":"tableflip","description":"An example of i18n as a browserify transform","archived":false,"fork":false,"pushed_at":"2015-08-05T13:14:44.000Z","size":393,"stargazers_count":7,"open_issues_count":8,"forks_count":0,"subscribers_count":4,"default_branch":"gh-pages","last_synced_at":"2025-04-06T05:36:00.024Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"isc","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/tableflip.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2015-04-20T13:42:39.000Z","updated_at":"2022-04-14T04:04:51.000Z","dependencies_parsed_at":"2022-08-26T11:21:43.827Z","dependency_job_id":null,"html_url":"https://github.com/tableflip/i18n-browserify","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/tableflip/i18n-browserify","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tableflip%2Fi18n-browserify","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tableflip%2Fi18n-browserify/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tableflip%2Fi18n-browserify/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tableflip%2Fi18n-browserify/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/tableflip","download_url":"https://codeload.github.com/tableflip/i18n-browserify/tar.gz/refs/heads/gh-pages","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tableflip%2Fi18n-browserify/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":278963821,"owners_count":26076544,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-08T02:00:06.501Z","response_time":56,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-05T20:15:45.112Z","updated_at":"2025-10-08T15:04:30.593Z","avatar_url":"https://github.com/tableflip.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# i18n with gettext and browserify\n\nA worked example of adding `gettext` style language translation to a [browerified](https://github.com/substack/node-browserify) client-side JS app.\n\nInternationalizing an app comes at a cost; it can reduce the readability of templates, and affect the run-time performance of the app. `gettext` has some limitations but is well supported and has been successfully helping translate apps for years.\n\nAs such, the the goals of this example are to implement i18n and:\n\n- Use a gettext compatible api.\n- Handle simple translations, pluralisation and date formatting.\n- Keep the HTML templates as clear and readable as possible.\n- As much as possible, do the work of translating up front, at build time.\n\n**Try it out cloning the repo and running:**\n\n```sh\nnpm install\nnpm start\n```\n\n## gettext\n\nThe `gettext` api supports simple text-replacement translations and locale-sensitive pluralization. This app uses [jed.js](http://slexaxton.github.io/Jed/) to provided a `gettext` compatible api for front-end code.\n\n`gettext` works with dictionaries stored as `.po` (Portable Object) human-readable files, and `.mo` (Machine Object) files optimised for robots. jed.js uses dictionaries stored as the JSON equivalent of the `.po` files that can be automatically derived from existing `.po` files via tools like [`po2json`](https://github.com/mikeedwards/po2json)\n\n`gettext` has no built in support for locale-sensitive date formatting, so we use [moment.js](http://momentjs.com/#multiple-locale-support) for that.\n\n## Readable templates\n\nTo identify the simple phrases that should be translated we add a `data-i18n` attribute to elements.\n\n```html\n\u003ch3 data-i18n\u003eLogin\u003c/h3\u003e\n\u003cp class=\"login__lead\" data-i18n\u003e\n  Please enter your email and password to log in\n\u003c/p\u003e\n```\n\n`gettext` recommends using the default language as the key for looking up translations where possible, so we use the trimmed text content of these elements as the phrase to translate, so `\u003ch3 data-i18n\u003eLogin\u003c/h3\u003e` becomes a gettext lookup for \"Login\" in the chosen locale.\n\nStandard handlebars style variable substitution is supported, so:\n\n```html\n\u003ch1 data-i18n\u003eWelcome {{name}}\u003c/h1\u003e\n```\n\n...would use `Welcome {{name}}` as the key to look up, and the entry in the Spanish locale dictionary would look like:\n\n```json\n\"Welcome {{name}}\": [\"Hola {{name}}\"]\n```\n\nThe translation is done first, and then the templates standard variable substitution fills out the `{{name}}` at run time...\n\n## Simple translations at build time\n\nThe `data-i18n` magic is done at build time. A custom `118ify` transform is used with browserify to process the templates, translating the text content of the elements before handing over to the handlebars transform, that converts the html into a javascript function. As the translation happens before the templates are compiled, the standard handlebars variable substitution still works, even within phrases that are translated.\n\nAs such these translations are done up-front, before deployment, creating language specific app bundles, via browserify:\n\n```sh\n\"bundle_es\": \"browserify app.js -o dist/es/bundle.js -t [ ./i18n/i18ify.js --lang es ] -t hbsfy\",\n```\n\n## Context sensitive translations at runtime\n\nSome phrases can only be translated when we know the value of the variables in them. The most common case is pluralisation, where the number of things changes at run time, _\"There is 1 new alert\"_ vs _\"There are 3 new alerts\"_\n\nThis is handled in the templates via a handlebars helper.\n\n```html\n\u003cp\u003e\n  {{ngettext \"There is 1 new alert\" \"There are %d new alerts\" alerts}}\n\u003c/p\u003e\n```\nwhere `ngettext` is the pluralisation function from `gettext`, provided to the templates as:\n\n```js\nHandlebars.registerHelper('ngettext', function (one, other, count) {\n  var res = i18n.translate(one).ifPlural(count, other).fetch(count)\n  return res\n})\n```\n\nIn the above example, `i18n` is an instance of jed.js, initialised with dictionary for the current locale. It uses the more readable api, which maps onto specific `gettext` api calls. Jed supports both styles, so calling `ngetext` is equivalent to asking `ifPlural`\n\n```js\nvar getText = i18n.sprintf(i18n.ngettext(one, other, count), count)\nvar chained = i18n.translate(one).ifPlural(count, other).fetch(count)\nconsole.assert(getText === chained)\n```\n\n## Bundling a locale; aliasing i18n\n\nAs we have to do some translations at runtime, we have to include a dictionary in the bundle, but we don't want to include every possible language, just the translations for a specific locale.\n\nWe hide those details in the `i18n` package. At build time we alias `i18n` to point to the right locale data and the rest of the app code simply uses `i18n` to get translations for the chosen locale.\n\nThe locale specific files are in `./i18n/\u003clanguage-code\u003e`. Each locale contains a dictionary of translations and a js file that sets up locale specific customisations.\n\nFor the default `en` locale, we only have to initialise jed.js, but for other locales we also configure moment.js to use the right locale for it's date formatting:\n\n```js\n// Configure moment.js for Spanish\nvar moment = require('moment')\nrequire('moment/locale/es')\nmoment.locale('es')\n\n// Configure gettext\nvar i18n = require('../i18n.js')\nmodule.exports = i18n(require('./dict.json'))\nmodule.exports.moment = moment\n```\n\nWith this file we can coax browserify to include the right dictionaries in the current bundle, rather than all of them.\n\nThe rest of the app sees this module exposed as `i18n` by aliasing of `i18n` to a specific locale. This is done using a browserify transform called `pkgify` which let's us map a package name to a file at build time:\n\n```sh\n\"bundle_es\": \"browserify app.js -o dist/es/bundle.js -t [ pkgify --packages [ --i18n i18n/es/es.js ] ]\"\n```\n\nwhere `[ --i18n i18n/es/es.js ]` is re-writing calls to `require('i18n')` as `require('./i18n/es/es.js')`\n\n## Building a locale specific bundle\n\nBrowserify is doing a lot of work for us, so we capture the command line configuration in [npm run scripts](http://substack.net/task_automation_with_npm_run) in the `package.json`\n\n```json\n\"scripts\": {\n  \"watch\": \"watchify app.js -o dist/en/bundle.js -t [ pkgify --packages [ --i18n i18n/en/en.js ] ] -t hbsfy\",\n  \"bundle\": \"browserify app.js -o dist/en/bundle.js -t [ pkgify --packages [ --i18n ./i18n/en/en.js ] ] -t hbsfy\",\n  \"bundle_de\": \"browserify app.js -o dist/de/bundle.js -t [ pkgify --packages [ --i18n i18n/de/de.js ] ] -t [ ./i18n/i18ify.js --lang de ] -t hbsfy\",\n  \"bundle_es\": \"browserify app.js -o dist/es/bundle.js -t [ pkgify --packages [ --i18n i18n/es/es.js ] ] -t [ ./i18n/i18ify.js --lang es ] -t hbsfy\"\n},\n```\n\nThe bundle commands only differ on an language code string so there is an exercise for the interested reader to optimise these commands.\n\nThe output of the commands are locale specific app bundles found in `/dist/\u003clocale\u003e/bundle.js` which contain all our app code and language specific templates.\n\nThey can be run by an `npm start`\n\n## Translating outside of templates and other advanced stories\n\nIf you're forced to do some translating in your app code, you can simply require the `i18n` module and call the api directly:\n\n```js\ni18n\n  .translate(\"There is 1 ship in your account\")\n  .ifPlural(totalShips, \"There are %d ships in your account\")\n  .fetch(totalShips),\n```\n\nWhich supports pluralisation and variable substution in the output.\n\nDate formatting is handled by moment which is configured and exposed as `i18n.moment`, and can be used in conjunction with variable substitution in translated text.\n\n```js\ni18n\n  .translate(\"There will be 1h of scheduled maintenance on %s\")\n  .fetch(i18n.moment(\"2015-12-25\").format('LLL'))\n```\n\n## Bonus points\n\n- Where we translate ahead of time as part of the build we add the path to the source file as an html data attribute. During dev we can use that info to show tooltips to help developers and translators to figure out where in the app the text is from.\n- jed.js supports a `\"missing_key_callback\"` function which we map to add a warning to the console where the current dictionary is asked to translated key it doesn't have.\n\nO!\n\n## References\n\n- http://slexaxton.github.io/Jed/\n- http://docs.translatehouse.org/projects/localization-guide/en/latest/l10n/pluralforms.html?id=l10n/pluralforms\n- https://github.com/substack/browserify-handbook\n- http://momentjs.com/#multiple-locale-support\n- http://www.jeromesteunou.net/internationalisation-in-javascript.html\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftableflip%2Fi18n-browserify","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftableflip%2Fi18n-browserify","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftableflip%2Fi18n-browserify/lists"}