refactor(v2): i18n cleanups / refactors (#4405)

* chore: fix various intl stuff
- remove intl-locales-supported & intl since they're deprecated and only
needed for IE11
- add new polyfills for node 12
- clean up babel intl extractor
- reset jest test timezone to UTC so it passes even for East Coast
contributor

* chore: change build to include Node 14

* docs: update i18n reqs
This commit is contained in:
Long Ho 2021-03-15 13:02:53 -04:00 committed by GitHub
parent c3968e2d8f
commit 1078341b22
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 162 additions and 124 deletions

View file

@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node: ['12']
node: ['12', '14']
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node }}

View file

@ -23,7 +23,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node: ['12']
node: ['12', '14']
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node }}

View file

@ -10,7 +10,7 @@ jobs:
runs-on: windows-latest
strategy:
matrix:
node: ['12']
node: ['12', '14']
steps:
- uses: actions/checkout@v2
- uses: dorny/paths-filter@v2

2
.nvmrc
View file

@ -1 +1 @@
12.13.0
14.16.0

View file

@ -39,7 +39,7 @@ module.exports = {
transform: {
'^.+\\.[jt]sx?$': 'babel-jest',
},
setupFiles: ['./jest/stylelint-rule-test.js'],
setupFiles: ['./jest/stylelint-rule-test.js', './jest/polyfills.js'],
moduleNameMapper: {
'@docusaurus/router': 'react-router-dom',
},

19
jest/polyfills.js Normal file
View file

@ -0,0 +1,19 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/* eslint-disable import/no-extraneous-dependencies */
require('@formatjs/intl-pluralrules/polyfill');
require('@formatjs/intl-pluralrules/locale-data/en');
require('@formatjs/intl-pluralrules/locale-data/fr');
require('@formatjs/intl-numberformat/polyfill');
require('@formatjs/intl-numberformat/locale-data/en');
require('@formatjs/intl-numberformat/locale-data/fr');
require('@formatjs/intl-datetimeformat/polyfill');
require('@formatjs/intl-datetimeformat/add-all-tz');
require('@formatjs/intl-datetimeformat/locale-data/en');
require('@formatjs/intl-datetimeformat/locale-data/fr');

View file

@ -46,7 +46,7 @@
"lint:js": "eslint --cache \"**/*.{js,jsx,ts,tsx}\"",
"lint:style": "stylelint \"**/*.css\"",
"lerna": "lerna",
"test": "jest",
"test": "cross-env TZ=UTC jest",
"test:build:v2": "./admin/scripts/test-release.sh",
"watch": "yarn lerna run --parallel --no-private watch",
"clear": "yarn workspace docusaurus-2-website clear && yarn lerna exec --ignore docusaurus yarn rimraf lib",
@ -71,6 +71,9 @@
"@babel/plugin-transform-modules-commonjs": "^7.12.13",
"@babel/preset-typescript": "^7.12.16",
"@crowdin/cli": "^3.5.3",
"@formatjs/intl-datetimeformat": "^3.2.12",
"@formatjs/intl-numberformat": "^6.2.2",
"@formatjs/intl-pluralrules": "^4.0.11",
"@types/express": "^4.17.2",
"@types/fs-extra": "^9.0.6",
"@types/jest": "^26.0.20",

View file

@ -26,7 +26,6 @@ import {
getEditUrl,
getFolderContainingFile,
posixPath,
getDateTimeFormat,
} from '@docusaurus/utils';
import {LoadContext} from '@docusaurus/types';
import {replaceMarkdownLinks} from '@docusaurus/utils/lib/markdownLinks';
@ -177,14 +176,11 @@ export async function generateBlogPosts(
// Use file create time for blog.
date = date || (await fs.stat(source)).birthtime;
const formattedDate = getDateTimeFormat(i18n.currentLocale)(
i18n.currentLocale,
{
day: 'numeric',
month: 'long',
year: 'numeric',
},
).format(date);
const formattedDate = new Intl.DateTimeFormat(i18n.currentLocale, {
day: 'numeric',
month: 'long',
year: 'numeric',
}).format(date);
const slug =
frontMatter.slug || (match ? toUrl({date, link: linkName}) : linkName);

View file

@ -14,7 +14,6 @@ import {
normalizeUrl,
parseMarkdownString,
posixPath,
getDateTimeFormat,
} from '@docusaurus/utils';
import {LoadContext} from '@docusaurus/types';
@ -212,7 +211,7 @@ export function processDocMetadata({
lastUpdatedBy: lastUpdate.lastUpdatedBy,
lastUpdatedAt: lastUpdate.lastUpdatedAt,
formattedLastUpdatedAt: lastUpdate.lastUpdatedAt
? getDateTimeFormat(i18n.currentLocale)(i18n.currentLocale).format(
? new Intl.DateTimeFormat(i18n.currentLocale).format(
lastUpdate.lastUpdatedAt * 1000,
)
: undefined,

View file

@ -65,7 +65,8 @@ function useLocalePluralForms(): LocalePluralForms {
i18n: {currentLocale},
} = useDocusaurusContext();
return useMemo(() => {
if (Intl && Intl.PluralRules) {
// @ts-expect-error checking Intl.PluralRules in case browser doesn't have it (e.g Safari 12-)
if (Intl.PluralRules) {
try {
return createLocalePluralForms(currentLocale);
} catch (e) {

View file

@ -24,8 +24,6 @@
"escape-string-regexp": "^4.0.0",
"fs-extra": "^9.1.0",
"gray-matter": "^4.0.2",
"intl": "^1.2.5",
"intl-locales-supported": "1.8.11",
"lodash": "^4.17.20",
"resolve-pathname": "^3.0.0"
},

View file

@ -21,7 +21,6 @@ import {
// @ts-expect-error: no typedefs :s
import resolvePathnameUnsafe from 'resolve-pathname';
import areIntlLocalesSupported from 'intl-locales-supported';
const fileHash = new Map();
export async function generate(
@ -634,13 +633,6 @@ export async function readDefaultCodeTranslationMessages({
return {};
}
export function getDateTimeFormat(locale: string) {
return areIntlLocalesSupported([locale])
? global.Intl.DateTimeFormat
: // eslint-disable-next-line @typescript-eslint/no-var-requires
require('intl').DateTimeFormat;
}
// Input: ## Some heading {#some-heading}
// Output: {text: "## Some heading", id: "some-heading"}
export function parseMarkdownHeadingId(

View file

@ -166,7 +166,7 @@ Need help understanding this?
Useful resources:
https://github.com/jamiebuilds/babel-handbook/blob/master/translations/en/plugin-handbook.md
https://github.com/formatjs/formatjs/blob/main/packages/babel-plugin-react-intl/index.ts
https://github.com/formatjs/formatjs/blob/main/packages/babel-plugin-formatjs/index.ts
https://github.com/pugjs/babel-walk
*/
function extractSourceCodeAstTranslations(
@ -190,20 +190,31 @@ function extractSourceCodeAstTranslations(
traverse(ast, {
JSXElement(path) {
if (
!path
.get('openingElement')
.get('name')
.isJSXIdentifier({name: 'Translate'})
) {
return;
}
function evaluateJSXProp(propName: string): string | undefined {
const attributePath = path
.get('openingElement.attributes')
.find(
(attr) => attr.isJSXAttribute() && attr.node.name.name === propName,
(attr) =>
attr.isJSXAttribute() &&
(attr as NodePath<t.JSXAttribute>)
.get('name')
.isJSXIdentifier({name: propName}),
);
if (attributePath) {
const attributeValue = attributePath.get('value') as NodePath;
const attributeValueEvaluated =
attributeValue.node.type === 'JSXExpressionContainer'
? (attributeValue.get('expression') as NodePath).evaluate()
: attributeValue.evaluate();
const attributeValueEvaluated = attributeValue.isJSXExpressionContainer()
? (attributeValue.get('expression') as NodePath).evaluate()
: attributeValue.evaluate();
if (
attributeValueEvaluated.confident &&
@ -222,100 +233,92 @@ function extractSourceCodeAstTranslations(
return undefined;
}
if (
path.node.openingElement.name.type === 'JSXIdentifier' &&
path.node.openingElement.name.name === 'Translate'
// We only handle the optimistic case where we have a single non-empty content
const singleChildren = path
.get('children')
// Remove empty/useless text nodes that might be around our translation!
// Makes the translation system more reliable to JSX formatting issues
.filter(
(childrenPath) =>
!(
childrenPath.isJSXText() &&
childrenPath.node.value.replace('\n', '').trim() === ''
),
)
.pop();
if (singleChildren && singleChildren.isJSXText()) {
const message = singleChildren.node.value.trim().replace(/\s+/g, ' ');
const id = evaluateJSXProp('id');
const description = evaluateJSXProp('description');
translations[id ?? message] = {
message,
...(description && {description}),
};
} else if (
singleChildren &&
singleChildren.isJSXExpressionContainer() &&
(singleChildren.get('expression') as NodePath).evaluate().confident
) {
// We only handle the optimistic case where we have a single non-empty content
const singleChildren = path
.get('children')
// Remove empty/useless text nodes that might be around our translation!
// Makes the translation system more reliable to JSX formatting issues
.filter(
(childrenPath) =>
!(
t.isJSXText(childrenPath.node) &&
childrenPath.node.value.replace('\n', '').trim() === ''
),
)
.pop();
const message = (singleChildren.get(
'expression',
) as NodePath).evaluate().value;
if (singleChildren && t.isJSXText(singleChildren.node)) {
const message = singleChildren.node.value.trim().replace(/\s+/g, ' ');
const id = evaluateJSXProp('id');
const description = evaluateJSXProp('description');
const id = evaluateJSXProp('id');
const description = evaluateJSXProp('description');
translations[id ?? message] = {
message,
...(description && {description}),
};
} else if (
singleChildren &&
t.isJSXExpressionContainer(singleChildren) &&
(singleChildren.get('expression') as NodePath).evaluate().confident
) {
const message = (singleChildren.get(
'expression',
) as NodePath).evaluate().value;
const id = evaluateJSXProp('id');
const description = evaluateJSXProp('description');
translations[id ?? message] = {
message,
...(description && {description}),
};
} else {
warnings.push(
`${staticTranslateJSXWarningPart}\n${sourceFileWarningPart(
path.node,
)}\n${generateCode(path.node)}`,
);
}
translations[id ?? message] = {
message,
...(description && {description}),
};
} else {
warnings.push(
`${staticTranslateJSXWarningPart}\n${sourceFileWarningPart(
path.node,
)}\n${generateCode(path.node)}`,
);
}
},
CallExpression(path) {
if (
path.node.callee.type === 'Identifier' &&
path.node.callee.name === 'translate'
) {
// console.log('CallExpression', path.node);
if (!path.get('callee').isIdentifier({name: 'translate'})) {
return;
}
// console.log('CallExpression', path.node);
const args = path.get('arguments');
if (args.length === 1 || args.length === 2) {
const firstArgPath = args[0];
// evaluation allows translate("x" + "y"); to be considered as translate("xy");
const firstArgEvaluated = firstArgPath.evaluate();
// console.log('firstArgEvaluated', firstArgEvaluated);
if (
path.node.arguments.length === 1 ||
path.node.arguments.length === 2
firstArgEvaluated.confident &&
typeof firstArgEvaluated.value === 'object'
) {
const firstArgPath = path.get('arguments.0') as NodePath;
// evaluation allows translate("x" + "y"); to be considered as translate("xy");
const firstArgEvaluated = firstArgPath.evaluate();
// console.log('firstArgEvaluated', firstArgEvaluated);
if (
firstArgEvaluated.confident &&
typeof firstArgEvaluated.value === 'object'
) {
const {message, id, description} = firstArgEvaluated.value;
translations[id ?? message] = {
message,
...(description && {description}),
};
} else {
warnings.push(
`translate() first arg should be a statically evaluable object.\nExample: translate({message: "text",id: "optional.id",description: "optional description"}\nDynamically constructed values are not allowed, because they prevent translations to be extracted.\n${sourceFileWarningPart(
path.node,
)}\n${generateCode(path.node)}`,
);
}
const {message, id, description} = firstArgEvaluated.value;
translations[id ?? message] = {
message,
...(description && {description}),
};
} else {
warnings.push(
`translate() function only takes 1 or 2 args\n${sourceFileWarningPart(
`translate() first arg should be a statically evaluable object.\nExample: translate({message: "text",id: "optional.id",description: "optional description"}\nDynamically constructed values are not allowed, because they prevent translations to be extracted.\n${sourceFileWarningPart(
path.node,
)}\n${generateCode(path.node)}`,
);
}
} else {
warnings.push(
`translate() function only takes 1 or 2 args\n${sourceFileWarningPart(
path.node,
)}\n${generateCode(path.node)}`,
);
}
},
});

View file

@ -13,6 +13,12 @@ i18n is a new feature (released early 2021), please report any bug you find.
:::
:::caution
i18n requires Node 13+ to build or Node 12 with [full-icu](https://www.npmjs.com/package/full-icu).
:::
## Goals
It is important to understand the **design decisions** behind the Docusaurus i18n support.

View file

@ -1393,6 +1393,37 @@
unique-filename "^1.1.1"
which "^1.3.1"
"@formatjs/ecma402-abstract@1.6.2":
version "1.6.2"
resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-1.6.2.tgz#9d064a2cf790769aa6721e074fb5d5c357084bb9"
integrity sha512-aLBODrSRhHaL/0WdQ0T2UsGqRbdtRRHqqrs4zwNQoRsGBEtEAvlj/rgr6Uea4PSymVJrbZBoAyECM2Z3Pq4i0g==
dependencies:
tslib "^2.1.0"
"@formatjs/intl-datetimeformat@^3.2.12":
version "3.2.12"
resolved "https://registry.yarnpkg.com/@formatjs/intl-datetimeformat/-/intl-datetimeformat-3.2.12.tgz#c9b2e85f0267ee13ea615a8991995da3075e3b13"
integrity sha512-qvY5+dl3vlgH0iWRXwl8CG9UkSVB5uP2+HH//fyZZ01G4Ww5rxMJmia1SbUqatpoe/dX+Z+aLejCqUUyugyL2g==
dependencies:
"@formatjs/ecma402-abstract" "1.6.2"
tslib "^2.1.0"
"@formatjs/intl-numberformat@^6.2.2":
version "6.2.2"
resolved "https://registry.yarnpkg.com/@formatjs/intl-numberformat/-/intl-numberformat-6.2.2.tgz#a75ea1d18ac3c89c2a28d9d9ac35f612699963e8"
integrity sha512-mM6dwNizz8GS62kVS7yj0avz+6HDvIJldsAv495XCTFxxcSmP4iTREG+0J+ex8puBTJRESgJx4MBcXJPj0K76Q==
dependencies:
"@formatjs/ecma402-abstract" "1.6.2"
tslib "^2.1.0"
"@formatjs/intl-pluralrules@^4.0.11":
version "4.0.11"
resolved "https://registry.yarnpkg.com/@formatjs/intl-pluralrules/-/intl-pluralrules-4.0.11.tgz#c7f09879a30550a6599f1b128021fcb7115a7c87"
integrity sha512-NkQl6eBJKMSMmNCan2gzpErqbw7j1SiHzqAQF3+coAKwB74E89hAGTigvnCKEGpMf+hHysL4+wThx5IJutoT5Q==
dependencies:
"@formatjs/ecma402-abstract" "1.6.2"
tslib "^2.1.0"
"@hapi/address@^2.1.2":
version "2.1.4"
resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.1.4.tgz#5d67ed43f3fd41a69d4b9ff7b56e7c0d1d0a81e5"
@ -10984,16 +11015,6 @@ interpret@^1.0.0:
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.2.0.tgz#d5061a6224be58e8083985f5014d844359576296"
integrity sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==
intl-locales-supported@1.8.11:
version "1.8.11"
resolved "https://registry.yarnpkg.com/intl-locales-supported/-/intl-locales-supported-1.8.11.tgz#a8488b2998b524754e020fef0c0a67fa7b87bd5b"
integrity sha512-J+RhLNDxEvaNPdsoWz2KKlLSqU0MfV4Pd6zS1Yx/tN/KGN5QYe4Z2+ifJod95LlaA4K6qogwrSzm/WyTNOV6RA==
intl@^1.2.5:
version "1.2.5"
resolved "https://registry.yarnpkg.com/intl/-/intl-1.2.5.tgz#82244a2190c4e419f8371f5aa34daa3420e2abde"
integrity sha1-giRKIZDE5Bn4Nx9ao02qNCDiq94=
into-stream@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/into-stream/-/into-stream-3.1.0.tgz#96fb0a936c12babd6ff1752a17d05616abd094c6"
@ -19700,7 +19721,7 @@ tslib@^1, tslib@^1.10.0, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043"
integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==
tslib@^2.0.0, tslib@^2.0.1:
tslib@^2.0.0, tslib@^2.0.1, tslib@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a"
integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==