Skip to content

Commit

Permalink
refactor(v2): i18n cleanups / refactors (#4405)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
longlho committed Mar 15, 2021
1 parent c3968e2 commit 1078341
Show file tree
Hide file tree
Showing 15 changed files with 163 additions and 125 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/e2e-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/migration-cli-e2e-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/nodejs-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
12.13.0
14.16.0
2 changes: 1 addition & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
Expand Down
19 changes: 19 additions & 0 deletions jest/polyfills.js
Original file line number Diff line number Diff line change
@@ -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');
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
14 changes: 5 additions & 9 deletions packages/docusaurus-plugin-content-blog/src/blogUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import {
getEditUrl,
getFolderContainingFile,
posixPath,
getDateTimeFormat,
} from '@docusaurus/utils';
import {LoadContext} from '@docusaurus/types';
import {replaceMarkdownLinks} from '@docusaurus/utils/lib/markdownLinks';
Expand Down Expand Up @@ -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);
Expand Down
3 changes: 1 addition & 2 deletions packages/docusaurus-plugin-content-docs/src/docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import {
normalizeUrl,
parseMarkdownString,
posixPath,
getDateTimeFormat,
} from '@docusaurus/utils';
import {LoadContext} from '@docusaurus/types';

Expand Down Expand Up @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion packages/docusaurus-theme-common/src/utils/usePluralForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
2 changes: 0 additions & 2 deletions packages/docusaurus-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
8 changes: 0 additions & 8 deletions packages/docusaurus-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down
175 changes: 89 additions & 86 deletions packages/docusaurus/src/server/translations/translationsExtractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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 &&
Expand All @@ -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();

if (singleChildren && t.isJSXText(singleChildren.node)) {
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 &&
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)}`,
);
}
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)}`,
);
}
},

CallExpression(path) {
if (
path.node.callee.type === 'Identifier' &&
path.node.callee.name === 'translate'
) {
// console.log('CallExpression', path.node);
if (
path.node.arguments.length === 1 ||
path.node.arguments.length === 2
) {
const firstArgPath = path.get('arguments.0') as NodePath;
if (!path.get('callee').isIdentifier({name: 'translate'})) {
return;
}

// evaluation allows translate("x" + "y"); to be considered as translate("xy");
const firstArgEvaluated = firstArgPath.evaluate();
// console.log('CallExpression', path.node);
const args = path.get('arguments');
if (args.length === 1 || args.length === 2) {
const firstArgPath = args[0];

// console.log('firstArgEvaluated', firstArgEvaluated);
// evaluation allows translate("x" + "y"); to be considered as translate("xy");
const firstArgEvaluated = firstArgPath.evaluate();

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)}`,
);
}
// 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() 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)}`,
);
}
},
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading

0 comments on commit 1078341

Please sign in to comment.