summaryrefslogtreecommitdiffstats
path: root/.gitlab-ci/check-potfiles.js
diff options
context:
space:
mode:
Diffstat (limited to '.gitlab-ci/check-potfiles.js')
-rw-r--r--.gitlab-ci/check-potfiles.js207
1 files changed, 207 insertions, 0 deletions
diff --git a/.gitlab-ci/check-potfiles.js b/.gitlab-ci/check-potfiles.js
new file mode 100644
index 0000000..0c8885e
--- /dev/null
+++ b/.gitlab-ci/check-potfiles.js
@@ -0,0 +1,207 @@
+const gettextFuncs = new Set([
+ '_',
+ 'N_',
+ 'C_',
+ 'NC_',
+ 'dcgettext',
+ 'dgettext',
+ 'dngettext',
+ 'dpgettext',
+ 'gettext',
+ 'ngettext',
+ 'pgettext',
+]);
+
+function dirname(file) {
+ const split = file.split('/');
+ split.pop();
+ return split.join('/');
+}
+
+const scriptDir = dirname(import.meta.url);
+const root = dirname(scriptDir);
+
+const excludedFiles = new Set();
+const foundFiles = new Set()
+
+function addExcludes(filename) {
+ const contents = os.file.readFile(filename);
+ const lines = contents.split('\n')
+ .filter(l => l && !l.startsWith('#'));
+ lines.forEach(line => excludedFiles.add(line));
+}
+
+addExcludes(`${root}/po/POTFILES.in`);
+addExcludes(`${root}/po/POTFILES.skip`);
+
+function walkAst(node, func) {
+ func(node);
+ nodesToWalk(node).forEach(n => walkAst(n, func));
+}
+
+function findGettextCalls(node) {
+ switch(node.type) {
+ case 'CallExpression':
+ if (node.callee.type === 'Identifier' &&
+ gettextFuncs.has(node.callee.name))
+ throw new Error();
+ if (node.callee.type === 'MemberExpression' &&
+ node.callee.object.type === 'Identifier' &&
+ node.callee.object.name === 'Gettext' &&
+ node.callee.property.type === 'Identifier' &&
+ gettextFuncs.has(node.callee.property.name))
+ throw new Error();
+ break;
+ }
+ return true;
+}
+
+function nodesToWalk(node) {
+ switch(node.type) {
+ case 'ArrayPattern':
+ case 'BreakStatement':
+ case 'CallSiteObject': // i.e. strings passed to template
+ case 'ContinueStatement':
+ case 'DebuggerStatement':
+ case 'EmptyStatement':
+ case 'Identifier':
+ case 'Literal':
+ case 'MetaProperty': // i.e. new.target
+ case 'Super':
+ case 'ThisExpression':
+ return [];
+ case 'ArrowFunctionExpression':
+ case 'FunctionDeclaration':
+ case 'FunctionExpression':
+ return [...node.defaults, node.body].filter(n => !!n);
+ case 'AssignmentExpression':
+ case 'BinaryExpression':
+ case 'ComprehensionBlock':
+ case 'LogicalExpression':
+ return [node.left, node.right];
+ case 'ArrayExpression':
+ case 'TemplateLiteral':
+ return node.elements.filter(n => !!n);
+ case 'BlockStatement':
+ case 'Program':
+ return node.body;
+ case 'StaticClassBlock':
+ return [node.body];
+ case 'ClassField':
+ return [node.name, node.init];
+ case 'CallExpression':
+ case 'NewExpression':
+ case 'OptionalCallExpression':
+ case 'TaggedTemplate':
+ return [node.callee, ...node.arguments];
+ case 'CatchClause':
+ return [node.body, node.guard].filter(n => !!n);
+ case 'ClassExpression':
+ case 'ClassStatement':
+ return [...node.body, node.superClass].filter(n => !!n);
+ case 'ClassMethod':
+ return [node.name, node.body];
+ case 'ComprehensionExpression':
+ case 'GeneratorExpression':
+ return [node.body, ...node.blocks, node.filter].filter(n => !!n);
+ case 'ComprehensionIf':
+ return [node.test];
+ case 'ComputedName':
+ return [node.name];
+ case 'ConditionalExpression':
+ case 'IfStatement':
+ return [node.test, node.consequent, node.alternate].filter(n => !!n);
+ case 'DoWhileStatement':
+ case 'WhileStatement':
+ return [node.body, node.test];
+ case 'ExportDeclaration':
+ return [node.declaration, node.source].filter(n => !!n);
+ case 'ImportDeclaration':
+ return [...node.specifiers, node.source];
+ case 'LetStatement':
+ return [...node.head, node.body];
+ case 'ExpressionStatement':
+ return [node.expression];
+ case 'ForInStatement':
+ case 'ForOfStatement':
+ return [node.body, node.left, node.right];
+ case 'ForStatement':
+ return [node.init, node.test, node.update, node.body].filter(n => !!n);
+ case 'LabeledStatement':
+ return [node.body];
+ case 'MemberExpression':
+ return [node.object, node.property];
+ case 'ObjectExpression':
+ case 'ObjectPattern':
+ return node.properties;
+ case 'OptionalExpression':
+ return [node.expression];
+ case 'OptionalMemberExpression':
+ return [node.object, node.property];
+ case 'Property':
+ case 'PrototypeMutation':
+ return [node.value];
+ case 'ReturnStatement':
+ case 'ThrowStatement':
+ case 'UnaryExpression':
+ case 'UpdateExpression':
+ case 'YieldExpression':
+ return node.argument ? [node.argument] : [];
+ case 'SequenceExpression':
+ return node.expressions;
+ case 'SpreadExpression':
+ return [node.expression];
+ case 'SwitchCase':
+ return [node.test, ...node.consequent].filter(n => !!n);
+ case 'SwitchStatement':
+ return [node.discriminant, ...node.cases];
+ case 'TryStatement':
+ return [node.block, node.handler, node.finalizer].filter(n => !!n);
+ case 'VariableDeclaration':
+ return node.declarations;
+ case 'VariableDeclarator':
+ return node.init ? [node.init] : [];
+ case 'WithStatement':
+ return [node.object, node.body];
+ default:
+ print(`Ignoring ${node.type}, you should probably fix this in the script`);
+ }
+}
+
+function walkDir(dir) {
+ os.file.listDir(dir).forEach(child => {
+ if (child.startsWith('.'))
+ return;
+
+ const path = os.path.join(dir, child);
+ const relativePath = path.replace(`${root}/`, '');
+ if (excludedFiles.has(relativePath))
+ return;
+
+ if (!child.endsWith('.js')) {
+ try {
+ walkDir(path);
+ } catch (e) {
+ // not a directory
+ }
+ return;
+ }
+
+ try {
+ const script = os.file.readFile(path);
+ const ast = Reflect.parse(script);
+ walkAst(ast, findGettextCalls);
+ } catch (e) {
+ foundFiles.add(path);
+ }
+ });
+}
+
+walkDir(root);
+
+if (foundFiles.size === 0)
+ quit(0);
+
+print('The following files are missing from po/POTFILES.in:')
+foundFiles.forEach(f => print(` ${f}`));
+quit(1);