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

core(i18n): localize strings at end of run #5655

Merged
merged 16 commits into from
Jul 23, 2018

Conversation

patrickhulce
Copy link
Collaborator

An example of how we might be able to "fix" the strings before shipping the LHR, basically the i18n formatting function just returns a reference to the string to be used which is stored in a map, then at the end before returning, runner asks i18n to replace all the references in the LHR with the string of the appropriate locale. BOOM done! a log is kept of what replacements were made and added to the LHR as localeLog so that later the report could be translated again without re-running.

image

keyUsages.push({key, template, values});
formattedStringUsages.set(key, keyUsages);

return `${key}#${keyUsages.length - 1}`;
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

just a reference is returned here instead of the real string

const occurrences = templateLogRecord.occurrences || []
occurrences.push({values: usage.values, path: currentPath})

const {message, template} = formatTemplate(locale, templateKey, usage.template, usage.values);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

we do the real formatting here in the specified locale

}
]
},
"lighthouse-core/lib/i18n.js!#ms": {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

a log keeps track of all the strings we used

"values": {
"timeInMs": 4927.278
},
"path": [
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

as well as the path in the LHR of where you can find them to replace them

@@ -3416,5 +3416,144 @@
"title": "Crawling and Indexing",
"description": "To appear in search results, crawlers need access to your app."
}
},
"localeLog": {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

this will get big, but maybe it can be an optional thing?

Copy link
Member

Choose a reason for hiding this comment

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

Yah +1 to this being an option that's off by default.

Copy link
Member

@paulirish paulirish 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 where things are going here. :D

@@ -406,3 +414,7 @@ module.exports = {
},
},
};

Object.defineProperty(module.exports, 'UIStrings', {
Copy link
Member

Choose a reason for hiding this comment

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

can you add a comment on why we export it all fancy like this?
( i dont totally grok why.. :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

sure :)

it's because config data gets cloned and validated and whatnot so we want a getter and not put the real strings on the actual object

@@ -3416,5 +3416,144 @@
"title": "Crawling and Indexing",
"description": "To appear in search results, crawlers need access to your app."
}
},
"localeLog": {
Copy link
Member

Choose a reason for hiding this comment

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

Yah +1 to this being an option that's off by default.

}
]
},
"lighthouse-core/lib/i18n.js!#columnURL": {
Copy link
Member

Choose a reason for hiding this comment

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

how about JSONPath?

    "lighthouse-core/lib/i18n.js!#columnURL": {
      "occurrences": [
        {
          "path": [
            "audits",
            "render-blocking-resources",
            "details",
            "headings",
            "0",
            "label"
          ]
        }
      ]
    },

could become:

    "lighthouse-core/lib/i18n.js!#columnURL": {
      "occurrences": ["$.audits['render-blocking-resources'].details.headings[0].label"],
    },

obviously it's more dense, but I think its a lot more readable.

(actually looks like lodash's get also supports this style, anyhow.)
We don't really need ALL of jsonpath, just the basics.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

do you want JSONPath or what lodash get supports? ;)

I'm on board for this 👍

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I should've said, "I'm on board for this, so long as we use someone else's getter code :)"

Copy link
Member

Choose a reason for hiding this comment

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

sg! based on their docs, the string form with lodash get lgtm

@patrickhulce patrickhulce force-pushed the i18n_core_but_replaceable_spike branch from 3df449a to f555090 Compare July 18, 2018 00:34
@patrickhulce patrickhulce changed the base branch from i18n_spike to master July 18, 2018 00:34
@patrickhulce patrickhulce changed the title WIP spike into config i18n and post-run translation core(i18n): localize strings at end of run Jul 18, 2018
@GoogleChrome GoogleChrome deleted a comment from googlebot Jul 18, 2018

/* eslint-disable max-len */
const UIStrings = {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

FWIW @brendankenny I think these are the only strings we really need for performance section, so it's relatively small

if you want to be noodling on a much better config solution before we start on the other categories, nows a good time :)

*/

/** @type {Map<string, StringUsage[]>} */
const formattedStringUsages = new Map();
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

this is the new singleton, we might want to put some disclaimers into README somewhere about YMMV using LH as a long-lived module not in a worker. CLI/DevTools/Extension all don't really care about these types of concerns so inevitably we'll make some trade-offs not great for node module users (example: #5683)

@patrickhulce
Copy link
Collaborator Author

I think this is ready for ready for re-review.

I'm going to throw one more thought out there as I was thinking about report strings. instead of lhr.localeLog...

lhr.localization = {
  log: {...},
  uiMessages: {},
}

thoughts?

typings/lhr.d.ts Outdated
@@ -6,6 +6,12 @@

declare global {
module LH {
export type LocaleLogEntry = string | {path: string, values: any};

export interface LocaleLog {
Copy link
Member

Choose a reason for hiding this comment

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

Naming bikeshed plz? 😁

Doesn't feel like a log. More of a map? or just locations? how about.......

lhr.i18n.messageLocations
lhr.i18n.uiMessages

Copy link
Collaborator Author

@patrickhulce patrickhulce Jul 20, 2018

Choose a reason for hiding this comment

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

how about

lhr.i18n.messages

I has saving uiMessages for the strings that the renderer will use but are in the LHR anywhere otherwise

:)

Copy link
Member

Choose a reason for hiding this comment

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

yup sg

}
}

return pathAsString;
}

module.exports = {
Copy link
Member

Choose a reason for hiding this comment

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

the export style is a little funky here. starts halfway and nests these other fns, but not formatPathAsString

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

it was whatever is used inside the module gets defined outside everything else is inline, I'll just move it all out though

* @param {string} template
* @param {*} [values]
*/
function formatTemplate(locale, templateKey, template, values) {
Copy link
Member

Choose a reason for hiding this comment

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

function _formatTemplate

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done

}

/** @param {string[]} path */
function formatPathAsString(path) {
Copy link
Member

Choose a reason for hiding this comment

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

also with a _ prefix? (even though we export for testing)?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done

const formatter = new MessageFormat(messageForMessageFormat, localeForMessageFormat, formats);
return formatter.format(values);
const unixStyleFilename = path.relative(LH_ROOT, filenameToLookup).replace(/\\/g, '/');
const key = unixStyleFilename + '!#' + keyname;
Copy link
Member

Choose a reason for hiding this comment

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

thinking we should use stronger delimiters. How about |.. e.g.

"lighthouse-core/audits/metrics/interactive.js | description"

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

sg, done

const unixStyleFilename = path.relative(LH_ROOT, filenameToLookup).replace(/\\/g, '/');
const key = unixStyleFilename + '!#' + keyname;
const keyUsages = formattedStringUsages.get(key) || [];
keyUsages.push({key, template, values});
Copy link
Member

Choose a reason for hiding this comment

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

There's something funky going on here:

image

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

how did you get that? are you looking at the test output? the tests invoke FCP .meta lots of times so that's probably what you're seeing

Copy link
Member

Choose a reason for hiding this comment

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

yup it's the getter of meta, as you suspected. we'll have to dedupe.

return formatter.format(values);
const unixStyleFilename = path.relative(LH_ROOT, filenameToLookup).replace(/\\/g, '/');
const key = unixStyleFilename + '!#' + keyname;
const keyUsages = formattedStringUsages.get(key) || [];
Copy link
Member

Choose a reason for hiding this comment

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

(related to next comment, but..)

why are there multiple usages of a given string ID? Well I guess i'd expect it of the i18n ones, but not typically the strings held in the audits, right?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

yeah it'd be unexpected for more than 1 audit title to be used, but for strings that will be used for table cells it will be expected

keyUsages.push({key, template, values});
formattedStringUsages.set(key, keyUsages);

return `${key}#${keyUsages.length - 1}`;
Copy link
Member

Choose a reason for hiding this comment

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

similar to previous one. giving this some space.. ${key} # {length - 1}

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

sg done

/**
* @param {*} objectInLHR
* @param {LH.LocaleLog} log
* @param {string[]} path
Copy link
Member

Choose a reason for hiding this comment

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

only for testing?

Copy link
Collaborator Author

@patrickhulce patrickhulce Jul 20, 2018

Choose a reason for hiding this comment

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

are you referring to why path is an array of strings in this function signature? it's so that we can easily push onto it and convert the name at each step rather than have two functions that create the full string from an array and one that builds incrementally

setLocale(newLocale) {
if (!newLocale) return;
locale = newLocale;
replaceLocaleStringReferences(lhr, locale) {
Copy link
Member

Choose a reason for hiding this comment

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

naming

okay we have:
string IDs?
string references?
object paths?

i'm also seeing: template key, key name, locale string reference, keyusages / usages.

can we make these names a bit more concrete and consistent?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

yeah this is a bit confusing, I'll do a pass to try to make them all consistent

@patrickhulce
Copy link
Collaborator Author

alright @paulirish I think the names have been sorted out except this area https://github.com/GoogleChrome/lighthouse/pull/5655/files#diff-7e3be24bffdebd4c067d12d0596858f9R171

now that it's not log and we also have usages saved in our formattedStringUsages not sure what to call these two different things

@patrickhulce patrickhulce force-pushed the i18n_core_but_replaceable_spike branch from 16b49ce to 24b4e24 Compare July 20, 2018 18:29
Copy link
Member

@paulirish paulirish left a comment

Choose a reason for hiding this comment

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

New namessss

formatter 👍
uiStrings 👍
rename 'template'.
kill 'localeString'
rename message in _formatTemplate => formattedString
createStringFormatter -> createMessageInstanceIdFn
maybe 'usages' -> 'instances', but nobigdeal

icuMessage
icuMessageId
icuMessageInstanceId

formattedString

icuMessageInstanceIdMap. Map(<icuMessageID, [*]>)

lhr.i18n.icuMessagePaths
lhr.i18n.rendererFormattedStrings

typings/lhr.d.ts Outdated
@@ -6,6 +6,12 @@

declare global {
module LH {
export type LocaleLogEntry = string | {path: string, values: any};

export interface LocaleLog {
Copy link
Member

Choose a reason for hiding this comment

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

yup sg

const unixStyleFilename = path.relative(LH_ROOT, filenameToLookup).replace(/\\/g, '/');
const key = unixStyleFilename + '!#' + keyname;
const keyUsages = formattedStringUsages.get(key) || [];
keyUsages.push({key, template, values});
Copy link
Member

Choose a reason for hiding this comment

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

yup it's the getter of meta, as you suspected. we'll have to dedupe.

* @param {string} templateID
* @param {string} template
* @param {*} [values]
*/
Copy link
Member

Choose a reason for hiding this comment

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

can you add a @return

@@ -43,57 +41,150 @@ const formats = {

/**
* @param {string} msg
* @param {Record<string, *>} values
* @param {Record<string, *>} [values]
*/
function preprocessMessageValues(msg, values) {
Copy link
Member

Choose a reason for hiding this comment

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

_preprocessMessageValues

const currentPathInLHR = pathInLHR.concat([property]);

// Check to see if the value in the LHR looks like a string reference. If it is, replace it.
if (typeof value === 'string' && /.* \| .* # \d+$/.test(value)) {
Copy link
Member

Choose a reason for hiding this comment

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

wanna throw in the param groups that you have on 177 so these look more similar?

@paulirish
Copy link
Member

paulirish commented Jul 21, 2018

@brendankenny u want to take a pass?

@@ -20,6 +19,8 @@ try {

// @ts-ignore
const IntlPolyfill = require('intl');
if (!IntlPolyfill.NumberFormat) throw new Error('Invalid polyfill');
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@paulirish FYI this was my fix to what you discovered in #5702

Copy link
Member

Choose a reason for hiding this comment

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

ah cool. nice.

The msg is a little weird though. Seems like this case is described as: "Polyfill is a no-op due to existing implementation". The fact that we exit via throwing is a little random. can we do a IIFE and return instead?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

yeah IFFE makes a lot more sense, done 👍

@paulirish
Copy link
Member

👍

@patrickhulce patrickhulce merged commit 4e11f25 into master Jul 23, 2018
@paulirish paulirish deleted the i18n_core_but_replaceable_spike branch January 11, 2019 02:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants