Skip to content

Commit

Permalink
Merge
Browse files Browse the repository at this point in the history
  • Loading branch information
klembot committed Dec 20, 2016
2 parents c5fba4f + add659a commit 8be21ae
Show file tree
Hide file tree
Showing 33 changed files with 2,992 additions and 2,313 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ make. You can also create a dev build at `build/` with `npm run build`.

`npm lint` and `npm test` will lint and test the source code respectively.

`npm pot` will create a POT template file for localization at
`src/locale/po/template.pot`. See Localization below for more information.

`npm nw` will build NW.js-based apps in `dist/nw`. In order to build Windows
apps on OS X or Linux, you will need to have [Wine](https://www.winehq.org/)
and [makensis](http://nsis.sourceforge.net/) installed.
Expand Down
8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "Twine",
"version": "2.1.0b4",
"version": "2.1.0b5",
"author": "Chris Klimas <[email protected]>",
"description": "a GUI for creating nonlinear stories",
"license": "GPL-3.0",
Expand Down Expand Up @@ -39,6 +39,7 @@
"jed": "^1.1.0",
"jquery": "^2.1.4",
"jszip": "^2.5.0",
"mkdirp": "^0.5.1",
"moment": "^2.10.3",
"osenv": "^0.1.3",
"scroll-to-element": "^2.0.0",
Expand All @@ -53,6 +54,7 @@
"vuex": "^0.6.3"
},
"devDependencies": {
"acorn": "^4.0.3",
"babel-core": "^6.18.2",
"babel-loader": "^6.2.8",
"babel-preset-es2015": "^6.18.0",
Expand All @@ -63,11 +65,14 @@
"ejs": "^2.5.2",
"ejs-loader": "^0.3.0",
"eslint": "^3.10.2",
"estraverse": "^4.2.0",
"extract-text-webpack-plugin": "^1.0.1",
"file-loader": "^0.9.0",
"fs-extra": "^1.0.0",
"glob": "^7.1.1",
"html-loader": "^0.4.4",
"html-webpack-plugin": "^2.24.1",
"htmlparser2": "^3.9.2",
"jscs": "^3.0.7",
"json-loader": "^0.5.4",
"karma": "^1.3.0",
Expand All @@ -81,6 +86,7 @@
"less-plugin-clean-css": "^1.5.1",
"nw-builder": "^3.1.2",
"po2json": "^0.4.5",
"pofile": "^1.0.2",
"raw-loader": "^0.5.1",
"rimraf": "^2.5.4",
"sinon": "^2.0.0-pre.4",
Expand Down
2 changes: 1 addition & 1 deletion scripts/build-nw.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ fsExtra.copySync('package.json', 'dist/web/package.json');
var nw = new NwBuilder({
files: 'dist/web/**',
platforms: ['osx64', 'win32', 'win64', 'linux32', 'linux64'],
version: '0.18.7',
version: '0.18.8',
buildDir: 'dist/nw',
cacheDir: 'nw-cache/',
macIcns: 'src/common/img/logo.icns',
Expand Down
249 changes: 153 additions & 96 deletions scripts/extract-pot.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,136 +3,193 @@ Creates src/locale/po/template.pot by scanning the application source.
*/

'use strict';
const acorn = require('acorn');
const estraverse = require('estraverse');
const fs = require('fs');
const htmlParser = require('htmlparser2');
const glob = require('glob');
let strings = {};
const poFile = require('pofile');

function addMatch(fileName, match) {
let result = new poFile();

function addItem(location, string, pluralString, comment) {
/*
matches[1] is the localization comment, if any. It may have
extra newlines and comment slashes in it.
Clean up the comment.
*/

matches[2] is the actual string to translate.
if (comment) {
comment = comment.trim().replace(/[\t\r\n]+/g, ' ');
}

matches[3] is the plural form of the string, if any.
/*
Check for an existing item.
*/

const text = match[2];
let existing = result.items.find(item => item.msgid === string);

if (!strings[text]) {
strings[text] = { locations: [] };
}
if (existing) {
existing.references.push(location);

if (strings[text].locations.indexOf(fileName) === -1) {
strings[text].locations.push(fileName);
}

if (match[1]) {
strings[text].comment = match[1].replace(/[\t\r\n]|(\/\/)/g, '');
if (comment) {
existing.extractedComments.push(comment);
}
}
else {
let item = new poFile.Item();

strings[text].plural = match[3];
}
item.msgid = string;
item.msgid_plural = pluralString;
item.references = [location];

/*
In templates, we look for Vue filters, optionally preceded by a HTML
comment with a localization note preceded by "L10n:".
*/
if (pluralString) {
item.msgstr = ['', ''];
}

glob.sync('src/**/*.html').forEach(fileName => {
let match;
const source = fs.readFileSync(fileName, { encoding: 'utf8' });
const templateRegexp = new RegExp(
/* Optional localization note in a HTML comment. */
/(?:<!-- *L10n: *([\s\S]+)-->[\s*])?/.source +
if (comment) {
item.extractedComments = [comment];
}

/* Opening moustache. */
/{{{? */.source +
result.items.push(item);
}
}

/* String to localize and say filter. */
/['"]([^}]*?)['"] *\| *say/.source +
/*
Parse .html files for text in this format:
{{ 'Simple string' | say }}
{{ 'Singular string' | sayPlural 'Plural string' }}
*/

/* Optional pluralization. */
/(?:Plural *['"](.+)['"].*)?/.source +
const templateRegexp = new RegExp(
/* Opening moustache. */
/{{{? */.source +

/* Closing moustache. */
/ *}}}?/.source,
/* String to localize and say filter. */
/['"]([^}]*?)['"] *\| *say/.source +

'gm'
);
/* Optional pluralization. */
/(?:Plural *['"](.+)['"].*)?/.source +

while (match = templateRegexp.exec(source)) {
addMatch(fileName, match);
}
});
/* Closing moustache. */
/ *}}}?/.source,

/*
In JavaScript files, we look for say or sayPlural function invocations,
with localization comments that begin with "L10n:".
*/
'gm'
);

glob.sync('src/**/*.js').forEach(fileName => {
let match;
glob.sync('src/**/*.html').forEach(fileName => {
const source = fs.readFileSync(fileName, { encoding: 'utf8' });
const jsRegexp = new RegExp(
/*
An optional localization note, set off by a line comment. Note that we
have to strip out extra //s and tabs if the comment extends over
multiple lines.
*/
/(?:L10n: *([\s\S]*?)\s*)?/.source +

/* The beginning of the say() or sayPlural() call. */
/say(?:Plural)?\(/.source +

/* The first argument, the text to localize. */
/['"](.*?)['"]/.source +

/* An optional second argument, the plural form. */
/(?:, *['"](.*?)['"])?/.source +

/* Extra arguments we don't care about, and closing paren. */
/.*\)/.source,

'gm'
);
const parser = new htmlParser.Parser({
ontext(text) {
let match;

while (match = templateRegexp.exec(text.trim())) {
/*
The first captured expression is a comment, if any.
The third is the plural form of the string, if any.
*/

addItem(fileName, match[1], match[2]);
}
}
});

while (match = jsRegexp.exec(source)) {
addMatch(fileName, match);
}
parser.write(source);
});

/*
Create the .pot file.
http://pology.nedohodnik.net/doc/user/en_US/ch-poformat.html has
a nice introduction to the format.
Parse .js files for say() and sayPlural() calls.
*/

fs.writeFileSync(
'src/locale/po/template.pot',
Object.keys(strings).reduce((result, text) => {
let entry = strings[text];

result += `#: ${entry.locations.join(' ')}\n`;
glob.sync('src/**/*.js').forEach(fileName => {
/*
Simplifies an expression (e.g. 'a compound ' + ' string') to a single
value.
*/

if (entry.comment) {
result += `#. ${entry.comment}\n`;
function parseValue(node) {
switch (node.type) {
case 'Literal':
/*
We can't use .value here because we need to keep the strings
intact with Unicode escapes.
*/

return node.raw.replace(/^['"]/, '').replace(/['"]$/, '');
break;

case 'BinaryExpression':
if (node.operator === '+') {
return parseValue(node.left) + parseValue(node.right);
}

throw new Error(
`Don't know how to parse operator ${node.operator}`
);
break;

default:
throw new Error(`Don't know how to parse value of ${node.type}`);
}
}

if (entry.plural) {
result += `msgid "${text.replace(/"/g, '\"')}"\n` +
`msgid_plural "${text.replace(/"/g, '\"')}"\n` +
'msgstr[0] ""\n' +
'msgstr[1] ""\n\n';
let comments = [];
const ast = acorn.parse(
fs.readFileSync(fileName, { encoding: 'utf8' }),
{
ecmaVersion: 6,
locations: true,
onComment: comments
}
else {
result += `msgid "${text.replace(/"/g, '\"')}"\n` +
'msgstr ""\n\n';
);

estraverse.traverse(
ast,
{
enter: function(node, parent) {
if (node.type === 'CallExpression') {
let funcName;

if (node.callee.type === 'Identifier') {
funcName = node.callee.name;
}
else if (node.callee.type === 'MemberExpression') {
funcName = node.callee.property.name;
}

/*
Check for a comment that ended 0-2 lines before this call.
*/

const precedingComment = comments.find(comment =>
Math.abs(comment.loc.end.line - node.loc.start.line) < 3 &&
/^\s*L10n/.test(comment.value)
);

if (funcName === 'say') {
addItem(
fileName + ':' + node.loc.start.line,
parseValue(node.arguments[0]),
null,
precedingComment ? precedingComment.value : null
);
}

if (funcName === 'sayPlural') {
addItem(
fileName + ':' + node.loc.start.line,
parseValue(node.arguments[0]),
parseValue(node.arguments[1]),
precedingComment ? precedingComment.value : null
);
}
}
}
}
);
});

return result;
}, ''),
fs.writeFileSync(
'src/locale/po/template.pot',
result.toString(),
{ encoding: 'utf8' }
);

console.log('Wrote src/locale/po/template.pot.\n');
console.log(`Wrote ${result.items.length} extracted strings to src/locale/po/template.pot.\n`);
2 changes: 2 additions & 0 deletions src/data/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@ const actions = module.exports = {

const format = {
name: props.name,
version: props.version,
url,
properties: props
};
Expand Down Expand Up @@ -348,6 +349,7 @@ const actions = module.exports = {

store.state.storyFormat.formats.forEach(format => {
if (typeof format.version !== 'string' || format.version === '') {
console.warn(`Deleting unversioned story format ${format.name}`);
actions.deleteFormat(store, format.id);
}
});
Expand Down
Loading

0 comments on commit 8be21ae

Please sign in to comment.