-
Notifications
You must be signed in to change notification settings - Fork 9.5k
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): add locale fallbacks when language not supported #5746
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,6 +10,7 @@ const isDeepEqual = require('lodash.isequal'); | |
const log = require('lighthouse-logger'); | ||
const MessageFormat = require('intl-messageformat').default; | ||
const MessageParser = require('intl-messageformat-parser'); | ||
const lookupClosestLocale = require('lookup-closest-locale'); | ||
const LOCALES = require('./locales'); | ||
|
||
const LH_ROOT = path.join(__dirname, '../../'); | ||
|
@@ -66,6 +67,24 @@ const formats = { | |
}, | ||
}; | ||
|
||
/** | ||
* Look up the best available locale for the requested language through these fall backs: | ||
* - exact match | ||
* - progressively shorter prefixes (`de-CH-1996` -> `de-CH` -> `de`) | ||
* - the default locale ('en-US') if no match is found | ||
* | ||
* If `locale` isn't provided, the default is used. | ||
* @param {string=} locale | ||
* @return {LH.Locale} | ||
*/ | ||
function lookupLocale(locale) { | ||
// TODO: could do more work to sniff out default locale | ||
const canonicalLocale = Intl.getCanonicalLocales(locale)[0]; | ||
|
||
const closestLocale = lookupClosestLocale(canonicalLocale, LOCALES); | ||
return closestLocale || 'en-US'; | ||
} | ||
|
||
/** | ||
* @param {string} icuMessage | ||
* @param {Record<string, *>} [values] | ||
|
@@ -118,7 +137,7 @@ const _icuMessageInstanceMap = new Map(); | |
* @return {{formattedString: string, icuMessage: string}} | ||
*/ | ||
function _formatIcuMessage(locale, icuMessageId, icuMessage, values) { | ||
const localeMessages = LOCALES[locale] || {}; | ||
const localeMessages = LOCALES[locale]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
const localeMessage = localeMessages[icuMessageId] && localeMessages[icuMessageId].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 | ||
|
@@ -150,15 +169,6 @@ function _formatPathAsString(pathInLHR) { | |
return pathAsString; | ||
} | ||
|
||
/** | ||
* @return {LH.Locale} | ||
*/ | ||
function getDefaultLocale() { | ||
const defaultLocale = MessageFormat.defaultLocale; | ||
if (defaultLocale in LOCALES) return /** @type {LH.Locale} */ (defaultLocale); | ||
return 'en-US'; | ||
} | ||
|
||
/** | ||
* @param {LH.Locale} locale | ||
* @return {LH.I18NRendererStrings} | ||
|
@@ -208,7 +218,7 @@ function createMessageInstanceIdFn(filename, fileStrings) { | |
|
||
/** | ||
* @param {string} icuMessageIdOrRawString | ||
* @param {LH.Locale} [locale] | ||
* @param {LH.Locale} locale | ||
* @return {string} | ||
*/ | ||
function getFormatted(icuMessageIdOrRawString, locale) { | ||
|
@@ -221,10 +231,10 @@ function getFormatted(icuMessageIdOrRawString, locale) { | |
|
||
/** | ||
* @param {string} icuMessageInstanceId | ||
* @param {LH.Locale} [locale] | ||
* @param {LH.Locale} locale | ||
* @return {{icuMessageInstance: IcuMessageInstance, formattedString: string}} | ||
*/ | ||
function _resolveIcuMessageInstanceId(icuMessageInstanceId, locale = 'en-US') { | ||
function _resolveIcuMessageInstanceId(icuMessageInstanceId, locale) { | ||
const matches = icuMessageInstanceId.match(MESSAGE_INSTANCE_ID_REGEX); | ||
if (!matches) throw new Error(`${icuMessageInstanceId} is not a valid message instance ID`); | ||
|
||
|
@@ -282,7 +292,7 @@ function replaceIcuMessageInstanceIds(lhr, locale) { | |
module.exports = { | ||
_formatPathAsString, | ||
UIStrings, | ||
getDefaultLocale, | ||
lookupLocale, | ||
getRendererFormattedStrings, | ||
createMessageInstanceIdFn, | ||
getFormatted, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,11 +3,17 @@ | |
* 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 | ||
'use strict'; | ||
|
||
module.exports = { | ||
/** @typedef {Record<string, {message: string}>} LocaleMessages */ | ||
|
||
/** @type {Record<LH.Locale, LocaleMessages>} */ | ||
const locales = { | ||
'ar': require('./ar-XB.json'), // TODO: fallback not needed when ar translation available | ||
'ar-XB': require('./ar-XB.json'), | ||
'en': require('./en-US.json'), // en-* fallback | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. both |
||
'en-US': require('./en-US.json'), | ||
'en-XA': require('./en-XA.json'), | ||
'ar-XB': require('./ar-XB.json'), | ||
}; | ||
|
||
module.exports = locales; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -32,7 +32,6 @@ class Runner { | |
try { | ||
const startTime = Date.now(); | ||
const settings = opts.config.settings; | ||
settings.locale = settings.locale || i18n.getDefaultLocale(); | ||
|
||
/** | ||
* List of top-level warnings for this Lighthouse run. | ||
|
@@ -218,7 +217,7 @@ class Runner { | |
*/ | ||
static async _runAudit(auditDefn, artifacts, settings, runWarnings) { | ||
const audit = auditDefn.implementation; | ||
const status = `Evaluating: ${i18n.getFormatted(audit.meta.title)}`; | ||
const status = `Evaluating: ${i18n.getFormatted(audit.meta.title, 'en-US')}`; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nice. good call. |
||
|
||
log.log('status', status); | ||
let auditResult; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
/** | ||
* @license Copyright 2018 Google Inc. All Rights Reserved. | ||
* 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. | ||
*/ | ||
'use strict'; | ||
|
||
const locales = require('../../../lib/locales/index.js'); | ||
const assert = require('assert'); | ||
|
||
/* eslint-env jest */ | ||
|
||
describe('locales', () => { | ||
it('has only canonical language tags', () => { | ||
for (const locale of Object.keys(locales)) { | ||
const canonicalLocale = Intl.getCanonicalLocales(locale)[0]; | ||
assert.strictEqual(locale, canonicalLocale); | ||
} | ||
}); | ||
|
||
it('has a base language prefix fallback for all supported languages', () => { | ||
for (const locale of Object.keys(locales)) { | ||
const basePrefix = locale.split('-')[0]; | ||
assert.ok(locales[basePrefix]); | ||
} | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,6 +13,7 @@ | |
"./typings" | ||
], | ||
|
||
"resolveJsonModule": true, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is for our messages files? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
yeah, no reason to keep tsc from not seeing them |
||
"diagnostics": true | ||
}, | ||
"include": [ | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
/** | ||
* @license Copyright 2018 Google Inc. All Rights Reserved. | ||
* 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. | ||
*/ | ||
|
||
declare module 'lookup-closest-locale' { | ||
function lookupClosestLocale(locale: string|undefined, available: Record<LH.Locale, any>): LH.Locale|undefined; | ||
|
||
export = lookupClosestLocale; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4168,6 +4168,10 @@ longest@^1.0.1: | |
version "1.0.1" | ||
resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" | ||
|
||
[email protected]: | ||
version "6.0.4" | ||
resolved "https://registry.yarnpkg.com/lookup-closest-locale/-/lookup-closest-locale-6.0.4.tgz#1279fed7546a601647bbc980f64423ee990a8590" | ||
|
||
loose-envify@^1.0.0: | ||
version "1.2.0" | ||
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.2.0.tgz#69a65aad3de542cf4ee0f4fe74e8e33c709ccb0f" | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
since
Config.initSettings
will never use this, what is it used forThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
since we declare the constants in this file to be a valid
Config.Settings
, unfortunately we have to give some valid value here. I can't think of a good way to not do that while still retaining all the benefits we currently have of the type-checkedSettings
and making sure every settings property that needs to be defined has a valid value.