-
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
449 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -102,3 +102,5 @@ dist | |
|
||
# TernJS port file | ||
.tern-port | ||
|
||
package-lock.json |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
}) | ||
} |
Oops, something went wrong.