@tim-greller/xgettext-regex 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.travis.yml ADDED
@@ -0,0 +1,3 @@
1
+ language: node_js
2
+ node_js:
3
+ - "stable"
package/LICENCE ADDED
@@ -0,0 +1,5 @@
1
+ Copyright (c) 2014, Alan Shaw
2
+
3
+ Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
4
+
5
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,53 @@
1
+ # xgettext-regex
2
+
3
+ Minimum viable xgettext .pot file generator. Uses a configurable regex to get translation keys.
4
+
5
+ ## Examples
6
+
7
+ ```sh
8
+ cat foo.js | xgettext-regex # Output to stdout
9
+ xgettext-regex foo.js -o foo.po # Output to foo.po
10
+ xgettext-regex app-dir -o app.po # Recursive read directory
11
+ ```
12
+
13
+ ```js
14
+ var fs = require('fs')
15
+ var xgettext = require('xgettext-regex')
16
+
17
+ var src = '/path/to/file'
18
+ var dest = '/path/to/en-GB.po'
19
+ var opts = {}
20
+
21
+ fs.createReadStream(src)
22
+ .pipe(xgettext(src, opts))
23
+ .pipe(fs.createWriteStream(dest))
24
+ ```
25
+
26
+ ```js
27
+ var fs = require('fs')
28
+ var xgettext = require('xgettext-regex')
29
+
30
+ var files = ['/path/to/file.js', '/path/to/html/dir']
31
+ var opts = {}
32
+
33
+ xgettext.createReadStream(files, opts))
34
+ .pipe(fs.createWriteStream('/path/to/en-GB.po'))
35
+ ```
36
+
37
+ ## Options
38
+
39
+ ```js
40
+ opts = {
41
+ /* i18n function name */
42
+ fn: '_',
43
+ /* The regex used to match i18n function calls */
44
+ regex: /_\(((["'])(?:(?=(\\?))\3.)*?\2)\)/g,
45
+ /* Capture index for the i18n text in the above regex */
46
+ regexTextCaptureIndex: 1,
47
+ /* readdirp filters etc. */
48
+ readdirp: {
49
+ fileFilter: ['!.*', '!*.png', '!*.jpg', '!*.gif', , '!*.zip', , '!*.gz'],
50
+ directoryFilter: ['!.*', '!node_modules', '!coverage']
51
+ }
52
+ }
53
+ ```
package/bin/usage.txt ADDED
@@ -0,0 +1,17 @@
1
+ Usage: xgettext-regex [files] {OPTIONS}
2
+
3
+ Standard Options:
4
+
5
+ --outfile, -o Write the .pot formatted output to this file.
6
+ If unspecified, xgettext-regex prints to stdout.
7
+
8
+ --fn, -f Name of the i18n translation function.
9
+ Defaults to "_" if unspecified.
10
+
11
+ --help, -h Show this message.
12
+
13
+ --regex, -r Complete regex that matches string literals in translation functions.
14
+ Defaults to the name of the i18n function followed by "\(((["'])(?:(?=(\\?))\3.)*?\2)\)".
15
+
16
+ --index, -i The index of the capturing group that contains the match's string literal.
17
+ Defaults to 1 if unspecified.
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env node
2
+ var fs = require('fs')
3
+ var argv = require('minimist')(process.argv.slice(2))
4
+ var path = require('path')
5
+ var xgettext = require('../')
6
+
7
+ if (argv.v || argv.version) {
8
+ return console.log(require('../package.json').version)
9
+ }
10
+
11
+ if (argv.h || argv.help) {
12
+ return fs.createReadStream(__dirname + '/usage.txt').pipe(process.stdout)
13
+ }
14
+
15
+ var outFile = argv.o || argv.outfile
16
+ var opts = {
17
+ fn: (argv.f || argv.fn)
18
+ }
19
+ if (argv.r || argv.regex) opts.regex = new RegExp(argv.r || argv.regex, 'g')
20
+ if (argv.i || argv.index) opts.regexTextCaptureIndex = argv.i || argv.index
21
+
22
+ if (argv._.length) {
23
+ var files = argv._.map(function (filename) { return path.resolve(filename) })
24
+ var readable = xgettext.createReadStream(files, opts)
25
+
26
+ if (outFile) {
27
+ readable.pipe(fs.createWriteStream(outFile))
28
+ } else {
29
+ readable.pipe(process.stdout)
30
+ }
31
+ } else {
32
+ var duplex = xgettext('process.stdin', opts)
33
+
34
+ if (outFile) {
35
+ process.stdin.pipe(duplex).pipe(fs.createWriteStream(outFile))
36
+ } else {
37
+ process.stdin.pipe(duplex).pipe(process.stdout)
38
+ }
39
+ }
package/index.js ADDED
@@ -0,0 +1,186 @@
1
+ var fs = require('fs')
2
+ var path = require('path')
3
+ var Readable = require('stream').Readable
4
+ var through = require('through2')
5
+ var readdirp = require('readdirp')
6
+ var once = require('once')
7
+ var split = require('split')
8
+ var xtend = require('xtend')
9
+ var combine = require('stream-combiner')
10
+
11
+ function createDuplexStream (filename, opts) {
12
+ filename = filename || ''
13
+ opts = opts || {}
14
+ opts.fn = opts.fn || '_'
15
+ /*
16
+ * RegExp explaination
17
+ *
18
+ * (?<= begin of a positive lookbehind that makes sure that the string literal is an argument of the i18n function
19
+ * opts.fn i18n function name
20
+ * \(\s* opening paranthesis followed by optional whitespace
21
+ * (?: non capturing group matching individual prior arguments to the function and their delimeters
22
+ * (?: non capturing group matching a single prior argument
23
+ * "(?:[^"]|\\.)*" possible argument: a string literal quoted with double quotes, containing only escaped quotes if any
24
+ * | or
25
+ * '(?:[^']|\\.)*' same with single quotes
26
+ * ) end of single argument
27
+ * \s*,\s* delimeter: the arguments are seperated by commas and optional whitespace
28
+ * )* end of argument list, ending with a delimeter
29
+ * ) end of positive lookbehind and therefore everything that comes before the actual string literal that should be matched
30
+ *
31
+ * ( begin first capturing group containing the string value including its quotes (stripped away later via text.slice(1, -1))
32
+ * (["']) second capturing group containing the quote character
33
+ * (?: begin non capturing group containing a single (escaped or unescaped) character of the string literal's text content
34
+ * (?= begin of a positive lookahead matching and capturing a backslash, if there is one at the current position
35
+ * (\\?) capturing the backslash (or an empty string) in \3
36
+ * )
37
+ * \3. matches any character, optionally preceeded by a backslash.
38
+ * )*? end of the character. The lazy repetition ensures, that no quotes (\2) are included. Escaped quotes are included, because the character immediately after a backslash is always included, since \3 is followed by a dot and therefore the string cannot end with a backslash, but automatically matches the next character as well.
39
+ * \2 the quote character, closing the string literal
40
+ * ) end of the capturing group containing the string literal
41
+ *
42
+ * (?= begin of a positive lookahead matching any paramters that might follow after the target string literal
43
+ * (?: analogous to the beginning: non capturing group matching arguments and their delimeters
44
+ * \s*,\s* now we start with the delimeter
45
+ * (?: match single argument
46
+ * "(?:[^"]|\\.)*" string literal with double quotes
47
+ * |'(?:[^']|\\.)*' or with single quotes
48
+ * |[^"')]+ or without any quotes (numbers, object references, variables, ...). Cannot contain any paranthesis either, as it is not possible to ensure that every closing bracket has a preceding opening one with regex (would require at least a context free language)
49
+ * )
50
+ * )* end of argument list
51
+ * \s* optional whitespace
52
+ * \) closing paranthesis
53
+ * ) end of positive lookahead
54
+ */
55
+ opts.regex = opts.regex || new RegExp("(?<=" + opts.fn + "\\(\\s*(?:(?:\"(?:[^\"]|\\\\.)*\"|'(?:[^']|\\\\.)*')\\s*,\\s*)*)(([\"'])(?:(?=(\\\\?))\\3.)*?\\2)(?=(?:\\s*,\\s*(?:\"(?:[^\"]|\\\\.)*\"|'(?:[^']|\\\\.)*'|[^\"')]+))*\\s*\\))", 'g')
56
+ opts.regexTextCaptureIndex = opts.regexTextCaptureIndex || 1
57
+
58
+ var lineNum = 1
59
+
60
+ return combine(
61
+ split(),
62
+ through(function (line, enc, cb) {
63
+ line = line.toString()
64
+
65
+ var matches
66
+ var first = true
67
+
68
+ while ((matches = opts.regex.exec(line)) !== null) {
69
+ var entry = '\n'
70
+
71
+ if (first) {
72
+ const relativeFilename = path.relative(process.cwd(), filename)
73
+ entry += '#: ' + relativeFilename + ':' + lineNum + '\n'
74
+ first = false
75
+ }
76
+
77
+ var text = matches[opts.regexTextCaptureIndex]
78
+
79
+ if (text[0] == "'") {
80
+ text = text.slice(1, -1)
81
+ text = text.replace(/\\'/g, "'")
82
+ text = '"' + text.replace(/"/g, '\\"') + '"'
83
+ }
84
+
85
+ entry += 'msgid ' + text + '\n'
86
+ entry += 'msgstr ' + text + '\n'
87
+
88
+ this.push(entry)
89
+ }
90
+
91
+ lineNum++
92
+ opts.regex.lastIndex = 0
93
+ cb()
94
+ })
95
+ )
96
+ }
97
+
98
+ module.exports = createDuplexStream
99
+
100
+ module.exports.createReadStream = function (files, opts) {
101
+ if (!Array.isArray(files)) files = [files]
102
+
103
+ var index = 0
104
+ var readable = new Readable()
105
+
106
+ readable._read = function () {
107
+ var push = true
108
+
109
+ while (push && index < files.length) {
110
+ push = this.push(files[index])
111
+ index++
112
+ }
113
+
114
+ this.push(null)
115
+ }
116
+
117
+ return readable.pipe(createDuplexFileStream(opts))
118
+ }
119
+
120
+ function getText (filename, opts) {
121
+ return fs.createReadStream(filename).pipe(createDuplexStream(filename, opts))
122
+ }
123
+
124
+ var READDIRP_OPTS = {
125
+ fileFilter: ['!.*', '!*.png', '!*.jpg', '!*.gif', '!*.zip', '!*.gz'],
126
+ directoryFilter: ['!.*', '!node_modules', '!coverage']
127
+ }
128
+
129
+ function createDuplexFileStream (opts) {
130
+ opts = opts || {}
131
+ opts.readdirp = opts.readdirp || {}
132
+
133
+ return through(function (filename, enc, cb) {
134
+ var self = this
135
+ filename = filename.toString()
136
+ cb = once(cb)
137
+
138
+ self.push('#, fuzzy\n')
139
+ self.push('msgid ""\n')
140
+ self.push('msgstr ""\n')
141
+ self.push('"Content-Type: text/plain; charset=UTF-8\\n"\n')
142
+
143
+ fs.stat(filename, function (er, stats) {
144
+ if (er) return cb(er)
145
+
146
+ if (stats.isFile()) {
147
+ getText(filename, opts)
148
+ .on('data', function (entry) {self.push(entry)})
149
+ .on('error', function (er) {
150
+ console.error('File getText error', filename, er)
151
+ cb(er)
152
+ })
153
+ .on('end', function () {
154
+ cb()
155
+ })
156
+ } else if (stats.isDirectory()) {
157
+ var total = 0
158
+ var complete = 0
159
+ var readdirpComplete = false
160
+
161
+ readdirp(filename, xtend(READDIRP_OPTS, opts.readdirp))
162
+ .on('data', function (entry) {
163
+ total++
164
+
165
+ getText(entry.fullPath, opts)
166
+ .on('data', function (entry) {self.push(entry)})
167
+ .on('error', function (er) {
168
+ console.error('Directory getText error', entry.fullPath, er)
169
+ cb(er)
170
+ })
171
+ .on('end', function () {
172
+ complete++
173
+ if (total == complete && readdirpComplete) cb()
174
+ })
175
+ })
176
+ .on('error', function (er) {
177
+ console.error('Directory error', filename, er)
178
+ cb(er)
179
+ })
180
+ .on('end', function () {
181
+ readdirpComplete = true
182
+ })
183
+ }
184
+ })
185
+ })
186
+ }
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@tim-greller/xgettext-regex",
3
+ "version": "0.4.0",
4
+ "description": "Minimum viable xgettext .po file generator. Uses a configurable regex to get translation keys.",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "test": "istanbul cover node_modules/.bin/tape test/test.js"
8
+ },
9
+ "author": "Alan Shaw",
10
+ "license": "ISC",
11
+ "dependencies": {
12
+ "minimist": "^1.1.0",
13
+ "once": "^1.3.1",
14
+ "readdirp": "^3.1.3",
15
+ "split": "^1.0.1",
16
+ "stream-combiner": "^0.2.1",
17
+ "through2": "^3.0.1",
18
+ "xtend": "^4.0.0"
19
+ },
20
+ "bin": {
21
+ "xgettext-regex": "bin/xgettext-regex.js"
22
+ },
23
+ "devDependencies": {
24
+ "concat-stream": "^2.0.0",
25
+ "istanbul": "^0.4.5",
26
+ "tape": "^4.11.0"
27
+ },
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "git+https://github.com/alanshaw/xgettext-regex.git"
31
+ },
32
+ "keywords": [
33
+ "xgettext",
34
+ "gettext",
35
+ "i18n",
36
+ "l18n"
37
+ ],
38
+ "bugs": {
39
+ "url": "https://github.com/alanshaw/xgettext-regex/issues"
40
+ },
41
+ "homepage": "https://github.com/alanshaw/xgettext-regex",
42
+ "directories": {
43
+ "test": "test"
44
+ }
45
+ }
@@ -0,0 +1,2 @@
1
+ div
2
+ a(href='#')= _("index.js")
@@ -0,0 +1,3 @@
1
+ <?php
2
+ $iHaveNoIdeaWhatImDoing = _('php hypertext preprocessor')
3
+ ?>
@@ -0,0 +1,5 @@
1
+ for (var i = 0; i < 1000; i++) {
2
+ console.log(_('app.js'))
3
+ }
4
+
5
+ alert(_("There's pie in them hills"))
@@ -0,0 +1,3 @@
1
+ $.xgettext = function () {
2
+ return _('w00t!')
3
+ }
package/test/test.js ADDED
@@ -0,0 +1,110 @@
1
+ var fs = require('fs')
2
+ var Readable = require('stream').Readable
3
+ var test = require('tape')
4
+ var concat = require('concat-stream')
5
+ var xgettext = require('../')
6
+
7
+ test('Can create .pot from code', function (t) {
8
+ t.plan(1)
9
+
10
+ var src = new Readable
11
+ src._read = function () {
12
+ this.push('_("foobar")\n\n')
13
+ this.push(null)
14
+ }
15
+
16
+ src
17
+ .pipe(xgettext())
18
+ .pipe(concat({encoding: 'string'}, function (pot) {
19
+ t.ok(pot.indexOf('msgid "foobar"') > -1, 'msgid "foobar" exists')
20
+ t.end()
21
+ }))
22
+ })
23
+
24
+ test('Can deal with single quotes', function (t) {
25
+ t.plan(1)
26
+
27
+ var src = new Readable
28
+ src._read = function () {
29
+ this.push("blah;\n_('foobar')")
30
+ this.push(null)
31
+ }
32
+
33
+ src
34
+ .pipe(xgettext())
35
+ .pipe(concat({encoding: 'string'}, function (pot) {
36
+ t.ok(pot.indexOf('msgid "foobar"') > -1, 'msgid "foobar" exists')
37
+ t.end()
38
+ }))
39
+ })
40
+
41
+ test('Can deal with escaped quotes', function (t) {
42
+ t.plan(2)
43
+
44
+ var src = new Readable
45
+ src._read = function () {
46
+ this.push("\n_('foobar\\'s');\nif(true){_(\"air \\\"quotes\\\"\")}")
47
+ this.push(null)
48
+ }
49
+
50
+ src
51
+ .pipe(xgettext())
52
+ .pipe(concat({encoding: 'string'}, function (pot) {
53
+ t.ok(pot.indexOf('msgid "foobar\'s"') > -1, 'msgid "foobar\'s" exists')
54
+ t.ok(pot.indexOf('msgid "air \\"quotes\\""') > -1, 'msgid "air \\"quotes\\"" exists')
55
+ t.end()
56
+ }))
57
+ })
58
+
59
+ test('Can create .pot from single file', function (t) {
60
+ t.plan(1)
61
+
62
+ xgettext.createReadStream(__dirname + '/fixtures/index.jade')
63
+ .pipe(concat({encoding: 'string'}, function (pot) {
64
+ t.ok(pot.indexOf('msgid "index.js"') > -1, 'msgid "index.js" exists')
65
+ t.end()
66
+ }))
67
+ })
68
+
69
+ test('Can change i18n function', function (t) {
70
+ t.plan(1)
71
+
72
+ var src = new Readable
73
+ src._read = function () {
74
+ this.push("blah;\ni18n('foobar')")
75
+ this.push(null)
76
+ }
77
+
78
+ src
79
+ .pipe(xgettext('src', {fn: 'i18n'}))
80
+ .pipe(concat({encoding: 'string'}, function (pot) {
81
+ t.ok(pot.indexOf('msgid "foobar"') > -1, 'msgid "foobar" exists')
82
+ t.end()
83
+ }))
84
+ })
85
+
86
+ test('Can create .pot from multiple files/directories', function (t) {
87
+ t.plan(6)
88
+
89
+ fs.readdir(__dirname + '/fixtures', function (er, files) {
90
+ t.ifError(er, 'No error getting fixtures dir listing')
91
+
92
+ files = files.map(function (f) {
93
+ return __dirname + '/fixtures/' + f
94
+ })
95
+
96
+ xgettext.createReadStream(files)
97
+ .pipe(concat({encoding: 'string'}, function (pot) {
98
+ // app.js
99
+ t.ok(pot.indexOf('msgid "app.js"') > -1, 'msgid "app.js" exists')
100
+ t.ok(pot.indexOf('msgid "There\'s pie in them hills"') > -1, 'msgid "There\'s pie in them hills" exists')
101
+ // plugin.js
102
+ t.ok(pot.indexOf('msgid "w00t!"') > -1, 'msgid "w00t!" exists')
103
+ // index.jade
104
+ t.ok(pot.indexOf('msgid "index.js"') > -1, 'msgid "index.js" exists')
105
+ // index.php
106
+ t.ok(pot.indexOf('msgid "php hypertext preprocessor"') > -1, 'msgid "php hypertext preprocessor" exists')
107
+ t.end()
108
+ }))
109
+ })
110
+ })