From 1078341b220c53f13d12e8d34661999a75174806 Mon Sep 17 00:00:00 2001 From: Long Ho Date: Mon, 15 Mar 2021 13:02:53 -0400 Subject: [PATCH] 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 --- .github/workflows/e2e-test.yml | 2 +- .github/workflows/migration-cli-e2e-test.yml | 2 +- .github/workflows/nodejs-windows.yml | 2 +- .nvmrc | 2 +- jest.config.js | 2 +- jest/polyfills.js | 19 ++ package.json | 5 +- .../src/blogUtils.ts | 14 +- .../src/docs.ts | 3 +- .../src/utils/usePluralForm.ts | 3 +- packages/docusaurus-utils/package.json | 2 - packages/docusaurus-utils/src/index.ts | 8 - .../translations/translationsExtractor.ts | 173 +++++++++--------- .../i18n/i18n-introduction.md | 6 + yarn.lock | 43 +++-- 15 files changed, 162 insertions(+), 124 deletions(-) create mode 100644 jest/polyfills.js diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index 00fa9f9d54..b4076a55f7 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -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 }} diff --git a/.github/workflows/migration-cli-e2e-test.yml b/.github/workflows/migration-cli-e2e-test.yml index 80aa06852d..ecf6932c91 100644 --- a/.github/workflows/migration-cli-e2e-test.yml +++ b/.github/workflows/migration-cli-e2e-test.yml @@ -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 }} diff --git a/.github/workflows/nodejs-windows.yml b/.github/workflows/nodejs-windows.yml index 5baf5a8906..a9f5123645 100644 --- a/.github/workflows/nodejs-windows.yml +++ b/.github/workflows/nodejs-windows.yml @@ -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 diff --git a/.nvmrc b/.nvmrc index 47c0a98a11..2a0dc9a810 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -12.13.0 +14.16.0 diff --git a/jest.config.js b/jest.config.js index 810caed743..370234ab92 100644 --- a/jest.config.js +++ b/jest.config.js @@ -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', }, diff --git a/jest/polyfills.js b/jest/polyfills.js new file mode 100644 index 0000000000..951678076e --- /dev/null +++ b/jest/polyfills.js @@ -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'); diff --git a/package.json b/package.json index 874c0a76b4..e62b7f4b08 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/docusaurus-plugin-content-blog/src/blogUtils.ts b/packages/docusaurus-plugin-content-blog/src/blogUtils.ts index 439aab7b7d..c0465346eb 100644 --- a/packages/docusaurus-plugin-content-blog/src/blogUtils.ts +++ b/packages/docusaurus-plugin-content-blog/src/blogUtils.ts @@ -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); diff --git a/packages/docusaurus-plugin-content-docs/src/docs.ts b/packages/docusaurus-plugin-content-docs/src/docs.ts index 3863d8f25a..2aac56b618 100644 --- a/packages/docusaurus-plugin-content-docs/src/docs.ts +++ b/packages/docusaurus-plugin-content-docs/src/docs.ts @@ -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, diff --git a/packages/docusaurus-theme-common/src/utils/usePluralForm.ts b/packages/docusaurus-theme-common/src/utils/usePluralForm.ts index bc5b97c6db..8c908fb042 100644 --- a/packages/docusaurus-theme-common/src/utils/usePluralForm.ts +++ b/packages/docusaurus-theme-common/src/utils/usePluralForm.ts @@ -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) { diff --git a/packages/docusaurus-utils/package.json b/packages/docusaurus-utils/package.json index a5d56e24e7..aff834b9e5 100644 --- a/packages/docusaurus-utils/package.json +++ b/packages/docusaurus-utils/package.json @@ -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" }, diff --git a/packages/docusaurus-utils/src/index.ts b/packages/docusaurus-utils/src/index.ts index 2d6d5e83e1..8a78a87a5b 100644 --- a/packages/docusaurus-utils/src/index.ts +++ b/packages/docusaurus-utils/src/index.ts @@ -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( diff --git a/packages/docusaurus/src/server/translations/translationsExtractor.ts b/packages/docusaurus/src/server/translations/translationsExtractor.ts index 001cea522d..dd5bd29dda 100644 --- a/packages/docusaurus/src/server/translations/translationsExtractor.ts +++ b/packages/docusaurus/src/server/translations/translationsExtractor.ts @@ -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) + .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)}`, + ); } }, }); diff --git a/website/versioned_docs/version-2.0.0-alpha.71/i18n/i18n-introduction.md b/website/versioned_docs/version-2.0.0-alpha.71/i18n/i18n-introduction.md index 51e55991bf..f723eb4430 100644 --- a/website/versioned_docs/version-2.0.0-alpha.71/i18n/i18n-introduction.md +++ b/website/versioned_docs/version-2.0.0-alpha.71/i18n/i18n-introduction.md @@ -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. diff --git a/yarn.lock b/yarn.lock index ec11679572..eabc476007 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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==