Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

i18n: use tsc to collect UIStrings #9487

Merged
merged 4 commits into from
Aug 1, 2019
Merged

i18n: use tsc to collect UIStrings #9487

merged 4 commits into from
Aug 1, 2019

Conversation

brendankenny
Copy link
Member

@brendankenny brendankenny commented Jul 30, 2019

moves from using esprima to the typescript compiler to extract UIStrings from our files. Discussed this with @exterkamp ahead of time, though this PR grew a little in the telling. Main benefits:

  • eliminates an aging and seldom updated dependency for one that we keep updated and we already know can parse our source
  • tsc automatically extracts jsdoc strings including putting @ tags in separate vars, eliminating one parse step and having to keep track of comment start/end positions
  • automatically does some format corrections, e.g. removes * and starting whitespace from jsdoc strings, joins across newlines, etc
  • is actually typed :)

also reorganized a bit so the parsing/ast walking is split off in a separate function, allowing testing to be a bit easier (and laying the groundwork for exposing as a utility for third-party plugins, etc, to use if they wish)

@brendankenny
Copy link
Member Author

brendankenny commented Jul 30, 2019

the changes to collectAllStringsInDir are easier to read if you hide whitespace changes, as a lot of it is removing a level of indentation

}

// @ts-ignore regex just matched
Copy link
Member Author

@brendankenny brendankenny Jul 30, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rearranged the ifs above to avoid this

createPsuedoLocaleStrings,
convertMessageToPlaceholders: convertMessageToCtc,
convertMessageToCtc,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it didn't make sense to rename this just for exporting for tests, but missed in the review for #9114 :)

// @ts-ignore - esprima's type definition is supremely lacking
const ast = esprima.parse(justUIStrings, {comment: true, range: true});

for (const stmt of ast.body) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there's only ever one stmt (if we did our extraction correctly), so no need to loop :)

Copy link
Collaborator

@patrickhulce patrickhulce left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like it! Looks pretty good, I think the dependency change for me mentally moves this script officially from rag-tag hacky solution to realistic code we support 😄

lighthouse-core/scripts/i18n/collect-strings.js Outdated Show resolved Hide resolved
lighthouse-core/scripts/i18n/collect-strings.js Outdated Show resolved Hide resolved
lighthouse-core/scripts/i18n/collect-strings.js Outdated Show resolved Hide resolved
lighthouse-core/scripts/i18n/collect-strings.js Outdated Show resolved Hide resolved

// @ts-ignore - Not part of the public tsc interface yet.
const jsDocComments = tsc.getJSDocCommentsAndTags(property);
if (jsDocComments.length === 0) throw new Error(`No Description for message "${message}"`);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we get rid of the message inside computeDescription then and make this the "Missing description comment" one?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we get rid of the message inside computeDescription then and make this the "Missing description comment" one?

hmm, do you mean like check that either ast.comment or ast.tags is defined here? Or output an empty description from computeDescription and throw here if it's the empty string?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah I meant this is a duplicate case and error message of the fallback in computeDescription, could computeDescription accept ast|undefined and do the check there?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could computeDescription accept ast|undefined and do the check there?

got it

lighthouse-core/test/scripts/i18n/collect-strings-test.js Outdated Show resolved Hide resolved
@patrickhulce
Copy link
Collaborator

eliminates an aging and seldom updated dependency

Kinda crazy that esprima has more than double weekly downloads than typescript still o.O

Copy link
Member

@exterkamp exterkamp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love it! 👋 esprima.

lighthouse-core/scripts/i18n/collect-strings.js Outdated Show resolved Hide resolved
for (const tag of ast.tags) {
const comment = coerceToSingleLineAndTrim(tag.comment);

if (tag.tagName.text === 'description') {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this throw if we encounter a non-standard tag?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this throw if we encounter a non-standard tag?

I think most of the time we don't care, and if someone really wanted to add a different tag to communicate something else, they could. I could see someone in a plugin adding an @const to the strings and then wanting us to still support them with collect-strings. It seems like a corner case, but otoh if someone misspells @description or something it will be picked up by the missing description error.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's fair. I'm not sure I'm convinced, but I guess there isn't much harm in letting new tags roam free.

lighthouse-core/scripts/i18n/collect-strings.js Outdated Show resolved Hide resolved
* @return {{placeholderName: string, exampleValue: string}}
*/
function parseAtExample(rawExample) {
const match = rawExample.match(/^{(?<exampleValue>[^}]+)} (?<placeholderName>.+)$/);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, at this point, the raw example is in the form "{exampleValue} placeholderName"? The @tag is gone? Seems like this wouldn't be the case based on the function description.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, at this point, the raw example is in the form "{exampleValue} placeholderName"? The @tag is gone? Seems like this wouldn't be the case based on the function description.

good call. I reworded to make it clearer what it was parsing, but also left the @example context so it's not pretending to not be used for that specific purpose.

lighthouse-core/test/scripts/i18n/collect-strings-test.js Outdated Show resolved Hide resolved
const liveUIStrings = evalJustUIStrings(justUIStrings);

expect(() => collect.parseUIStrings(justUIStrings, liveUIStrings))
.toThrow(/Incorrectly formatted @example/);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this error descriptive enough inside a stack trace?
image

I think it needs more context around it, what message was bad, or what @example text it was iterating over. This would help immediately know what is wrong.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it needs more context around it, what message was bad, or what @example text it was iterating over. This would help immediately know what is wrong.

good point. added example string to error message


if (regexMatches && !exportsUIStrings) {
throw new Error('UIStrings defined but not exported');
if (!regexMatch) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dunno if it's worth it, but can this be a XOR then a ternary on which error to throw? Ignore if it's pointless, but this kind of boolean logic is deceptively simple, but yet hard to follow.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dunno if it's worth it, but can this be a XOR then a ternary on which error to throw? Ignore if it's pointless, but this kind of boolean logic is deceptively simple, but yet hard to follow.

that sounds more complicated, but I added a comment and an error message tweak to try to make it clearer :)

Copy link
Collaborator

@patrickhulce patrickhulce left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

const {placeholderName, exampleValue} = parseExampleJsDoc(comment);
examples[placeholderName] = exampleValue;
} else {
throw new Error('bad tag' + tag.tagName.text);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
throw new Error('bad tag' + tag.tagName.text);
throw new Error(`Unexpected tagName "@${tag.tagName.text}"`);

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haha, whoops, I threw that in there to see how hard it was to accidentally trigger that case with reasonable typos and then I guess accidentally committed it. Maybe we'll go with throwing in that case after all :)

*/
function coerceToSingleLineAndTrim(comment = '') {
// Line breaks within a jsdoc comment should always be replaceable with a space.
return comment.replace(/\n/g, ' ').trim();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW I tend to go with /\n+/g, ' ' if i'm turning into single line in other contexts, not sure if this is different

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW I tend to go with /\n+/g, ' ' if i'm turning into single line in other contexts, not sure if this is different

it might be stretching interpreting a string with multiple line breaks in it as still the same jsdoc string but I think it's the only thing that makes sense here, so, sounds good :)

Copy link
Member

@exterkamp exterkamp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, my nits were addressed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants