diff --git a/packages/react-i18n/package.json b/packages/react-i18n/package.json index 9ce186455c..5e151eee44 100644 --- a/packages/react-i18n/package.json +++ b/packages/react-i18n/package.json @@ -29,6 +29,7 @@ "@types/babel__core": "^7.0.4", "@types/babel__template": "^7.0.1", "@types/babel__traverse": "^7.0.5", + "@types/glob": "^7.1.1", "intl-pluralrules": "^0.2.1", "typescript": "~3.2.1" }, @@ -38,6 +39,7 @@ "@shopify/react-hooks": "^1.2.1", "@shopify/useful-types": "^1.3.0", "@types/hoist-non-react-statics": "^3.0.1", + "glob": "^7.1.4", "hoist-non-react-statics": "^3.0.1", "prop-types": "^15.6.2", "string-hash": "^1.1.3", diff --git a/packages/react-i18n/src/babel-plugin.ts b/packages/react-i18n/src/babel-plugin.ts index aabda65fcf..79c65a27b6 100644 --- a/packages/react-i18n/src/babel-plugin.ts +++ b/packages/react-i18n/src/babel-plugin.ts @@ -1,5 +1,5 @@ import path from 'path'; -import fs from 'fs'; +import glob from 'glob'; import {TemplateBuilder} from '@babel/template'; import * as Types from '@babel/types'; import {NodePath} from '@babel/traverse'; @@ -22,14 +22,28 @@ export default function injectWithI18nArguments({ })() as Types.ImportDeclaration; } - function i18nCallExpression({id, fallbackID, bindingName}) { + function i18nCallExpression({id, fallbackID, bindingName, translations}) { return template( `${bindingName}({ id: '${id}', fallback: ${fallbackID}, - async translations(locale) { - const dictionary = await import(/* webpackChunkName: "${id}-i18n", webpackMode: "lazy-once" */ \`./translations/$\{locale}.json\`); - return dictionary && dictionary.default; + translations(locale) { + const translations = [${translations + .filter(locale => !locale.endsWith('en.json')) + .map(locale => + JSON.stringify(path.basename(locale, path.extname(locale))), + ) + .sort() + .join(', ')}]; + + if (translations.indexOf(locale) < 0) { + return; + } + + return (async () => { + const dictionary = await import(/* webpackChunkName: "${id}-i18n", webpackMode: "lazy-once" */ \`./translations/$\{locale}.json\`); + return dictionary && dictionary.default; + })(); }, })`, { @@ -66,7 +80,9 @@ export default function injectWithI18nArguments({ ); } - if (!hasTranslations(filename)) { + const translations = getTranslations(filename); + + if (translations.length === 0) { return; } @@ -77,6 +93,7 @@ export default function injectWithI18nArguments({ id: generateID(filename), fallbackID, bindingName, + translations, }), ); } @@ -129,13 +146,13 @@ export default function injectWithI18nArguments({ }; } -function hasTranslations(filename) { - const enJSONPath = path.resolve( - path.dirname(filename), - 'translations', - 'en.json', +function getTranslations(filename: string): string[] { + return glob.sync( + path.resolve(path.dirname(filename), 'translations', '*.json'), + { + nodir: true, + }, ); - return fs.existsSync(enJSONPath); } // based on postcss-modules implementation diff --git a/packages/react-i18n/src/test/babel-plugin.test.ts b/packages/react-i18n/src/test/babel-plugin.test.ts index 0e12f4b90b..6bad9be059 100644 --- a/packages/react-i18n/src/test/babel-plugin.test.ts +++ b/packages/react-i18n/src/test/babel-plugin.test.ts @@ -6,9 +6,23 @@ jest.mock('string-hash', () => () => { return Number.MAX_SAFE_INTEGER; }); -describe('babel-pluin-react-i18n', () => { - const defaultHash = Number.MAX_SAFE_INTEGER.toString(36).substr(0, 5); +const defaultHash = Number.MAX_SAFE_INTEGER.toString(36).substr(0, 5); +const expectedTranslationsOption = ` + translations(locale) { + const translations = ["de", "fr"]; + + if (translations.indexOf(locale) < 0) { + return; + } + return (async () => { + const dictionary = await import(/* webpackChunkName: "MyComponent_${defaultHash}-i18n", webpackMode: "lazy-once" */ \`./translations/$\{locale}.json\`); + return dictionary && dictionary.default; + })(); + } +`; + +describe('babel-pluin-react-i18n', () => { it('injects arguments into withI18n when adjacent translations exist', async () => { expect( await transform( @@ -36,10 +50,7 @@ describe('babel-pluin-react-i18n', () => { export default withI18n({ id: 'MyComponent_${defaultHash}', fallback: _en, - async translations(locale) { - const dictionary = await import(/* webpackChunkName: "MyComponent_${defaultHash}-i18n", webpackMode: "lazy-once" */ \`./translations/$\{locale}.json\`); - return dictionary && dictionary.default; - }, + ${expectedTranslationsOption} })(MyComponent); `, ), @@ -69,10 +80,7 @@ describe('babel-pluin-react-i18n', () => { const [i18n] = useI18n({ id: 'MyComponent_${defaultHash}', fallback: _en, - async translations(locale) { - const dictionary = await import(/* webpackChunkName: "MyComponent_${defaultHash}-i18n", webpackMode: "lazy-once" */ \`./translations/$\{locale}.json\`); - return dictionary && dictionary.default; - }, + ${expectedTranslationsOption} }); return i18n.translate('key'); } @@ -98,43 +106,21 @@ describe('babel-pluin-react-i18n', () => { }); it('does not transform other react-i18n imports', async () => { - expect( - await transform( - `import React from 'react'; - import {withI18n, translate} from '@shopify/react-i18n'; - - function MyComponent({i18n}) { - return i18n.translate('key'); - } - export const key = translate('key'); - - export default withI18n()(MyComponent); - `, - optionsForFile('MyComponent.tsx', true), - ), - ).toBe( - await normalize( - `import _en from './translations/en.json'; - import React from 'react'; - import {withI18n, translate} from '@shopify/react-i18n'; + const content = ` + import React from 'react'; + import {withFoo} from '@shopify/react-i18n'; - function MyComponent({i18n}) { - return i18n.translate('key'); - } + function MyComponent({i18n}) { + return i18n.translate('key'); + } + export const key = translate('key'); - export const key = translate('key'); + export default withFoo()(MyComponent); + `; - export default withI18n({ - id: 'MyComponent_${defaultHash}', - fallback: _en, - async translations(locale) { - const dictionary = await import(/* webpackChunkName: "MyComponent_${defaultHash}-i18n", webpackMode: "lazy-once" */ \`./translations/$\{locale}.json\`); - return dictionary && dictionary.default; - }, - })(MyComponent); - `, - ), - ); + expect( + await transform(content, optionsForFile('MyComponent.tsx', true)), + ).toBe(await normalize(content)); }); it('does not transform withI18n imports from other libraries', async () => { @@ -195,10 +181,7 @@ describe('babel-pluin-react-i18n', () => { export default foo({ id: 'MyComponent_${defaultHash}', fallback: _en, - async translations(locale) { - const dictionary = await import(/* webpackChunkName: "MyComponent_${defaultHash}-i18n", webpackMode: "lazy-once" */ \`./translations/$\{locale}.json\`); - return dictionary && dictionary.default; - }, + ${expectedTranslationsOption} })(MyComponent); `, ), @@ -228,10 +211,7 @@ describe('babel-pluin-react-i18n', () => { const [i18n] = useFunI18n({ id: 'MyComponent_${defaultHash}', fallback: _en, - async translations(locale) { - const dictionary = await import(/* webpackChunkName: "MyComponent_${defaultHash}-i18n", webpackMode: "lazy-once" */ \`./translations/$\{locale}.json\`); - return dictionary && dictionary.default; - }, + ${expectedTranslationsOption} }); return i18n.translate('key'); } diff --git a/packages/react-i18n/src/test/fixtures/adjacentTranslations/translations/de.json b/packages/react-i18n/src/test/fixtures/adjacentTranslations/translations/de.json new file mode 100644 index 0000000000..25e4f1ace9 --- /dev/null +++ b/packages/react-i18n/src/test/fixtures/adjacentTranslations/translations/de.json @@ -0,0 +1,3 @@ +{ + "key": "etwas Text" +} diff --git a/packages/react-i18n/src/test/fixtures/adjacentTranslations/translations/fr.json b/packages/react-i18n/src/test/fixtures/adjacentTranslations/translations/fr.json new file mode 100644 index 0000000000..ace128d3c8 --- /dev/null +++ b/packages/react-i18n/src/test/fixtures/adjacentTranslations/translations/fr.json @@ -0,0 +1,3 @@ +{ + "key": "du texte" +} diff --git a/yarn.lock b/yarn.lock index edd05e1796..b67b43fdae 100644 --- a/yarn.lock +++ b/yarn.lock @@ -429,6 +429,15 @@ dependencies: "@types/node" "*" +"@types/glob@^7.1.1": + version "7.1.1" + resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.1.tgz#aa59a1c6e3fbc421e07ccd31a944c30eba521575" + integrity sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w== + dependencies: + "@types/events" "*" + "@types/minimatch" "*" + "@types/node" "*" + "@types/graphql@^14.0.0": version "14.0.7" resolved "https://registry.yarnpkg.com/@types/graphql/-/graphql-14.0.7.tgz#daa09397220a68ce1cbb3f76a315ff3cd92312f6" @@ -566,6 +575,11 @@ resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.0.tgz#5a7306e367c539b9f6543499de8dd519fac37a8b" integrity sha512-A2TAGbTFdBw9azHbpVd+/FkdW2T6msN1uct1O9bH3vTerEHKZhTXJUQXy+hNq1B0RagfU8U+KBdqiZpxjhOUQA== +"@types/minimatch@*": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" + integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== + "@types/node@*", "@types/node@^12.0.2": version "12.0.4" resolved "https://registry.yarnpkg.com/@types/node/-/node-12.0.4.tgz#46832183115c904410c275e34cf9403992999c32" @@ -3834,6 +3848,18 @@ glob@^7.1.3: once "^1.3.0" path-is-absolute "^1.0.0" +glob@^7.1.4: + version "7.1.4" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255" + integrity sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + global-modules@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-1.0.0.tgz#6d770f0eb523ac78164d72b5e71a8877265cc3ea"