Skip to content

Commit

Permalink
add package content
Browse files Browse the repository at this point in the history
  • Loading branch information
m90 committed Jul 17, 2020
1 parent 013dbb7 commit 743e5aa
Show file tree
Hide file tree
Showing 8 changed files with 449 additions and 0 deletions.
19 changes: 19 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
version: 2.1

jobs:
test:
executor:
name: node/default
steps:
- checkout
- node/with-cache:
steps:
- run: npm install
- run: npm test
workflows:
test:
jobs:
- test

orbs:
node: circleci/[email protected]
18 changes: 18 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Copyright 2020 - Offen Authors <[email protected]>
# SPDX-License-Identifier: Apache-2.0

# EditorConfig is awesome: http://EditorConfig.org

# top-most EditorConfig file
root = true

# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 2

[*.md]
trim_trailing_whitespace = false
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,5 @@ dist

# TernJS port file
.tern-port

package-lock.json
87 changes: 87 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,89 @@
# l10nify

Localization workflow for Browserify

## How does this work

This is a Browserify-based workflow for localizing client side applications. It is built for use cases where you can and want to ship one bundle per locale. Strings are defined by calling a pseudo-global function (i.e. `__(string, args..)`) in the default language in your code (similar to GNU gettext or similar). Strings are stored in `.po` files.

## Installation

Install from npm:

```
npm install @offen/l10nify
```

This installs two things:

- a Browserify transform for localizing strings at bundle time. It is references as `@offen/l10nify`
- a `extract-strings` command that you can use to generate PO files from your JavaScript code.

## Usage

### Defining strings in client side code

In your code, use the `__(string, args...)` function (`__` is the default, but you can use anything) to declare strings in your default language (which defaults to `en` but can be anything you want it to):

```js
const HappyBirthdayComponent = (props) => {
return (
<h1>{__('Happy birthday, %s!', props.name)}</h1>
)
}
```

### Extract strings from your code

Next, you can extract these strings from your code into `.po` files using the `extract-strings` command:

```
$(npm bin)/extract-strings --default-locale en \
--locale es \
--locale fr \
--locale de \
**/*.js
```

This will create a `.po` file for each non-default locale in a directory called `./locales` (pass `--target` if you want to change this).

Refer to `extract-strings --help` for a full list of options

### Apply the transform at bundle time

Apply the transform to your Browserify setup passing the target `locale`. In development, you can omit this parameter to make the transform return the default locale, i.e. the strings you defined in code.

```js
var browserify = require('browserify')

var b = browserify()
b.add('app.js')
b.transform('@offen/l10nify', { locale: 'fr' })
b.bundle(function (err, src) {
// consume bundle
})
```

#### Options

The following options can be passed to the transform:

##### `locale`

`locale` specifies the locale which you want to return when bundling. It defaults to `en`.

##### `defaultLocale`

`defaultLocale` specifies the default locale that is used to define strings in code. It defaults to `en`.

##### `source`

`source` specifies the directory in which the `<LOCALE>.po` files are stored. It defaults to `./locales`

##### `global`

`global` defines the global function identifier that is used for defining strings in code. It defaults to `__`.

## License

Copyright 2020 Frederik Ring - `l10nify` is available under the MIT License
77 changes: 77 additions & 0 deletions bin/cmd.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#!/usr/bin/env node

/**
* Copyright 2020 - Frederik Ring <[email protected]>
* SPDX-License-Identifier: MIT
*/

var path = require('path')
var arg = require('arg')

var extractStrings = require('./../extract.js')

var args = arg({
'--help': Boolean,
'--default-locale': String,
'--locale': [String],
'--target': String,
'--match': String,
'-h': '--help',
'-d': '--default-locale',
'-l': '--locale',
'-t': '--target',
'-m': '--match'
})

if (args['--help']) {
console.log(`Usage: extract-strings [options]
Options:
-l, --locale [LOCALE] Specify the locales to extract. Pass multiple
locales by passing multiple flags.
-d, --default-locale [LOCALE] Specify the default locale that is used in code.
Defaults to "en".
-t, --target [DIRECTORY] Specify the target directory for saving .po
files. Defaults to "./locales".
-m, --match [DIRECTORY] Specify a glob pattern for files to extract
strings from. Defaults to "**/*.js".
-g, --global [IDENTIFIER] Specify the global identifier used as l10n
function in code, Defaults to "__"
-h, --help Display this help message.
`)
process.exit(0)
}

args = Object.assign({
'--default-locale': 'en',
'--locale': [],
'--target': './locales/',
'--match': '**/*.js',
'--global': '__'
}, args)

var eligible = args['--locale']
.filter(function (locale) {
return locale !== args['--default-locale']
})

if (eligible.length === 0) {
console.log('No non-default locales were configured. Nothing to do.')
console.log('If this is unintended, check the locales passed to this task.')
process.exit(0)
}

Promise.all(eligible.map(function (locale) {
return extractStrings(
path.join(process.cwd(), args['--target'], locale + '.po'),
args['--match'],
args['--global']
)
}))
.then(() => {
console.log('Successfully extracted %d locales.', eligible.length)
})
.catch(function (err) {
console.error('Error extracting strings: %s', err.message)
console.error(err)
process.exit(1)
})
104 changes: 104 additions & 0 deletions extract.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/**
* Copyright 2020 - Frederik Ring <[email protected]>
* SPDX-License-Identifier: MIT
*/

var fs = require('fs')
var util = require('util')
var jscodeshift = require('jscodeshift')
var glob = require('glob')
var touch = require('touch')
var PO = require('pofile')

module.exports = extractStrings

function extractStrings (destination, globPattern, globalFunctionIdentifier) {
return util.promisify(glob)(globPattern)
.then(function (files) {
if (!files || !files.length) {
console.log('No files found using given pattern %s, exiting', globPattern)
return null
}
return parse(files, globalFunctionIdentifier)
})
.then(function (allStrings) {
return merge(destination, allStrings)
})
.then(function () {
console.log('Successfully saved strings to %j', destination)
})
}

function merge (file, allStrings) {
var currentPo = new PO()
currentPo.items = allStrings

return util.promisify(PO.load)(file)
.catch(function (err) {
if (err.code === 'ENOENT') {
return null
}
throw err
})
.then(function (existingPo) {
if (existingPo) {
currentPo.items = currentPo.items.map(function (item) {
var exists = existingPo.items.filter(function (existingItem) {
return existingItem.msgid === item.msgid
})
if (exists.length && exists[0].msgstr.length) {
item.msgstr = exists[0].msgstr
}
return item
})
}
return util.promisify(touch)(file)
})
.then(function () {
return util.promisify(currentPo.save).bind(currentPo)(file)
})
}

function parse (files, globalFunctionIdentifier) {
var all = files
.filter(function (fileName) {
return !(/node_modules/.test(fileName))
})
.map(function (fileName) {
return new Promise(function (resolve, reject) {
fs.readFile(fileName, 'utf-8', function (err, data) {
if (err) {
return reject(err)
}
var j = jscodeshift(data)
var calls = j.find(jscodeshift.CallExpression, {
callee: {
type: 'Identifier',
name: globalFunctionIdentifier
}
})
var strings = []
calls.forEach(function (node) {
var dupes = strings.filter(function (string) {
return string.msgid === node.value.arguments[0].value
})
if (dupes.length) {
dupes[0].comments.push(fileName + ':' + node.node.loc.start.line)
} else {
var item = new PO.Item()
item.msgid = node.value.arguments[0].value
item.comments = [
fileName + ':' + node.node.loc.start.line
]
strings.push(item)
}
})
resolve(strings)
})
})
})
return Promise.all(all)
.then(function (results) {
return [].concat.apply([], results)
})
}
Loading

0 comments on commit 743e5aa

Please sign in to comment.