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): initial utility library #5644

Merged
merged 6 commits into from
Jul 17, 2018
Merged

core(i18n): initial utility library #5644

merged 6 commits into from
Jul 17, 2018

Conversation

patrickhulce
Copy link
Collaborator

@patrickhulce patrickhulce commented Jul 9, 2018

a slightly different take on the dynamic approach we ditched earlier.

I wanted to pitch this one more time because I ran into 2 issues with the static approach that felt 😝
also @paulirish likes this one more :)

} delayed first paint by {timeInMs, number, milliseconds} ms`
};

const i18n = i18nUtils.createStringFormatter(__filename, UI_STRINGS);
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 a formatting function that has context bundled in explicitly via __filename and UI_STRINGS

{key: 'url', valueType: 'url', label: 'URL'},
{key: 'totalBytes', valueType: 'bytes', label: 'Size (KB)'},
{key: 'wastedMs', valueType: 'timespanMs', label: 'Download Time (ms)'},
{key: 'url', valueType: 'url', label: i18n(i18nUtils.UI_STRINGS.columnURL)},
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

global shared UI_STRINGS can be used too

@@ -34,6 +35,7 @@ const Config = require('./config/config');
async function lighthouse(url, flags, configJSON) {
// TODO(bckenny): figure out Flags types.
flags = flags || /** @type {LH.Flags} */ ({});
i18n.setLocale(flags.locale || 'en-US');
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

all it takes to swap locales at runtime is a single call here

Copy link
Member

Choose a reason for hiding this comment

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

we can use something like this https://github.com/brandonhorst/javascript-environment-lang/blob/master/index.js before we fall back to english. (well i suppose it wouldn't ever fall back)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

oh yeah this was me being lazy for WIP, I'd have setLocale just use the default locale determined by MessageFormat.defaultLocale when this is undefined

if (!keyname) throw new Error(`Could not locate: ${msg}`);

const parsed = MessageParser.parse(msg);
parsed.elements
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

by forcing the use of our own format function first, we can do things like preserve our fancy rounding logic for milliseconds, automatically convert bytes to KB, etc.

Copy link
Member

Choose a reason for hiding this comment

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

sg. i like it. let's split into its own fn

'lighthouse-extension',
]

function collectAllStringsInDir(dir) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

simple script recurses the directories looking for strings

fs.writeFileSync(path.join(LH_ROOT, 'lighthouse-core/lib/locales/en-US.js'), 'module.exports = ' + JSON.stringify(strings, null, 2) + ';')

const gibberish = {}
for (const [key, value] of Object.entries(strings)) {
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's easy to generate other locales

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.

this feels great so far

const fs = require('fs')
const path = require('path')

const LH_ROOT = path.join(__dirname, '../../')
Copy link
Member

Choose a reason for hiding this comment

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

lets put this file in scripts/i18n/

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

@@ -34,6 +35,7 @@ const Config = require('./config/config');
async function lighthouse(url, flags, configJSON) {
// TODO(bckenny): figure out Flags types.
flags = flags || /** @type {LH.Flags} */ ({});
i18n.setLocale(flags.locale || 'en-US');
Copy link
Member

Choose a reason for hiding this comment

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

we can use something like this https://github.com/brandonhorst/javascript-environment-lang/blob/master/index.js before we fall back to english. (well i suppose it wouldn't ever fall back)


fs.writeFileSync(path.join(LH_ROOT, 'lighthouse-core/lib/locales/en-US.js'), 'module.exports = ' + JSON.stringify(strings, null, 2) + ';')

const gibberish = {}
Copy link
Member

Choose a reason for hiding this comment

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

wrap into a createPsuedoLocaleStrings fn (or 'gibberish')

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

collectAllStringsInDir(path.join(LH_ROOT, 'lighthouse-core'))
console.log('Done!')

fs.writeFileSync(path.join(LH_ROOT, 'lighthouse-core/lib/locales/en-US.js'), 'module.exports = ' + JSON.stringify(strings, null, 2) + ';')
Copy link
Member

Choose a reason for hiding this comment

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

template literal for this multiline thang

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 mod = require(fullPath)
if (!mod.UI_STRINGS) continue
for (const [key, value] of Object.entries(mod.UI_STRINGS)) {
strings[`${relativePath}!#${key}`] = value
Copy link
Member

Choose a reason for hiding this comment

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

wdyt about just adopting the basic chrome i18n format here:

        "relativePathAndKey": {
          "message": "stringValue",
          // "description": "..."
        },

Like.. we could translate again into that format, but perhaps its nicer to just start with it here.
also it looks like git diff doesnt handle small changes in these strings files well. considers the WHOLE block to be a change.

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 sg, done

* @param {string} filename
* @param {Record<string, string>} localStrings
*/
createStringFormatter(filename, localStrings) {
Copy link
Member

Choose a reason for hiding this comment

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

localStrings => fileStrings ? or moduleStrings

local had me thinking local to the i18n file for a sec..

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

totally 👍 done

createStringFormatter(filename, localStrings) {
const mergedStrings = {...UI_STRINGS, ...localStrings};
/** @param {string} msg @param {*} [values] */
const format = (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.

formatFn (or w/e) to differentiate from all the other 'format's in this file

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 lookupKey = path.relative(LH_ROOT, filenameToLookup) + '!#' + keyname;
const localeStrings = LOCALES[locale] || {};
const localeForMessageFormat = locale === 'gibberish' ? 'de-DE' : locale;
const formatter = new MessageFormat(localeStrings[lookupKey] || msg, localeForMessageFormat, formats);
Copy link
Member

Choose a reason for hiding this comment

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

so we prefer to pull it from the locale file, but if its not present we fall back to the msg passed in? can you comment why we want that fallback behavior?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

totes

const filenameToLookup = keyname in UI_STRINGS ? __filename : filename;
const lookupKey = path.relative(LH_ROOT, filenameToLookup) + '!#' + keyname;
const localeStrings = LOCALES[locale] || {};
const localeForMessageFormat = locale === 'gibberish' ? 'de-DE' : locale;
Copy link
Member

Choose a reason for hiding this comment

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

g3 uses "en-XA" for psuedo locales. (and also en-XB and en-XC)

	"en-XA",  # Accented English

i'm okay doing that replacement here, though we could just switch to 'en-XA' everywhere now.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

perfect, sg 👍

if (!keyname) throw new Error(`Could not locate: ${msg}`);

const parsed = MessageParser.parse(msg);
parsed.elements
Copy link
Member

Choose a reason for hiding this comment

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

sg. i like it. let's split into its own fn

.forEach(el => (values[el.id] = values[el.id] / 1024));

const filenameToLookup = keyname in UI_STRINGS ? __filename : filename;
const lookupKey = path.relative(LH_ROOT, filenameToLookup) + '!#' + keyname;
Copy link
Member

Choose a reason for hiding this comment

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

btw is !# a convention?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

not that I know of, just seemed like some good characters that won't appear in a valid path and look good for separating things :)

@patrickhulce patrickhulce changed the title [WIP] core: i18n spike core(i18n): initial utility library Jul 10, 2018
@@ -1940,7 +1937,7 @@
{
"key": "wastedMs",
"valueType": "timespanMs",
"label": "Download Time (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.

And so the string consolidation begins :)

@@ -39,6 +39,7 @@ const defaultSettings = {

// the following settings have no defaults but we still want ensure that `key in settings`
// in config will work in a typechecked way
locale: null, // default determined by the intl library
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Hm, I'm not 100% sure about this anymore. It might make more sense to default to en-US and then explicitly warn when folks try to change to a locale we don't have

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 WDYT?

Copy link
Member

Choose a reason for hiding this comment

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

we don't need to block this PR on this decision IMO.

but we should be sniffing the user's locale somewhere, right? in both the CLI and browser case.

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 sg we can punt

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 sg we can punt

@paulirish
Copy link
Member

Shall we land this so we can start expanding our coverage?

{key: 'url', valueType: 'url', label: 'URL'},
{key: 'totalBytes', valueType: 'bytes', label: 'Size (KB)'},
{key: 'wastedMs', valueType: 'timespanMs', label: 'Download Time (ms)'},
{key: 'url', valueType: 'url', label: i18n(i18nUtils.UIStrings.columnURL)},
Copy link
Member

Choose a reason for hiding this comment

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

naming-wise, how about: uiS(i18n.UIStrings.columnURL)
bikeshedding wise, "i18n" feels most like a namespace so i hesitate to use it for a method name.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

chatted outside and went with str_ for simplicity and easy future refactoring :)

@patrickhulce
Copy link
Collaborator Author

Shall we land this so we can start expanding our coverage?

let's!

@patrickhulce
Copy link
Collaborator Author

just need an approval ;)

@paulirish paulirish merged commit 589162b into master Jul 17, 2018

/**
* @param {string} msg
* @param {Record<string, *>} values
Copy link
Member

Choose a reason for hiding this comment

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

* could be more specific? (just numbers)

Copy link
Member

Choose a reason for hiding this comment

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

I guess only numbers touched inside this function, but could have other types passed in (just not for ms or bytes), so that wouldn't work. We could probably get more clever here, though, since only a subset of them are going to support being preprocessed. Otherwise we lose all checking on these

createStringFormatter(filename, fileStrings) {
const mergedStrings = {...UIStrings, ...fileStrings};

/** @param {string} msg @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.

same

const localeString = localeStrings[lookupKey] && localeStrings[lookupKey].message;
// fallback to the original english message if we couldn't find a message in the specified locale
// better to have an english message than no message at all, in some number cases it won't even matter
const messageForMessageFormat = localeString || msg;
Copy link
Member

Choose a reason for hiding this comment

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

Should we differentiate between cases where there is no particular string for that locale vs there are no strings at all for that locale? If a particular string is missing in a locale should that be an error?

/**
* @param {LH.Locale|null} [newLocale]
*/
setLocale(newLocale) {
Copy link
Member

Choose a reason for hiding this comment

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

singletons :(

Copy link
Member

Choose a reason for hiding this comment

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

(I guess the only way to avoid is all translations could be done in the style of #5655 and just use the default locale until report generation time)

if (!newLocale) return;
locale = newLocale;
},
};
Copy link
Member

Choose a reason for hiding this comment

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

need tests for this :)

* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
// @ts-nocheck
Copy link
Member

Choose a reason for hiding this comment

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

problem here?

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.

3 participants