├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── babel.config.js ├── bin └── cli ├── fixtures └── Simple.tsx ├── jest.config.js ├── package.json ├── src ├── BabelPluginI18n.ts ├── __test__ │ ├── filterFiles.test.ts │ ├── generateResources.spec.ts │ ├── i18nTransfomerCodemod.test.ts │ └── stableString.test.ts ├── __testfixtures__ │ ├── CallExpression.input.tsx │ ├── CallExpression.output.tsx │ ├── CallExpression.resource.tsx │ ├── Classes.input.tsx │ ├── Classes.output.tsx │ ├── Classes.resource.tsx │ ├── Classnames.input.tsx │ ├── Classnames.output.tsx │ ├── Diacritics.input.tsx │ ├── Diacritics.output.tsx │ ├── Diacritics.resource.tsx │ ├── ExpressionContainer.input.tsx │ ├── ExpressionContainer.output.tsx │ ├── ExpressionContainer.resource.tsx │ ├── Functional.input.tsx │ ├── Functional.output.tsx │ ├── Functional.resource.tsx │ ├── Hooks.input.tsx │ ├── Hooks.output.tsx │ ├── HooksInlineExport.input.tsx │ ├── HooksInlineExport.output.tsx │ ├── Imported.input.tsx │ ├── Imported.output.tsx │ ├── NoChange.input.tsx │ ├── NoChange.output.tsx │ ├── Parameters.input.tsx │ ├── Parameters.output.tsx │ ├── Parameters.resource.tsx │ ├── Props.input.tsx │ ├── Props.output.tsx │ ├── Props.resource.tsx │ ├── Svg.input.tsx │ ├── Svg.output.tsx │ ├── Svg.resource.tsx │ ├── Tsx.input.tsx │ ├── Tsx.output.tsx │ ├── Tsx.resource.tsx │ ├── WithHoc.input.tsx │ ├── WithHoc.output.tsx │ ├── Yup.input.tsx │ ├── Yup.output.tsx │ └── Yup.resource.tsx ├── cli.ts ├── config.ts ├── filterFiles.ts ├── generateResources.ts ├── i18nTransformerCodemod.ts ├── index.ts ├── stableString.ts └── visitorChecks.ts ├── test └── testUtils.ts ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.sublime-project 3 | *.sublime-workspace 4 | .idea/ 5 | 6 | lib-cov 7 | *.seed 8 | *.log 9 | *.csv 10 | *.dat 11 | *.out 12 | *.pid 13 | *.gz 14 | *.map 15 | 16 | pids 17 | logs 18 | results 19 | test-results 20 | 21 | node_modules 22 | npm-debug.log 23 | 24 | dump.rdb 25 | bundle.js 26 | 27 | lib 28 | build 29 | dist 30 | coverage 31 | .nyc_output 32 | .env 33 | 34 | graphql.*.json 35 | junit.xml 36 | 37 | .vs 38 | 39 | test/globalConfig.json 40 | distTs 41 | 42 | # Random things to ignore 43 | ignore/ 44 | package-lock.json 45 | /yarn-offline-cache 46 | .cache 47 | resource.tsx 48 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 8 4 | script: 5 | - npm test 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Sibelius Seraphini 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AST i18n [![Build Status](https://travis-ci.org/sibelius/ast-i18n.svg?branch=master)](https://travis-ci.org/sibelius/ast-i18n) 2 | 3 | The objective of this tool is to make easy to migrate an existing codebase to use i18n 4 | 5 | ## How it works 6 | - it gets a list of files from the command line 7 | - it runs a babel plugin transform to find all string inside JSXText 8 | - it generates a stable key for the extracted strings 9 | - it generates i18n files format based on this map 10 | - it modify your existing code to use i18n library of your preference 11 | 12 | ## Example 13 | 14 | Before this transform 15 | ```jsx 16 | import React from 'react'; 17 | 18 | const Simple = () => ( 19 | My simple text 20 | ); 21 | ``` 22 | 23 | After this transform 24 | ```jsx 25 | import React from 'react'; 26 | import { withTranslation } from 'react-i18next' 27 | 28 | const Simple = ({ t }) => ( 29 | {t('my_simple_text')} 30 | ); 31 | ``` 32 | 33 | ## Usage of string extractor 34 | ```bash 35 | yarn start --src=myapp/src 36 | ``` 37 | 38 | - It will generate a resource.jsx file, like the below: 39 | ```jsx 40 | export default { 41 | translation: { 42 | 'ok': `ok`, 43 | 'cancelar': `cancelar`, 44 | 'continuar': `continuar`, 45 | 'salvar': `salvar`, 46 | 'endereco': `endereço:`, 47 | 'troca_de_senha': `troca de senha`, 48 | 'dados_pessoais': `dados pessoais`, 49 | [key]: 'value', 50 | } 51 | } 52 | ``` 53 | 54 | ### How to use resource with react-i18next? 55 | - rename resource.tsx to your main language, like en.ts 56 | - create other resource languages based on the generated one 57 | 58 | ```jsx 59 | import en from './en'; 60 | 61 | i18n.use(LanguageDetector).init({ 62 | resources: { 63 | en, 64 | }, 65 | fallbackLng: 'ptBR', 66 | debug: false, 67 | 68 | interpolation: { 69 | escapeValue: false, // not needed for react!! 70 | formatSeparator: ',', 71 | }, 72 | 73 | react: { 74 | wait: true, 75 | }, 76 | }); 77 | ``` 78 | 79 | ## Usage of i18n codemod 80 | ```bash 81 | npm i -g jscodeshift 82 | 83 | jscodeshift -t src/i18nTransformerCodemod.ts PATH_TO_FILES 84 | ``` 85 | 86 | ## How to customize blacklist 87 | Use ast.config.js to customize blacklist for jsx attribute name and call expression calle 88 | 89 | ```jsx 90 | module.exports = { 91 | blackListJsxAttributeName: [ 92 | 'type', 93 | 'id', 94 | 'name', 95 | 'children', 96 | 'labelKey', 97 | 'valueKey', 98 | 'labelValue', 99 | 'className', 100 | 'color', 101 | 'key', 102 | 'size', 103 | 'charSet', 104 | 'content', 105 | ], 106 | blackListCallExpressionCalle: [ 107 | 't', 108 | '_interopRequireDefault', 109 | 'require', 110 | 'routeTo', 111 | 'format', 112 | 'importScripts', 113 | 'buildPath', 114 | 'createLoadable', 115 | 'import', 116 | 'setFieldValue', 117 | ], 118 | }; 119 | ``` 120 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | const { workspaces = [] } = require('./package.json'); 2 | 3 | module.exports = { 4 | babelrcRoots: ['.', ...(workspaces.packages || workspaces)], 5 | presets: [ 6 | '@babel/preset-flow', 7 | [ 8 | '@babel/preset-env', 9 | { 10 | targets: { 11 | node: 'current', 12 | }, 13 | }, 14 | ], 15 | '@babel/preset-react', 16 | '@babel/preset-typescript', 17 | ], 18 | plugins: [ 19 | '@babel/plugin-proposal-object-rest-spread', 20 | '@babel/plugin-proposal-class-properties', 21 | '@babel/plugin-proposal-export-default-from', 22 | '@babel/plugin-proposal-export-namespace-from', 23 | '@babel/plugin-transform-async-to-generator', 24 | '@babel/plugin-proposal-async-generator-functions', 25 | '@babel/plugin-syntax-dynamic-import', 26 | '@babel/plugin-proposal-optional-chaining', 27 | '@babel/plugin-proposal-nullish-coalescing-operator' 28 | ], 29 | }; 30 | -------------------------------------------------------------------------------- /bin/cli: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('../lib/cli.js').run(); 4 | -------------------------------------------------------------------------------- /fixtures/Simple.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Simple = () => ( 4 | My simple text 5 | ); 6 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: ["src"], 3 | testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(js|ts|tsx)?$', 4 | }; 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ast-i18n", 3 | "version": "1.0.2", 4 | "author": "Sibelius Seraphini (https://github.com/sibelius)", 5 | "bin": { 6 | "ast-i18n": "./bin/cli" 7 | }, 8 | "dependencies": { 9 | "ast-types": "^0.12.2", 10 | "cosmiconfig": "^5.1.0", 11 | "diacritics": "^1.3.0", 12 | "find": "^0.2.9", 13 | "graceful-fs": "^4.1.15", 14 | "jscodeshift-imports": "^1.1.0", 15 | "prettier": "^1.16.4", 16 | "shelljs": "^0.8.3", 17 | "slugify": "^1.3.4", 18 | "yargs": "^12.0.5" 19 | }, 20 | "devDependencies": { 21 | "@babel/cli": "^7.0.0", 22 | "@babel/core": "^7.0.0", 23 | "@babel/node": "^7.0.0", 24 | "@babel/plugin-proposal-async-generator-functions": "^7.0.0", 25 | "@babel/plugin-proposal-class-properties": "^7.0.0", 26 | "@babel/plugin-proposal-export-default-from": "^7.0.0", 27 | "@babel/plugin-proposal-export-namespace-from": "^7.0.0", 28 | "@babel/plugin-proposal-nullish-coalescing-operator": "^7.10.4", 29 | "@babel/plugin-proposal-object-rest-spread": "^7.0.0", 30 | "@babel/plugin-proposal-optional-chaining": "^7.11.0", 31 | "@babel/plugin-syntax-dynamic-import": "^7.2.0", 32 | "@babel/plugin-transform-async-to-generator": "^7.0.0", 33 | "@babel/plugin-transform-flow-strip-types": "^7.0.0", 34 | "@babel/preset-env": "^7.0.0", 35 | "@babel/preset-flow": "^7.0.0", 36 | "@babel/preset-react": "^7.0.0", 37 | "@babel/preset-typescript": "^7.0.0", 38 | "@types/babel__core": "^7.0.4", 39 | "@types/babel__generator": "^7.0.1", 40 | "@types/babel__template": "^7.0.1", 41 | "@types/babel__traverse": "^7.0.5", 42 | "@types/cosmiconfig": "^5.0.3", 43 | "@types/diacritics": "^1.3.1", 44 | "@types/find": "^0.2.1", 45 | "@types/graceful-fs": "^4.1.2", 46 | "@types/jest": "^24.0.0", 47 | "@types/jscodeshift": "^0.6.0", 48 | "@types/prettier": "^1.16.0", 49 | "@types/react": "^16.8.2", 50 | "@types/shelljs": "^0.8.2", 51 | "@types/yargs": "^12.0.8", 52 | "babel-jest": "^24.1.0", 53 | "jest": "^24.1.0", 54 | "jscodeshift": "^0.6.3" 55 | }, 56 | "license": "MIT", 57 | "main": "index.js", 58 | "scripts": { 59 | "b": "babel-node --extensions \".es6,.js,.es,.jsx,.mjs,.ts,.tsx\"", 60 | "build": "rm -rf lib/* && babel src --extensions \".es6,.js,.es,.jsx,.mjs,.ts,.tsx\" --ignore __test__,__testfixtures__,*.spec.ts --out-dir lib", 61 | "fixtures": "yarn start --src=fixtures", 62 | "prepublish": "npm run build", 63 | "start": "yarn b src/index.ts", 64 | "test": "jest", 65 | "watch": "babel --extensions \".es6,.js,.es,.jsx,.mjs,.ts,.tsx\" -w -d ./lib ./src" 66 | }, 67 | "files": [ 68 | "lib", 69 | "bin" 70 | ] 71 | } 72 | -------------------------------------------------------------------------------- /src/BabelPluginI18n.ts: -------------------------------------------------------------------------------- 1 | import { PluginObj } from '@babel/core'; 2 | import { getStableKey, getStableValue } from './stableString'; 3 | import { 4 | hasStringLiteralArguments, 5 | hasStringLiteralJSXAttribute, 6 | isSvgElementAttribute 7 | } from "./visitorChecks"; 8 | import { NodePath } from "ast-types"; 9 | import { JSXElement } from "ast-types/gen/nodes"; 10 | import jsx from "ast-types/def/jsx"; 11 | 12 | let keyMaxLength = 40; 13 | let phrases: string[] = []; 14 | let i18nMap = {}; 15 | 16 | const addPhrase = (displayText: string, keyText?: string) => { 17 | const key = getStableKey(keyText ? keyText: displayText, keyMaxLength); 18 | const value = getStableValue(displayText); 19 | 20 | if (!key || !value) { 21 | return null; 22 | } 23 | 24 | i18nMap[key] = value; 25 | phrases = [ 26 | ...phrases, 27 | value, 28 | ]; 29 | 30 | return { key, value }; 31 | }; 32 | 33 | function BabelPluginI18n(): PluginObj { 34 | return { 35 | name: 'i18n', 36 | visitor: { 37 | JSXAttribute(path) { 38 | const { node } = path; 39 | 40 | if (hasStringLiteralJSXAttribute(path) && !isSvgElementAttribute(path)) { 41 | addPhrase(node.value.value); 42 | } 43 | }, 44 | JSXElement(path) { 45 | const { node } = path; 46 | const jsxContentNodes = node.children; 47 | extractArguments(jsxContentNodes) 48 | .forEach(({textWithArgs, textWithoutArgs}) => 49 | addPhrase(textWithArgs, textWithoutArgs)); 50 | 51 | }, 52 | JSXExpressionContainer(path) { 53 | const { node } = path; 54 | 55 | if (node.expression.type === 'StringLiteral') { 56 | addPhrase(path.node.expression.value); 57 | } else if (node.expression.type === 'ConditionalExpression') { 58 | let expression = path.node.expression; 59 | if (expression.consequent.type === 'StringLiteral') { 60 | addPhrase(expression.consequent.value) 61 | } 62 | if (expression.alternate.type === 'StringLiteral') { 63 | addPhrase(expression.alternate.value); 64 | } 65 | } 66 | }, 67 | CallExpression(path) { 68 | if (hasStringLiteralArguments(path)) { 69 | for (const arg of path.node.arguments) { 70 | if (arg.type === 'StringLiteral') { 71 | addPhrase(arg.value) 72 | } 73 | 74 | if (arg.type === 'ObjectExpression') { 75 | if (arg.properties.length === 0) { 76 | continue; 77 | } 78 | 79 | for (const prop of arg.properties) { 80 | if (prop.value && prop.value.type === 'StringLiteral') { 81 | addPhrase(prop.value.value); 82 | } 83 | } 84 | } 85 | } 86 | } 87 | } 88 | } 89 | } 90 | } 91 | 92 | function extractArguments(jsxContentNodes: NodePath[]) { 93 | let textWithArgs = ''; 94 | let textWithoutArgs = ''; 95 | let argIndex = 0; 96 | let hasText = false; 97 | const texts = []; 98 | for(let i = 0; i < jsxContentNodes.length; i++) { 99 | const element = jsxContentNodes[i]; 100 | if (element.type === 'JSXText') { 101 | hasText = true; 102 | textWithArgs += element.value; 103 | textWithoutArgs += element.value; 104 | } else if (element.type === 'JSXExpressionContainer') { 105 | textWithArgs += `{arg${argIndex}}`; 106 | argIndex++; 107 | } else { 108 | if (hasText) { 109 | texts.push({ textWithArgs, textWithoutArgs }); 110 | textWithArgs = ''; 111 | textWithoutArgs = '' 112 | } 113 | } 114 | } 115 | if (hasText) { 116 | texts.push({ textWithArgs, textWithoutArgs }); 117 | } 118 | return texts; 119 | } 120 | 121 | BabelPluginI18n.clear = () => { 122 | phrases = []; 123 | i18nMap = {}; 124 | }; 125 | BabelPluginI18n.setMaxKeyLength = (maxLength: number) => { 126 | keyMaxLength = maxLength; 127 | }; 128 | BabelPluginI18n.getExtractedStrings = () => phrases; 129 | BabelPluginI18n.getI18Map = () => i18nMap; 130 | 131 | export default BabelPluginI18n; 132 | -------------------------------------------------------------------------------- /src/__test__/filterFiles.test.ts: -------------------------------------------------------------------------------- 1 | import { filterFiles, DEFAULT_TEST_FILE_REGEX } from "../filterFiles"; 2 | 3 | const mockFilesPath = [ 4 | "src/__test__/filterFiles.test.ts", 5 | "src/__test__/generateResources.spec.ts", 6 | "src/__testfixtures__/CallExpression.input.tsx", 7 | "src/__testfixtures__/CallExpression.output.tsx", 8 | "src/userArgv.js", 9 | "src/visitorChecks.ts", 10 | "src/run.ex", 11 | ]; 12 | 13 | const shellFindMock = () => mockFilesPath; 14 | const filterFilesWithShell = filterFiles(shellFindMock); 15 | 16 | describe("filterFiles", () => { 17 | it(`should receive a path and return a list of files with (js|ts|tsx) extension and ignore files that match with ${DEFAULT_TEST_FILE_REGEX}`, () => { 18 | const files = filterFilesWithShell("./src"); 19 | 20 | expect(files).toStrictEqual([ 21 | "src/__testfixtures__/CallExpression.input.tsx", 22 | "src/__testfixtures__/CallExpression.output.tsx", 23 | "src/userArgv.js", 24 | "src/visitorChecks.ts", 25 | ]); 26 | }); 27 | 28 | it("should receive a path, a custom regex and return a list of files with (js|ts|tsx) extension and ignore the files that match with regex", () => { 29 | const files = filterFilesWithShell("./src", "/(__testfixtures__|__test__)/"); 30 | 31 | expect(files).toStrictEqual([ 32 | "src/userArgv.js", 33 | "src/visitorChecks.ts", 34 | ]); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /src/__test__/generateResources.spec.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | 3 | import { generateResources, getResourceSource } from '../generateResources'; 4 | import fs from "fs"; 5 | import BabelPluginI18n from '../BabelPluginI18n'; 6 | 7 | const defineTest = (dirName: string, testFilePrefix: string, only: boolean = false) => { 8 | const testName = `extra string from ${testFilePrefix}`; 9 | 10 | const myIt = only ? it.only : it; 11 | 12 | myIt(testName, () => { 13 | const fixtureDir = path.join(dirName, '..', '__testfixtures__'); 14 | const inputPath = path.join(fixtureDir, testFilePrefix + '.input.tsx'); 15 | const expectedOutput = fs.readFileSync( 16 | path.join(fixtureDir, testFilePrefix + '.resource.tsx'), 17 | 'utf8' 18 | ); 19 | 20 | const files = [inputPath]; 21 | const i18nMap = generateResources(files); 22 | 23 | expect(getResourceSource(i18nMap).trim()).toEqual(expectedOutput.trim()); 24 | }); 25 | }; 26 | 27 | describe('generateResources', () => { 28 | beforeEach(() => { 29 | BabelPluginI18n.clear(); 30 | }); 31 | 32 | defineTest(__dirname, 'Classes'); 33 | defineTest(__dirname, 'Diacritics'); 34 | defineTest(__dirname, 'ExpressionContainer'); 35 | defineTest(__dirname, 'Functional'); 36 | defineTest(__dirname, 'Props'); 37 | defineTest(__dirname, 'Tsx'); 38 | defineTest(__dirname, 'Yup'); 39 | defineTest(__dirname, 'CallExpression'); 40 | defineTest(__dirname, 'Svg'); 41 | defineTest(__dirname, 'Parameters'); 42 | }); 43 | -------------------------------------------------------------------------------- /src/__test__/i18nTransfomerCodemod.test.ts: -------------------------------------------------------------------------------- 1 | import { defineTest } from '../../test/testUtils'; 2 | 3 | describe('i18nTransformerCodemod', () => { 4 | defineTest(__dirname, 'i18nTransformerCodemod', null, 'Classes'); 5 | defineTest(__dirname, 'i18nTransformerCodemod', null, 'Diacritics'); 6 | defineTest(__dirname, 'i18nTransformerCodemod', null, 'ExpressionContainer'); 7 | defineTest(__dirname, 'i18nTransformerCodemod', null, 'Functional'); 8 | defineTest(__dirname, 'i18nTransformerCodemod', null, 'Props'); 9 | defineTest(__dirname, 'i18nTransformerCodemod', null, 'Tsx'); 10 | defineTest(__dirname, 'i18nTransformerCodemod', null, 'Yup'); 11 | defineTest(__dirname, 'i18nTransformerCodemod', null, 'CallExpression'); 12 | defineTest(__dirname, 'i18nTransformerCodemod', null, 'Imported'); 13 | defineTest(__dirname, 'i18nTransformerCodemod', null, 'WithHoc'); 14 | defineTest(__dirname, 'i18nTransformerCodemod', null, 'NoChange'); 15 | defineTest(__dirname, 'i18nTransformerCodemod', null, 'Hooks'); 16 | defineTest(__dirname, 'i18nTransformerCodemod', null, 'HooksInlineExport'); 17 | defineTest(__dirname, 'i18nTransformerCodemod', null, 'Classnames'); 18 | defineTest(__dirname, 'i18nTransformerCodemod', null, 'Svg'); 19 | defineTest(__dirname, 'i18nTransformerCodemod', null, 'Parameters'); 20 | }); 21 | -------------------------------------------------------------------------------- /src/__test__/stableString.test.ts: -------------------------------------------------------------------------------- 1 | import { getStableKey } from '../stableString'; 2 | 3 | it('should handle convert to lowercase', () => { 4 | expect(getStableKey('AWESOME')).toBe('awesome'); 5 | }); 6 | 7 | it('should transform white space to underscore', () => { 8 | expect(getStableKey('AWESOME wOrk')).toBe('awesome_work'); 9 | }); 10 | 11 | it('should not add underscore because of start white space', () => { 12 | expect(getStableKey(' AWESOME wOrk')).toBe('awesome_work'); 13 | }); 14 | 15 | it('should not add underscore because of end white space', () => { 16 | expect(getStableKey(' AWESOME wOrk ')).toBe('awesome_work'); 17 | }); 18 | 19 | it('should not add multiple underscores because of multiple white spaces', () => { 20 | expect(getStableKey(' AWESOME wOrk ')).toBe('awesome_work'); 21 | }); 22 | 23 | it('should remove diacrits', () => { 24 | expect(getStableKey('Olá Brasil')).toBe('ola_brasil'); 25 | }); 26 | 27 | // TODO - figure it out how to remove this × 28 | it('handle weird chars', () => { 29 | expect(getStableKey('×_fechar')).toBe('_fechar'); 30 | }); 31 | -------------------------------------------------------------------------------- /src/__testfixtures__/CallExpression.input.tsx: -------------------------------------------------------------------------------- 1 | const callIt = ({ showSnackbar }) => { 2 | showSnackbar({ message: 'User editted successfully!'}); 3 | }; 4 | 5 | export default callIt; 6 | -------------------------------------------------------------------------------- /src/__testfixtures__/CallExpression.output.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslation } from 'react-i18next'; 2 | const callIt = ({ showSnackbar }) => { 3 | const { t } = useTranslation(); 4 | showSnackbar({ message: t('user_editted_successfully')}); 5 | }; 6 | 7 | export default callIt; 8 | -------------------------------------------------------------------------------- /src/__testfixtures__/CallExpression.resource.tsx: -------------------------------------------------------------------------------- 1 | export default { 2 | translation: { 3 | user_editted_successfully: `User editted successfully!`, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /src/__testfixtures__/Classes.input.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class MyClass extends React.Component { 4 | render() { 5 | return ( 6 |
7 | my great class component 8 |
9 | ); 10 | } 11 | } 12 | 13 | export default MyClass; 14 | -------------------------------------------------------------------------------- /src/__testfixtures__/Classes.output.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { withTranslation } from 'react-i18next'; 4 | 5 | class MyClass extends React.Component { 6 | render() { 7 | const { t } = this.props; 8 | return ( 9 |
10 | {t('my_great_class_component')} 11 |
12 | ); 13 | } 14 | } 15 | 16 | export default withTranslation()(MyClass); 17 | -------------------------------------------------------------------------------- /src/__testfixtures__/Classes.resource.tsx: -------------------------------------------------------------------------------- 1 | export default { 2 | translation: { 3 | my_great_class_component: `my great class component`, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /src/__testfixtures__/Classnames.input.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from "classnames"; 3 | 4 | function Button({ isPrimary, title }) { 5 | const className = classNames("special-button", { 6 | "special-button--primary": isPrimary 7 | }); 8 | 9 | return ; 10 | } 11 | 12 | export default Button; 13 | -------------------------------------------------------------------------------- /src/__testfixtures__/Classnames.output.tsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ast-i18n/f5ac4c365dd5279d77685a7a966b574c95d7ac80/src/__testfixtures__/Classnames.output.tsx -------------------------------------------------------------------------------- /src/__testfixtures__/Diacritics.input.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Simple = () => ( 4 | Olá Antônio 5 | ); 6 | 7 | export default Simple; 8 | -------------------------------------------------------------------------------- /src/__testfixtures__/Diacritics.output.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { useTranslation } from 'react-i18next'; 4 | 5 | const Simple = () => { 6 | const { t } = useTranslation(); 7 | return {t('ola_antonio')}; 8 | }; 9 | 10 | export default Simple; -------------------------------------------------------------------------------- /src/__testfixtures__/Diacritics.resource.tsx: -------------------------------------------------------------------------------- 1 | export default { 2 | translation: { 3 | ola_antonio: `Olá Antônio`, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /src/__testfixtures__/ExpressionContainer.input.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Simple = ({enabled, text}) => ( 4 |
5 | {"My simple text"} 6 | {enabled ? "OK" : "Not OK"} 7 | {text && text} 8 |
9 | ); 10 | 11 | export default Simple; -------------------------------------------------------------------------------- /src/__testfixtures__/ExpressionContainer.output.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { useTranslation } from 'react-i18next'; 4 | 5 | const Simple = ({enabled, text}) => { 6 | const { t } = useTranslation(); 7 | 8 | return ( 9 |
10 | {t('my_simple_text')} 11 | {enabled ? t('ok') : t('not_ok')} 12 | {text && text} 13 |
14 | ); 15 | }; 16 | 17 | export default Simple; -------------------------------------------------------------------------------- /src/__testfixtures__/ExpressionContainer.resource.tsx: -------------------------------------------------------------------------------- 1 | export default { 2 | translation: { 3 | my_simple_text: `My simple text`, 4 | ok: `OK`, 5 | not_ok: `Not OK`, 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /src/__testfixtures__/Functional.input.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function Simple() { 4 | return My simple text; 5 | } 6 | 7 | export default Simple; 8 | -------------------------------------------------------------------------------- /src/__testfixtures__/Functional.output.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { useTranslation } from 'react-i18next'; 4 | 5 | function Simple() { 6 | const { t } = useTranslation(); 7 | return {t('my_simple_text')}; 8 | } 9 | 10 | export default Simple; -------------------------------------------------------------------------------- /src/__testfixtures__/Functional.resource.tsx: -------------------------------------------------------------------------------- 1 | export default { 2 | translation: { 3 | my_simple_text: `My simple text`, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /src/__testfixtures__/Hooks.input.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | const SiteHeader = () => { 4 | const [text] = useState(''); 5 | return ( 6 | My simple text 7 | ); 8 | }; 9 | 10 | export default SiteHeader; 11 | -------------------------------------------------------------------------------- /src/__testfixtures__/Hooks.output.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | import { useTranslation } from 'react-i18next'; 4 | 5 | const SiteHeader = () => { 6 | const { t } = useTranslation(); 7 | const [text] = useState(''); 8 | return {t('my_simple_text')}; 9 | }; 10 | 11 | export default SiteHeader; 12 | -------------------------------------------------------------------------------- /src/__testfixtures__/HooksInlineExport.input.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | 3 | export default function SiteFooter() { 4 | const [text] = useState("Something something"); 5 | return ( 6 | <> 7 | {text} 8 | My simple text 9 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /src/__testfixtures__/HooksInlineExport.output.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | 3 | import { useTranslation } from 'react-i18next'; 4 | 5 | export default function SiteFooter() { 6 | const { t } = useTranslation(); 7 | const [text] = useState(t('something_something')); 8 | return <> 9 | {text} 10 | {t('my_simple_text')} 11 | ; 12 | } -------------------------------------------------------------------------------- /src/__testfixtures__/Imported.input.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withTranslation } from 'react-i18next'; 3 | 4 | const Simple = () => ( 5 | My simple text 6 | ); 7 | -------------------------------------------------------------------------------- /src/__testfixtures__/Imported.output.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withTranslation } from 'react-i18next'; 3 | 4 | const Simple = () => ( 5 | {t('my_simple_text')} 6 | ); 7 | -------------------------------------------------------------------------------- /src/__testfixtures__/NoChange.input.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { MuiPickersUtilsProvider } from 'material-ui-pickers'; 3 | import { MuiThemeProvider } from '@material-ui/core/styles'; 4 | import { BrowserRouter } from 'react-router-dom'; 5 | import DateFnsUtils from '@date-io/date-fns'; 6 | import muiTheme from './muiTheme'; 7 | import Routes from './Routes'; 8 | 9 | 10 | function App() { 11 | return ( 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | ); 20 | } 21 | 22 | export default App; 23 | -------------------------------------------------------------------------------- /src/__testfixtures__/NoChange.output.tsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ast-i18n/f5ac4c365dd5279d77685a7a966b574c95d7ac80/src/__testfixtures__/NoChange.output.tsx -------------------------------------------------------------------------------- /src/__testfixtures__/Parameters.input.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | 3 | const SiteHeader = () => { 4 | const [number] = useState(42); 5 | return 6 | My simple {number} text 7 | Other text 8 | Even more Text 9 | ; 10 | }; 11 | 12 | export default SiteHeader; 13 | -------------------------------------------------------------------------------- /src/__testfixtures__/Parameters.output.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | 3 | import { useTranslation } from 'react-i18next'; 4 | 5 | const SiteHeader = () => { 6 | const { t } = useTranslation(); 7 | const [number] = useState(42); 8 | return ( 9 | {t('my_simple_text', { 10 | arg0: number 11 | })}{t('other_text')}{t('even_more_text')} 12 | ); 13 | }; 14 | 15 | export default SiteHeader; -------------------------------------------------------------------------------- /src/__testfixtures__/Parameters.resource.tsx: -------------------------------------------------------------------------------- 1 | export default { 2 | translation: { 3 | my_simple_text: `My simple {arg0} text`, 4 | even_more_text: `Even more Text`, 5 | other_text: `Other text`, 6 | }, 7 | }; -------------------------------------------------------------------------------- /src/__testfixtures__/Props.input.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | type CustomProps = { 4 | title: string, 5 | } 6 | const Custom = (props: CustomProps) => { 7 | return ( 8 |
9 | {props.title} 10 |
11 | ) 12 | } 13 | 14 | const Component123 = () => ( 15 |
16 | Simple text 17 | 18 |
19 | ); 20 | 21 | export default Component123; 22 | -------------------------------------------------------------------------------- /src/__testfixtures__/Props.output.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { useTranslation } from 'react-i18next'; 4 | 5 | type CustomProps = { 6 | title: string, 7 | } 8 | const Custom = (props: CustomProps) => { 9 | return ( 10 |
11 | {props.title} 12 |
13 | ); 14 | } 15 | 16 | const Component123 = () => { 17 | const { t } = useTranslation(); 18 | 19 | return ( 20 |
21 | {t('simple_text')} 22 | 23 |
24 | ); 25 | }; 26 | 27 | export default Component123; -------------------------------------------------------------------------------- /src/__testfixtures__/Props.resource.tsx: -------------------------------------------------------------------------------- 1 | export default { 2 | translation: { 3 | simple_text: `Simple text`, 4 | custom_name: `Custom name`, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /src/__testfixtures__/Svg.input.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const SomeIcon = () => { 4 | return ( 5 | 6 | 7 | 8 | 9 | 13 | 14 | 15 | 16 | 17 | ); 18 | }; 19 | 20 | export default SomeIcon; 21 | -------------------------------------------------------------------------------- /src/__testfixtures__/Svg.output.tsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ast-i18n/f5ac4c365dd5279d77685a7a966b574c95d7ac80/src/__testfixtures__/Svg.output.tsx -------------------------------------------------------------------------------- /src/__testfixtures__/Svg.resource.tsx: -------------------------------------------------------------------------------- 1 | export default { 2 | translation: {}, 3 | }; 4 | -------------------------------------------------------------------------------- /src/__testfixtures__/Tsx.input.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | type CustomProps = { 4 | title: string, 5 | } 6 | const Custom = (props: CustomProps) => { 7 | return ( 8 |
9 | {props.title} 10 |
11 | ) 12 | } 13 | 14 | const Simple = () => ( 15 |
16 | Simple text 17 | 18 |
19 | ); 20 | 21 | export default Simple; 22 | -------------------------------------------------------------------------------- /src/__testfixtures__/Tsx.output.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { useTranslation } from 'react-i18next'; 4 | 5 | type CustomProps = { 6 | title: string, 7 | } 8 | const Custom = (props: CustomProps) => { 9 | return ( 10 |
11 | {props.title} 12 |
13 | ); 14 | } 15 | 16 | const Simple = () => { 17 | const { t } = useTranslation(); 18 | 19 | return ( 20 |
21 | {t('simple_text')} 22 | 23 |
24 | ); 25 | }; 26 | 27 | export default Simple; -------------------------------------------------------------------------------- /src/__testfixtures__/Tsx.resource.tsx: -------------------------------------------------------------------------------- 1 | export default { 2 | translation: { 3 | simple_text: `Simple text`, 4 | custom_name: `Custom name`, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /src/__testfixtures__/WithHoc.input.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withSnackbar } from 'snackbar'; 3 | 4 | function Simple() { 5 | return My simple text; 6 | } 7 | 8 | export default withSnackbar(Simple); 9 | -------------------------------------------------------------------------------- /src/__testfixtures__/WithHoc.output.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withSnackbar } from 'snackbar'; 3 | 4 | import { useTranslation } from 'react-i18next'; 5 | 6 | function Simple() { 7 | const { t } = useTranslation(); 8 | return {t('my_simple_text')}; 9 | } 10 | 11 | export default withSnackbar(Simple); -------------------------------------------------------------------------------- /src/__testfixtures__/Yup.input.tsx: -------------------------------------------------------------------------------- 1 | import { withFormik } from 'formik'; 2 | import * as yup from 'yup'; 3 | 4 | const UserInnerForm = () => ( 5 | user form here 6 | ); 7 | 8 | type Values = { 9 | name: string, 10 | email: string, 11 | } 12 | const UserForm = withFormik({ 13 | validationSchema: yup.object().shape({ 14 | name: yup.string().required('Name is required'), 15 | email: yup.string().required('Email is required'), 16 | }), 17 | handleSubmit: (values: Values, formikBag) => { 18 | const { props } = formikBag; 19 | const { showSnackbar } = props; 20 | 21 | showSnackbar({ message: 'User editted successfully!'}); 22 | }, 23 | })(UserInnerForm); 24 | 25 | export default UserForm; 26 | -------------------------------------------------------------------------------- /src/__testfixtures__/Yup.output.tsx: -------------------------------------------------------------------------------- 1 | import { withFormik } from 'formik'; 2 | import * as yup from 'yup'; 3 | 4 | import { withTranslation } from 'react-i18next'; 5 | 6 | const UserInnerForm = () => ( 7 | {t('user_form_here')} 8 | ); 9 | 10 | type Values = { 11 | name: string, 12 | email: string, 13 | } 14 | const UserForm = withFormik({ 15 | validationSchema: yup.object().shape({ 16 | name: yup.string().required(t('name_is_required')), 17 | email: yup.string().required(t('email_is_required')), 18 | }), 19 | handleSubmit: (values: Values, formikBag) => { 20 | const { props } = formikBag; 21 | const { showSnackbar } = props; 22 | 23 | showSnackbar({ message: t('user_editted_successfully')}); 24 | }, 25 | })(UserInnerForm); 26 | 27 | export default withTranslation()(UserForm); 28 | -------------------------------------------------------------------------------- /src/__testfixtures__/Yup.resource.tsx: -------------------------------------------------------------------------------- 1 | export default { 2 | translation: { 3 | user_form_here: `user form here`, 4 | name_is_required: `Name is required`, 5 | email_is_required: `Email is required`, 6 | user_editted_successfully: `User editted successfully!`, 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /src/cli.ts: -------------------------------------------------------------------------------- 1 | import yargs from 'yargs'; 2 | import shell from 'shelljs'; 3 | 4 | import { generateResources } from './generateResources'; 5 | import { filterFiles, DEFAULT_TEST_FILE_REGEX } from './filterFiles'; 6 | 7 | type Argv = { 8 | src: string, 9 | keyMaxLength: number, 10 | ignoreFilesRegex: string; 11 | } 12 | 13 | export const run = (argv: Argv) => { 14 | argv = yargs(argv || process.argv.slice(2)) 15 | .usage( 16 | 'Extract all string inside JSXElement' 17 | ) 18 | .default('src', process.cwd()) 19 | .describe( 20 | 'src', 21 | 'The source to collect strings' 22 | ) 23 | .default('keyMaxLength', 40) 24 | .describe( 25 | 'src', 26 | 'The source to collect strings' 27 | ) 28 | .default('ignoreFilesRegex', DEFAULT_TEST_FILE_REGEX) 29 | .describe( 30 | 'ignoreFilesRegex', 31 | `The regex to ignore files in the source.\nThe files with this match is ignored by default:\n${DEFAULT_TEST_FILE_REGEX}` 32 | ) 33 | .argv; 34 | 35 | const jsFiles = filterFiles(shell.find)(argv.src, argv.ignoreFilesRegex); 36 | 37 | generateResources(jsFiles, argv.keyMaxLength); 38 | }; 39 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import cosmiconfig from 'cosmiconfig'; 2 | 3 | const explorer = cosmiconfig('ast'); 4 | 5 | type ASTConfig = { 6 | blackListJsxAttributeName: string[], 7 | blackListCallExpressionCalle: string[], 8 | } 9 | 10 | const defaultConfig: ASTConfig = { 11 | blackListJsxAttributeName: [ 12 | 'type', 13 | 'id', 14 | 'name', 15 | 'children', 16 | 'labelKey', 17 | 'valueKey', 18 | 'labelValue', 19 | 'className', 20 | ], 21 | blackListCallExpressionCalle: [ 22 | 't', 23 | '_interopRequireDefault', 24 | 'require', 25 | 'routeTo', 26 | 'format', 27 | 'importScripts', 28 | ], 29 | }; 30 | 31 | let config: ASTConfig | null = null; 32 | 33 | export const getAstConfig = (): ASTConfig => { 34 | if (config) { 35 | return config; 36 | } 37 | 38 | const result = explorer.searchSync(); 39 | 40 | if (result) { 41 | config = { 42 | ...defaultConfig, 43 | ...result.config, 44 | }; 45 | 46 | return config; 47 | } 48 | 49 | config = defaultConfig; 50 | 51 | return config; 52 | }; 53 | -------------------------------------------------------------------------------- /src/filterFiles.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_TEST_FILE_REGEX = 2 | "(/__tests__/.*|(\\.|/)(test|spec))\\.(js|ts|tsx|jsx)?$"; 3 | 4 | type ShellFind = (...path: Array) => string[]; 5 | 6 | export const filterFiles = (shellFind: ShellFind) => ( 7 | path: string, 8 | ignoreFilesRegex?: string 9 | ) => { 10 | const regex = new RegExp(ignoreFilesRegex || DEFAULT_TEST_FILE_REGEX); 11 | return shellFind(path).filter( 12 | (path) => /\.(js|ts|tsx)$/.test(path) && !regex.test(path) 13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /src/generateResources.ts: -------------------------------------------------------------------------------- 1 | import fs from 'graceful-fs'; 2 | import * as babel from '@babel/core'; 3 | import prettier, { Options } from 'prettier'; 4 | 5 | import BabelPluginI18n from './BabelPluginI18n'; 6 | 7 | import babelConfig from '../babel.config.js'; 8 | 9 | export const resource = (i18nResource: {[key: string]: string}) => { 10 | const formatted = Object.keys(i18nResource) 11 | .map(key => ` '${key}': \`${i18nResource[key]}\``) 12 | .join(',\n'); 13 | 14 | return `export default { 15 | translation: { 16 | ${formatted} 17 | } 18 | } 19 | ` 20 | }; 21 | 22 | const prettierDefaultConfig: Options = { 23 | singleQuote: true, 24 | jsxSingleQuote: true, 25 | trailingComma: 'all', 26 | printWidth: 120, 27 | parser: 'babel', 28 | }; 29 | 30 | export const getResourceSource = (i18nResource: {[key: string]: string}) => { 31 | const source = resource(i18nResource); 32 | 33 | return prettier.format(source, prettierDefaultConfig); 34 | }; 35 | 36 | export const generateResources = (files: string[], keyMaxLength: number = 40) => { 37 | BabelPluginI18n.setMaxKeyLength(keyMaxLength); 38 | 39 | let phrases = []; 40 | for (const filename of files) { 41 | const source = fs.readFileSync(filename, 'utf8'); 42 | 43 | try { 44 | babel.transformSync(source, { 45 | ast: false, 46 | code: true, 47 | plugins: [...babelConfig.plugins, BabelPluginI18n], 48 | sourceType: 'unambiguous', 49 | filename, 50 | }); 51 | 52 | const newPhrases = BabelPluginI18n.getExtractedStrings(); 53 | 54 | phrases = [ 55 | ...phrases, 56 | ...newPhrases, 57 | ]; 58 | } catch (err) { 59 | console.log('err: ', filename, err); 60 | } 61 | } 62 | 63 | const i18nMap = BabelPluginI18n.getI18Map(); 64 | 65 | fs.writeFileSync('resource.tsx', resource(i18nMap)); 66 | 67 | // tslint:disable-next-line 68 | console.log('generate resource file: resource.tsx'); 69 | 70 | return i18nMap; 71 | }; 72 | -------------------------------------------------------------------------------- /src/i18nTransformerCodemod.ts: -------------------------------------------------------------------------------- 1 | import { API, FileInfo, Options, JSCodeshift, Collection } from 'jscodeshift'; 2 | import { getStableKey } from './stableString'; 3 | import { hasStringLiteralArguments, hasStringLiteralJSXAttribute, isSvgElement } from "./visitorChecks"; 4 | import { CallExpression, ImportDeclaration, JSXAttribute, JSXText } from "@babel/types"; 5 | import { 6 | ConditionalExpression, 7 | JSXElement, 8 | JSXExpressionContainer 9 | } from "ast-types/gen/nodes"; 10 | import { NodePath } from "ast-types"; 11 | 12 | const tCallExpression = (j: JSCodeshift, key: string) => { 13 | return j.callExpression( 14 | j.identifier('t'), 15 | [j.stringLiteral(key)], 16 | ); 17 | }; 18 | 19 | const getImportStatement = (useHoc: boolean = true, useHooks: boolean = false) => { 20 | if (useHoc && !useHooks) { 21 | return `import { withTranslation } from 'react-i18next';`; 22 | } 23 | 24 | if (useHooks && !useHoc) { 25 | return `import { useTranslation } from 'react-i18next';`; 26 | } 27 | 28 | return `import { useTranslation, withTranslation } from 'react-i18next';`; 29 | }; 30 | 31 | const addI18nImport = (j: JSCodeshift, root: Collection, {useHooks, useHoc}: any) => { 32 | // TODO - handle hoc or hooks based on file 33 | const statement = getImportStatement(useHoc, useHooks); 34 | 35 | // check if there is a react-i18next import already 36 | const reactI18nNextImports = root 37 | .find(j.ImportDeclaration) 38 | .filter((path : NodePath) => path.node.source.value === 'react-i18next'); 39 | 40 | if (reactI18nNextImports.length > 0) { 41 | return; 42 | } 43 | 44 | const imports = root.find(j.ImportDeclaration); 45 | 46 | if(imports.length > 0){ 47 | j(imports.at(imports.length-1).get()).insertAfter(statement); // after the imports 48 | }else{ 49 | root.get().node.program.body.unshift(statement); // begining of file 50 | } 51 | }; 52 | 53 | function transform(file: FileInfo, api: API, options: Options) { 54 | const j = api.jscodeshift; // alias the jscodeshift API 55 | if (file.path.endsWith('.spec.js') || file.path.endsWith('.test.js')) { 56 | return; 57 | } 58 | const root = j(file.source); // parse JS code into an AST 59 | 60 | const printOptions = options.printOptions || { 61 | quote: 'single', 62 | trailingComma: false, 63 | // TODO make this configurable 64 | lineTerminator: '\n' 65 | }; 66 | 67 | let hasI18nUsage = false; 68 | 69 | hasI18nUsage = translateJsxContent(j, root) || hasI18nUsage; 70 | hasI18nUsage = translateJsxProps(j, root) || hasI18nUsage; 71 | hasI18nUsage = translateFunctionArguments(j, root) || hasI18nUsage; 72 | 73 | if (hasI18nUsage) { 74 | // export default withTranslation()(Component) 75 | let hooksUsed = false; 76 | let hocUsed = false; 77 | root 78 | .find(j.ExportDefaultDeclaration) 79 | .filter(path => { 80 | let exportDeclaration = path.node.declaration; 81 | return j.Identifier.check(exportDeclaration) 82 | || j.CallExpression.check(exportDeclaration) 83 | || j.FunctionDeclaration.check(exportDeclaration); 84 | }) 85 | .forEach(path => { 86 | let exportDeclaration = path.node.declaration; 87 | 88 | if (j.Identifier.check(exportDeclaration)) { 89 | const exportedName = exportDeclaration.name; 90 | const functions = findFunctionByIdentifier(j, exportedName, root); 91 | let hookFound = addUseHookToFunctionBody( 92 | j, functions 93 | ); 94 | 95 | if(!hookFound) { 96 | hocUsed = true; 97 | path.node.declaration = withTranslationHoc(j, j.identifier(exportDeclaration.name)); 98 | const classDeclaration = root.find(j.ClassDeclaration, (n) => n.id.name === exportDeclaration.name) 99 | .nodes()[0]; 100 | if (classDeclaration) { 101 | const renderMethod = classDeclaration.body.body.find( 102 | n => j.ClassMethod.check(n) && n.key.name === 'render' 103 | ); 104 | if (renderMethod) { 105 | renderMethod.body = j.blockStatement([ 106 | createTranslationDefinition(j), 107 | ...renderMethod.body.body 108 | ]) 109 | } 110 | } 111 | 112 | } else { 113 | hooksUsed = true; 114 | } 115 | return; 116 | } 117 | else if (j.CallExpression.check(exportDeclaration)) { 118 | if (exportDeclaration.callee.name === 'withTranslate') { 119 | return; 120 | } 121 | 122 | exportDeclaration.arguments.forEach(identifier => { 123 | const functions = findFunctionByIdentifier(j, identifier.name, root); 124 | hooksUsed = addUseHookToFunctionBody(j, functions) || hooksUsed; 125 | }); 126 | 127 | if (!hooksUsed) { 128 | hooksUsed = true; 129 | path.node.declaration = withTranslationHoc(j, exportDeclaration); 130 | } 131 | } else if (j.FunctionDeclaration.check(exportDeclaration)) { 132 | hooksUsed = true; 133 | exportDeclaration.body = j.blockStatement([createUseTranslationCall(j), ...exportDeclaration.body.body]) 134 | } 135 | }); 136 | 137 | addI18nImport(j, root, {useHooks: hooksUsed, useHoc: hocUsed}); 138 | // print 139 | return root.toSource(printOptions); 140 | } 141 | } 142 | 143 | function createUseTranslationCall(j: JSCodeshift) { 144 | return j.variableDeclaration('const', 145 | [j.variableDeclarator( 146 | j.identifier('{ t }'), 147 | j.callExpression(j.identifier('useTranslation'), []) 148 | )] 149 | ); 150 | } 151 | 152 | function createTranslationDefinition(j: JSCodeshift) { 153 | return j.variableDeclaration('const', 154 | [j.variableDeclarator( 155 | j.identifier('{ t }'), 156 | j.memberExpression(j.thisExpression(), j.identifier('props')) 157 | )] 158 | ); 159 | } 160 | 161 | function findFunctionByIdentifier(j: JSCodeshift, identifier: string, root: Collection) { 162 | return root.find(j.Function) 163 | .filter((p: NodePath) => { 164 | if (j.FunctionDeclaration.check(p.node)) { 165 | return p.node.id.name === identifier; 166 | } 167 | return p.parent.value.id && p.parent.value.id.name === identifier; 168 | }); 169 | } 170 | 171 | function addUseHookToFunctionBody(j: JSCodeshift, functions: Collection) { 172 | let hookFound = false; 173 | functions 174 | .every(n => { 175 | hookFound = true; 176 | const body = n.node.body; 177 | n.node.body = j.BlockStatement.check(body) 178 | ? j.blockStatement([createUseTranslationCall(j), ...body.body]) 179 | : j.blockStatement([createUseTranslationCall(j), j.returnStatement(body)]) 180 | }); 181 | return hookFound; 182 | } 183 | 184 | // Yup.string().required('this field is required') 185 | // showSnackbar({ message: 'ok' }) 186 | function translateFunctionArguments(j: JSCodeshift, root: Collection) { 187 | let hasI18nUsage = false; 188 | root 189 | .find(j.CallExpression) 190 | .filter((path: NodePath) => !['classNames'].includes(path.value.callee.name)) 191 | .filter((path: NodePath) => hasStringLiteralArguments(path)) 192 | .forEach((path: NodePath) => { 193 | if (hasStringLiteralArguments(path)) { 194 | path.node.arguments = path.node.arguments.map(arg => { 195 | if (arg.type === 'StringLiteral' && arg.value) { 196 | const key = getStableKey(arg.value); 197 | hasI18nUsage = true; 198 | 199 | return tCallExpression(j, key) 200 | } 201 | 202 | if (arg.type === 'ObjectExpression') { 203 | arg.properties = arg.properties.map(prop => { 204 | if (prop.value && prop.value.type === 'StringLiteral') { 205 | 206 | const key = getStableKey(prop.value.value); 207 | prop.value = tCallExpression(j, key); 208 | hasI18nUsage = true; 209 | } 210 | return prop; 211 | }); 212 | } 213 | 214 | return arg; 215 | }) 216 | } 217 | }); 218 | 219 | return hasI18nUsage; 220 | } 221 | 222 | //test 223 | function translateJsxContent(j: JSCodeshift, root: Collection) { 224 | let hasI18nUsage = false; 225 | root.find(j.JSXElement) 226 | .forEach((n: NodePath) => { 227 | const jsxContentNodes = n.value.children; 228 | let text = ''; 229 | let translateArgs = []; 230 | let newChildren = []; 231 | for(let i = 0; i < jsxContentNodes.length; i++) { 232 | const element = jsxContentNodes[i]; 233 | if (j.JSXText.check(element)) { 234 | if (element.value.trim().length > 0) { 235 | text += element.value; 236 | } else { 237 | newChildren.push(element); 238 | } 239 | continue; 240 | } else if (j.JSXExpressionContainer.check(element)) { 241 | translateArgs.push(element.expression); 242 | continue; 243 | } 244 | if (text.trim().length > 0) { 245 | hasI18nUsage = true; 246 | newChildren.push(buildTranslationWithArgumentsCall( 247 | j, translateArgs, text.trim() 248 | )); 249 | } 250 | text = ''; 251 | translateArgs = []; 252 | newChildren.push(element); 253 | } 254 | 255 | if (text.trim().length > 0) { 256 | hasI18nUsage = true; 257 | newChildren.push(buildTranslationWithArgumentsCall( 258 | j, translateArgs, text.trim() 259 | )); 260 | } 261 | if (newChildren.length > 0) { 262 | //n.value.children = newChildren; 263 | n.replace( 264 | j.jsxElement( 265 | n.node.openingElement, n.node.closingElement, 266 | newChildren 267 | ) 268 | ) 269 | } 270 | }); 271 | 272 | root 273 | .find(j.JSXText) 274 | .filter((path: NodePath) => path.node.value && path.node.value.trim()) 275 | .replaceWith((path: NodePath) => { 276 | hasI18nUsage = true; 277 | const key = getStableKey(path.node.value); 278 | return j.jsxExpressionContainer(j.callExpression(j.identifier('t'), [j.literal(key)])) 279 | }); 280 | return hasI18nUsage; 281 | } 282 | 283 | function translateJsxProps(j: JSCodeshift, root: Collection) { 284 | let hasI18nUsage = false; 285 | // 286 | root 287 | .find(j.JSXElement) 288 | .filter((path: NodePath) => !isSvgElement(path)) 289 | .find(j.JSXAttribute) 290 | .filter((path: NodePath) => hasStringLiteralJSXAttribute(path)) 291 | .forEach((path: NodePath) => { 292 | if (!path.node.value || !path.node.value.value) { 293 | return; 294 | } 295 | const key = getStableKey(path.node.value.value); 296 | hasI18nUsage = true; 297 | 298 | path.node.value = j.jsxExpressionContainer( 299 | tCallExpression(j, key), 300 | ); 301 | }); 302 | 303 | // 304 | root 305 | .find(j.JSXExpressionContainer) 306 | .filter((path: NodePath) => { 307 | return path.node.expression && j.StringLiteral.check(path.node.expression) 308 | }) 309 | .forEach(path => { 310 | const key = getStableKey(path.node.expression.value); 311 | hasI18nUsage = true; 312 | 313 | path.node.expression = tCallExpression(j, key); 314 | }); 315 | 316 | // 317 | root 318 | .find(j.JSXExpressionContainer) 319 | .filter((path: NodePath) => { 320 | return path.node.expression && j.ConditionalExpression.check(path.node.expression) 321 | }) 322 | .forEach(((path: NodePath) => { 323 | let expression = path.value.expression; 324 | if (j.Literal.check(expression.consequent)) { 325 | hasI18nUsage = true; 326 | const key = getStableKey(expression.consequent.value); 327 | expression.consequent = tCallExpression(j, key); 328 | } 329 | if (j.Literal.check(expression.alternate)) { 330 | hasI18nUsage = true; 331 | const key = getStableKey(expression.alternate.value); 332 | expression.alternate = tCallExpression(j, key); 333 | } 334 | hasI18nUsage = true; 335 | })); 336 | 337 | return hasI18nUsage; 338 | } 339 | 340 | function buildTranslationWithArgumentsCall(j: JSCodeshift, translateArgs: any, text: string) { 341 | const translationCallArguments = [ 342 | j.literal(getStableKey(text)), 343 | ] as any; 344 | if (translateArgs.length > 0) { 345 | translationCallArguments.push( 346 | j.objectExpression( 347 | translateArgs.map((expression: any, index: any) => 348 | j.property('init', j.identifier('arg' + index), expression ) 349 | ) 350 | ) 351 | ) 352 | } 353 | return j.jsxExpressionContainer( 354 | j.callExpression(j.identifier('t'), translationCallArguments)); 355 | } 356 | 357 | function withTranslationHoc(j: JSCodeshift, identifier: any) { 358 | return j.callExpression( 359 | j.callExpression( 360 | j.identifier('withTranslation'), 361 | [], 362 | ), 363 | [ 364 | identifier 365 | ], 366 | ) 367 | } 368 | 369 | 370 | module.exports = transform; 371 | module.exports.parser = 'tsx'; 372 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | get list of files from command line 3 | parse each file to find JSXText (where the translable string should be extracted from) 4 | generate a stable key for each string 5 | generate i18n files based on this 6 | */ 7 | import shell from 'shelljs'; 8 | import yargs from 'yargs'; 9 | 10 | import { generateResources } from './generateResources'; 11 | 12 | const argv = yargs 13 | .usage( 14 | 'Extract all string inside JSXElement' 15 | ) 16 | .default('src', process.cwd()) 17 | .describe( 18 | 'src', 19 | 'The source to collect strings' 20 | ) 21 | .default('keyMaxLength', 40) 22 | .describe( 23 | 'src', 24 | 'The source to collect strings' 25 | ) 26 | .argv; 27 | 28 | const jsFiles = shell.find(argv.src).filter(path => /\.(js|ts|tsx)$/.test(path)); 29 | generateResources(jsFiles, argv.keyMaxLength); 30 | -------------------------------------------------------------------------------- /src/stableString.ts: -------------------------------------------------------------------------------- 1 | import { remove } from 'diacritics'; 2 | import slugify from 'slugify'; 3 | 4 | export const getStableKey = (str: string, keyMaxLength: number = 40) => { 5 | const cleanStr = remove(str) 6 | .toLocaleLowerCase() 7 | .normalize('NFD') 8 | .replace(/[\u0300-\u036f]/g, "") 9 | .trim() 10 | .replace(/ +/g, '_') 11 | .replace(/\s+/g, '') 12 | .replace(/[.*+?^${}()|[\]\\\/-:,!"]/g, '') 13 | .replace(/'+/g, '') 14 | .replace(/[^\x00-\x7F]/g, "") 15 | .slice(0, keyMaxLength); 16 | 17 | return slugify(cleanStr); 18 | }; 19 | 20 | export const getStableValue = (str: string) => { 21 | return str 22 | .trim() 23 | .replace(/\s+/g, ' ') 24 | }; 25 | -------------------------------------------------------------------------------- /src/visitorChecks.ts: -------------------------------------------------------------------------------- 1 | import { CallExpression, JSXAttribute } from '@babel/types'; 2 | import { NodePath } from "ast-types"; 3 | import { JSXElement, JSXIdentifier } from "ast-types/gen/nodes"; 4 | import { getAstConfig } from './config'; 5 | 6 | const svgElementNames = ["svg", 'path', 'g']; 7 | 8 | export const hasStringLiteralJSXAttribute = (path: NodePath) => { 9 | if (!path.node.value) { 10 | return false; 11 | } 12 | 13 | if (path.node.value.type !== 'StringLiteral') { 14 | return false; 15 | } 16 | 17 | const { blackListJsxAttributeName } = getAstConfig(); 18 | 19 | if (blackListJsxAttributeName.indexOf(path.node.name.name) > -1) { 20 | return false; 21 | } 22 | 23 | return true; 24 | }; 25 | 26 | export const hasStringLiteralArguments = (path: NodePath) => { 27 | const { callee } = path.node; 28 | 29 | const { blackListCallExpressionCalle } = getAstConfig(); 30 | 31 | if (callee.type === 'Identifier') { 32 | const { callee } = path.node; 33 | 34 | if (blackListCallExpressionCalle.indexOf(callee.name) > -1) { 35 | return false; 36 | } 37 | } 38 | 39 | if (callee.type === 'Import') { 40 | return false; 41 | } 42 | 43 | if (callee.type === 'MemberExpression') { 44 | const { property } = path.node.callee; 45 | 46 | if (property && property.type === 'Identifier' && property.name === 'required') { 47 | if (path.node.arguments.length === 1) { 48 | if (path.node.arguments[0].type === 'StringLiteral') { 49 | return true; 50 | } 51 | } 52 | 53 | return true; 54 | } 55 | 56 | // do not convert react expressions 57 | return false; 58 | } 59 | 60 | if (path.node.arguments.length === 0) { 61 | return false; 62 | } 63 | 64 | for (const arg of path.node.arguments) { 65 | // myFunc('ok') 66 | if (arg.type === 'StringLiteral') { 67 | return true; 68 | } 69 | 70 | // showSnackbar({ message: 'ok' }); 71 | if (arg.type === 'ObjectExpression') { 72 | if (arg.properties.length === 0) { 73 | continue; 74 | } 75 | 76 | for (const prop of arg.properties) { 77 | if (prop.value && prop.value.type === 'StringLiteral') { 78 | return true; 79 | } 80 | } 81 | } 82 | 83 | // myFunc(['ok', 'blah']) - should we handle this case? 84 | } 85 | 86 | return false; 87 | }; 88 | 89 | export const isSvgElement = (path: NodePath) => { 90 | const jsxIdentifier = path.node.openingElement.name = path.node.openingElement.name as JSXIdentifier; 91 | return svgElementNames.includes(jsxIdentifier.name); 92 | }; 93 | 94 | export const isSvgElementAttribute = (path: NodePath) => { 95 | if (!path.parent || !path.parent.name) { 96 | return false; 97 | } 98 | return svgElementNames.includes(path.parent.name.name); 99 | }; 100 | -------------------------------------------------------------------------------- /test/testUtils.ts: -------------------------------------------------------------------------------- 1 | // based on jscodeshift code base 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | 5 | function runInlineTest(module, options, input, expectedOutput) { 6 | // Handle ES6 modules using default export for the transform 7 | const transform = module.default ? module.default : module; 8 | 9 | // Jest resets the module registry after each test, so we need to always get 10 | // a fresh copy of jscodeshift on every test run. 11 | let jscodeshift = require('jscodeshift'); 12 | if (module.parser) { 13 | jscodeshift = jscodeshift.withParser(module.parser); 14 | } 15 | 16 | const output = transform( 17 | input, 18 | { 19 | jscodeshift, 20 | stats: () => {}, 21 | }, 22 | options || {} 23 | ); 24 | expect((output || '').trim()).toEqual(expectedOutput.trim()); 25 | } 26 | exports.runInlineTest = runInlineTest; 27 | 28 | /** 29 | * Utility function to run a jscodeshift script within a unit test. This makes 30 | * several assumptions about the environment: 31 | * 32 | * - `dirName` contains the name of the directory the test is located in. This 33 | * should normally be passed via __dirname. 34 | * - The test should be located in a subdirectory next to the transform itself. 35 | * Commonly tests are located in a directory called __tests__. 36 | * - `transformName` contains the filename of the transform being tested, 37 | * excluding the .js extension. 38 | * - `testFilePrefix` optionally contains the name of the file with the test 39 | * data. If not specified, it defaults to the same value as `transformName`. 40 | * This will be suffixed with ".input.js" for the input file and ".output.js" 41 | * for the expected output. For example, if set to "foo", we will read the 42 | * "foo.input.js" file, pass this to the transform, and expect its output to 43 | * be equal to the contents of "foo.output.js". 44 | * - Test data should be located in a directory called __testfixtures__ 45 | * alongside the transform and __tests__ directory. 46 | */ 47 | function runTest(dirName, transformName, options, testFilePrefix) { 48 | if (!testFilePrefix) { 49 | testFilePrefix = transformName; 50 | } 51 | 52 | const fixtureDir = path.join(dirName, '..', '__testfixtures__'); 53 | const inputPath = path.join(fixtureDir, testFilePrefix + '.input.tsx'); 54 | const source = fs.readFileSync(inputPath, 'utf8'); 55 | const expectedOutput = fs.readFileSync( 56 | path.join(fixtureDir, testFilePrefix + '.output.tsx'), 57 | 'utf8' 58 | ); 59 | // Assumes transform is one level up from __tests__ directory 60 | const module = require(path.join(dirName, '..', transformName + '.ts')); 61 | runInlineTest(module, options, { 62 | path: inputPath, 63 | source 64 | }, expectedOutput); 65 | } 66 | exports.runTest = runTest; 67 | 68 | /** 69 | * Handles some boilerplate around defining a simple jest/Jasmine test for a 70 | * jscodeshift transform. 71 | */ 72 | function defineTest(dirName, transformName, options, testFilePrefix, only: boolean = false) { 73 | const testName = testFilePrefix 74 | ? `transforms correctly using "${testFilePrefix}" data` 75 | : 'transforms correctly'; 76 | describe(transformName, () => { 77 | const myIt = only ? it.only : it; 78 | 79 | myIt(testName, () => { 80 | runTest(dirName, transformName, options, testFilePrefix); 81 | }); 82 | }); 83 | } 84 | exports.defineTest = defineTest; 85 | 86 | function defineInlineTest(module, options, input, expectedOutput, testName) { 87 | it(testName || 'transforms correctly', () => { 88 | runInlineTest(module, options, { 89 | source: input 90 | }, expectedOutput); 91 | }); 92 | } 93 | exports.defineInlineTest = defineInlineTest; 94 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "es2017", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ 5 | "module": "umd", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 6 | "lib": [ /* Specify library files to be included in the compilation. */ 7 | "esnext", 8 | "dom" 9 | ], 10 | // "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | "jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 13 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./", /* Concatenate and emit output to single file. */ 17 | "outDir": "./distTs", /* Redirect output structure to the directory. */ 18 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "composite": true, /* Enable project compilation */ 20 | // "removeComments": true, /* Do not emit comments to output. */ 21 | "noEmit": true, /* Do not emit outputs. */ 22 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 23 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 24 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 25 | 26 | /* Strict Type-Checking Options */ 27 | "strict": true, /* Enable all strict type-checking options. */ 28 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 29 | // "strictNullChecks": true, /* Enable strict null checks. */ 30 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | 35 | /* Additional Checks */ 36 | "noUnusedLocals": true, /* Report errors on unused locals. */ 37 | "noUnusedParameters": true, /* Report errors on unused parameters. */ 38 | "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 39 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 40 | 41 | /* Module Resolution Options */ 42 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 43 | "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 46 | // "typeRoots": [], /* List of folders to include type definitions from. */ 47 | // "types": [], /* Type declaration files to be included in compilation. */ 48 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 49 | "resolveJsonModule": true, 50 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 51 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 52 | 53 | /* Source Map Options */ 54 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 55 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 56 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 57 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 58 | 59 | /* Experimental Options */ 60 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 61 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 62 | }, 63 | "include": [ 64 | "src/**/*", 65 | "src/__test__/**/*" 66 | ] 67 | } 68 | --------------------------------------------------------------------------------