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 runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
node: ['12'] node: ['12', '14']
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node }} - name: Use Node.js ${{ matrix.node }}

View file

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

View file

@ -10,7 +10,7 @@ jobs:
runs-on: windows-latest runs-on: windows-latest
strategy: strategy:
matrix: matrix:
node: ['12'] node: ['12', '14']
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: dorny/paths-filter@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: { transform: {
'^.+\\.[jt]sx?$': 'babel-jest', '^.+\\.[jt]sx?$': 'babel-jest',
}, },
setupFiles: ['./jest/stylelint-rule-test.js'], setupFiles: ['./jest/stylelint-rule-test.js', './jest/polyfills.js'],
moduleNameMapper: { moduleNameMapper: {
'@docusaurus/router': 'react-router-dom', '@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:js": "eslint --cache \"**/*.{js,jsx,ts,tsx}\"",
"lint:style": "stylelint \"**/*.css\"", "lint:style": "stylelint \"**/*.css\"",
"lerna": "lerna", "lerna": "lerna",
"test": "jest", "test": "cross-env TZ=UTC jest",
"test:build:v2": "./admin/scripts/test-release.sh", "test:build:v2": "./admin/scripts/test-release.sh",
"watch": "yarn lerna run --parallel --no-private watch", "watch": "yarn lerna run --parallel --no-private watch",
"clear": "yarn workspace docusaurus-2-website clear && yarn lerna exec --ignore docusaurus yarn rimraf lib", "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/plugin-transform-modules-commonjs": "^7.12.13",
"@babel/preset-typescript": "^7.12.16", "@babel/preset-typescript": "^7.12.16",
"@crowdin/cli": "^3.5.3", "@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/express": "^4.17.2",
"@types/fs-extra": "^9.0.6", "@types/fs-extra": "^9.0.6",
"@types/jest": "^26.0.20", "@types/jest": "^26.0.20",

View file

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

View file

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

View file

@ -65,7 +65,8 @@ function useLocalePluralForms(): LocalePluralForms {
i18n: {currentLocale}, i18n: {currentLocale},
} = useDocusaurusContext(); } = useDocusaurusContext();
return useMemo(() => { 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 { try {
return createLocalePluralForms(currentLocale); return createLocalePluralForms(currentLocale);
} catch (e) { } catch (e) {

View file

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

View file

@ -21,7 +21,6 @@ import {
// @ts-expect-error: no typedefs :s // @ts-expect-error: no typedefs :s
import resolvePathnameUnsafe from 'resolve-pathname'; import resolvePathnameUnsafe from 'resolve-pathname';
import areIntlLocalesSupported from 'intl-locales-supported';
const fileHash = new Map(); const fileHash = new Map();
export async function generate( export async function generate(
@ -634,13 +633,6 @@ export async function readDefaultCodeTranslationMessages({
return {}; 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} // Input: ## Some heading {#some-heading}
// Output: {text: "## Some heading", id: "some-heading"} // Output: {text: "## Some heading", id: "some-heading"}
export function parseMarkdownHeadingId( export function parseMarkdownHeadingId(

View file

@ -166,7 +166,7 @@ Need help understanding this?
Useful resources: Useful resources:
https://github.com/jamiebuilds/babel-handbook/blob/master/translations/en/plugin-handbook.md 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 https://github.com/pugjs/babel-walk
*/ */
function extractSourceCodeAstTranslations( function extractSourceCodeAstTranslations(
@ -190,18 +190,29 @@ function extractSourceCodeAstTranslations(
traverse(ast, { traverse(ast, {
JSXElement(path) { JSXElement(path) {
if (
!path
.get('openingElement')
.get('name')
.isJSXIdentifier({name: 'Translate'})
) {
return;
}
function evaluateJSXProp(propName: string): string | undefined { function evaluateJSXProp(propName: string): string | undefined {
const attributePath = path const attributePath = path
.get('openingElement.attributes') .get('openingElement.attributes')
.find( .find(
(attr) => attr.isJSXAttribute() && attr.node.name.name === propName, (attr) =>
attr.isJSXAttribute() &&
(attr as NodePath<t.JSXAttribute>)
.get('name')
.isJSXIdentifier({name: propName}),
); );
if (attributePath) { if (attributePath) {
const attributeValue = attributePath.get('value') as NodePath; const attributeValue = attributePath.get('value') as NodePath;
const attributeValueEvaluated = const attributeValueEvaluated = attributeValue.isJSXExpressionContainer()
attributeValue.node.type === 'JSXExpressionContainer'
? (attributeValue.get('expression') as NodePath).evaluate() ? (attributeValue.get('expression') as NodePath).evaluate()
: attributeValue.evaluate(); : attributeValue.evaluate();
@ -222,10 +233,6 @@ function extractSourceCodeAstTranslations(
return undefined; 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 // We only handle the optimistic case where we have a single non-empty content
const singleChildren = path const singleChildren = path
.get('children') .get('children')
@ -234,13 +241,13 @@ function extractSourceCodeAstTranslations(
.filter( .filter(
(childrenPath) => (childrenPath) =>
!( !(
t.isJSXText(childrenPath.node) && childrenPath.isJSXText() &&
childrenPath.node.value.replace('\n', '').trim() === '' childrenPath.node.value.replace('\n', '').trim() === ''
), ),
) )
.pop(); .pop();
if (singleChildren && t.isJSXText(singleChildren.node)) { if (singleChildren && singleChildren.isJSXText()) {
const message = singleChildren.node.value.trim().replace(/\s+/g, ' '); const message = singleChildren.node.value.trim().replace(/\s+/g, ' ');
const id = evaluateJSXProp('id'); const id = evaluateJSXProp('id');
@ -252,7 +259,7 @@ function extractSourceCodeAstTranslations(
}; };
} else if ( } else if (
singleChildren && singleChildren &&
t.isJSXExpressionContainer(singleChildren) && singleChildren.isJSXExpressionContainer() &&
(singleChildren.get('expression') as NodePath).evaluate().confident (singleChildren.get('expression') as NodePath).evaluate().confident
) { ) {
const message = (singleChildren.get( const message = (singleChildren.get(
@ -273,20 +280,17 @@ function extractSourceCodeAstTranslations(
)}\n${generateCode(path.node)}`, )}\n${generateCode(path.node)}`,
); );
} }
}
}, },
CallExpression(path) { CallExpression(path) {
if ( if (!path.get('callee').isIdentifier({name: 'translate'})) {
path.node.callee.type === 'Identifier' && return;
path.node.callee.name === 'translate' }
) {
// console.log('CallExpression', path.node); // console.log('CallExpression', path.node);
if ( const args = path.get('arguments');
path.node.arguments.length === 1 || if (args.length === 1 || args.length === 2) {
path.node.arguments.length === 2 const firstArgPath = args[0];
) {
const firstArgPath = path.get('arguments.0') as NodePath;
// evaluation allows translate("x" + "y"); to be considered as translate("xy"); // evaluation allows translate("x" + "y"); to be considered as translate("xy");
const firstArgEvaluated = firstArgPath.evaluate(); const firstArgEvaluated = firstArgPath.evaluate();
@ -316,7 +320,6 @@ function extractSourceCodeAstTranslations(
)}\n${generateCode(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 ## Goals
It is important to understand the **design decisions** behind the Docusaurus i18n support. It is important to understand the **design decisions** behind the Docusaurus i18n support.

View file

@ -1393,6 +1393,37 @@
unique-filename "^1.1.1" unique-filename "^1.1.1"
which "^1.3.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": "@hapi/address@^2.1.2":
version "2.1.4" version "2.1.4"
resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.1.4.tgz#5d67ed43f3fd41a69d4b9ff7b56e7c0d1d0a81e5" 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" resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.2.0.tgz#d5061a6224be58e8083985f5014d844359576296"
integrity sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw== 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: into-stream@^3.1.0:
version "3.1.0" version "3.1.0"
resolved "https://registry.yarnpkg.com/into-stream/-/into-stream-3.1.0.tgz#96fb0a936c12babd6ff1752a17d05616abd094c6" 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" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043"
integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q== 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" version "2.1.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a"
integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A== integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==