-
-
Notifications
You must be signed in to change notification settings - Fork 639
/
check-messages-en
executable file
·214 lines (188 loc) · 6.77 KB
/
check-messages-en
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
#!/usr/bin/env node
// TODO: Type-check this file
// Sadly our auto-format doesn't seem to run eslint on this file; give in to
// Prettier here.
// TODO: debug
/* eslint-disable operator-linebreak */
const fs = require('fs');
const path = require('path');
const { namedTypes: n, visit } = require('ast-types');
const flowParser = require('flow-parser');
const { parse } = require('recast');
const messages_en = require('../static/translations/messages_en.json');
/**
* Make a list of files that might contain UI strings, by recursing in src/.
*/
function getPossibleUiStringFilePaths() {
const result = [];
const kSrcDirName = 'src/';
function walk(dir, _dirName = '') {
let dirent;
// eslint-disable-next-line no-cond-assign
while ((dirent = dir.readSync())) {
// To reduce false negatives, `continue` when nothing in `dirent` can
// cause UI strings to appear in the app.
if (dirent.isFile()) {
if (!dirent.name.endsWith('.js')) {
// Non-JS code, and Flow type definitions in .js.flow files.
continue;
}
result.push(path.join(kSrcDirName, _dirName, dirent.name));
} else if (dirent.isDirectory()) {
const subdirName = path.join(_dirName, dirent.name);
// e.g., …/__tests__ and …/__flow-tests__
if (subdirName.endsWith('tests__')) {
// Test code.
continue;
}
walk(fs.opendirSync(path.join(kSrcDirName, subdirName)), subdirName);
} else {
// Something we don't expect to find under src/, probably containing
// no UI strings. (symlinks? fifos, sockets, devices??)
continue;
}
}
}
walk(fs.opendirSync(kSrcDirName));
return result;
}
const parseOptions = {
parser: {
parse(src) {
return flowParser.parse(src, {
// Comments can't cause UI strings to appear in the app; ignore them.
all_comments: false,
comments: false,
// We use Flow enums; the parser shouldn't crash on them.
enums: true,
// Set `tokens: true` just to work around a mysterious error.
//
// From the doc for this option:
//
// > include a list of all parsed tokens in a top-level tokens
// > property
//
// We don't actually want this list of tokens. String literals do
// get represented in the list, but as tokens, i.e., meaningful
// chunks of the literal source code. They come with surrounding
// quotes, escape syntax, etc:
//
// 'doesn\'t'
// "doesn't"
//
// What we really want is the *value* of a string literal:
//
// doesn't
//
// and we get that from the AST.
//
// Anyway, we set `true` for this because otherwise I've been seeing
// `parse` throw an error:
//
// Error: Line 72: Invalid regular expression: missing /
//
// TODO: Debug and/or file an issue upstream.
tokens: true,
});
},
},
};
/**
* Look at all given files and collect all strings that might be UI strings.
*
* The result will include non-UI strings because we can't realistically
* filter them all out. That means, when the caller checks messages_en
* against these strings, it could get false negatives: perhaps messages_en
* has a string 'message-empty' (why would it, though), and that string
* won't be flagged because it appears as an enum value in ComposeBox.
*
* To reduce these false negatives, we filter out low-hanging fruit, like
* the string 'foo' in `import Foo from 'foo'`.
*/
function getPossibleUiStrings(possibleUiStringFilePaths) {
const result = new Set();
possibleUiStringFilePaths.forEach(filePath => {
const source = fs.readFileSync(filePath).toString();
const ast = parse(source, parseOptions);
visit(ast, {
// Find nodes with type "Literal" in the AST.
/* eslint-disable no-shadow */
visitLiteral(path) {
const { value } = path.value;
if (
// (Non-string literals include numbers, booleans, etc.)
typeof value === 'string' &&
// This string isn't like 'react' in `import React from 'react'`.
!n.ImportDeclaration.check(path.parent.value)
) {
result.add(value);
}
// A literal is always a leaf, right? No need to call this.traverse.
return false;
},
// Find nodes with type "TemplateLiteral" in the AST. We sometimes use
// template literals in UI strings for readability.
/* eslint-disable no-shadow */
visitTemplateLiteral(path) {
if (
// Translatable UI strings are unlikely to contain
// sub-expressions.
//
// Also, if a template literal has nontrivial sub-expressions, we
// can't reasonably interpret them here anyway.
path.value.quasis.length === 1 &&
path.value.expressions.length === 0
) {
result.add(path.value.quasis[0].value.cooked);
}
return this.traverse(path);
},
});
});
return result;
}
function main() {
let didError = false;
// We use a convention where a message's ID matches its value; a mismatch
// is probably accidental.
const mismatchedMessageEntries = Object.entries(messages_en).filter(
([messageId, message]) => messageId !== message,
);
if (mismatchedMessageEntries.length > 0) {
console.error(
'Each message in static/translations/messages_en.json should match its ID, but some do not:',
);
console.error(mismatchedMessageEntries);
didError = true;
}
// Mobile's style is to use curly quotes. They look nicer, but also: since
// v3, react-intl uses the single straight quote as an escape character:
// https://formatjs.io/docs/intl-messageformat/#features
const messagesWithStraightQuotes = Object.values(messages_en).filter(message =>
/['"]/.test(message),
);
if (messagesWithStraightQuotes.length > 0) {
console.error(
'Found messages in static/translations/messages_en.json that have straight quotes. Please use smart quotes: “” and ‘’.',
);
console.error(messagesWithStraightQuotes);
didError = true;
}
const possibleUiStrings = getPossibleUiStrings(getPossibleUiStringFilePaths());
// Check each key ("message ID" in formatjs's lingo) against
// possibleUiStrings, and make a list of any that aren't found.
const danglingMessageIds = Object.keys(messages_en).filter(
messageId => !possibleUiStrings.has(messageId),
);
if (danglingMessageIds.length > 0) {
console.error(
"Found message IDs in static/translations/messages_en.json that don't seem to be used in the app:",
);
console.error(danglingMessageIds);
didError = true;
}
if (didError) {
process.exit(1);
}
}
main();