Skip to content

Commit

Permalink
feat: Use hook for i18n (#1306)
Browse files Browse the repository at this point in the history
feat: Use hook for i18n
  • Loading branch information
ptbrowne authored Jan 7, 2020
2 parents 7d38efc + 15fd3be commit f0f7f99
Show file tree
Hide file tree
Showing 14 changed files with 574 additions and 73 deletions.
94 changes: 94 additions & 0 deletions codemods/remove-import-if-unused.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
const mkRemoveIfUnused = j => root => {
const removeIfUnused = (importSpecifier, importDeclaration) => {
if (!importSpecifier.value.local) {
return
}
const varName = importSpecifier.value.local.name
if (varName === 'React') {
return false
}

const isUsedInScopes = () => {
return (
j(importDeclaration)
.closestScope()
.find(j.Identifier, { name: varName })
.filter(p => {
if (p.value.start === importSpecifier.value.local.start)
return false
if (p.parentPath.value.type === 'Property' && p.name === 'key')
return false
if (p.name === 'property') return false
return true
})
.size() > 0
)
}

// Caveat, this doesn't work with annonymously exported class declarations.
const isUsedInDecorators = () => {
// one could probably cache these, but I'm lazy.
let used = false
root.find(j.ClassDeclaration).forEach(klass => {
used =
used ||
(klass.node.decorators &&
j(klass.node.decorators)
.find(j.Identifier, { name: varName })
.filter(p => {
if (p.parentPath.value.type === 'Property' && p.name === 'key')
return false
if (p.name === 'property') return false
return true
})
.size() > 0)
})
return used
}

if (!(isUsedInScopes() || isUsedInDecorators())) {
j(importSpecifier).remove()
return true
}
return false
}

const removeUnusedDefaultImport = importDeclaration => {
return (
j(importDeclaration)
.find(j.ImportDefaultSpecifier)
.filter(s => removeIfUnused(s, importDeclaration))
.size() > 0
)
}

const removeUnusedNonDefaultImports = importDeclaration => {
return (
j(importDeclaration)
.find(j.ImportSpecifier)
.filter(s => removeIfUnused(s, importDeclaration))
.size() > 0
)
}

// Return True if somethin was transformed.
const processImportDeclaration = importDeclaration => {
// e.g. import 'styles.css'; // please Don't Touch these imports!
if (importDeclaration.value.specifiers.length === 0) return false

const hadUnusedDefaultImport = removeUnusedDefaultImport(importDeclaration)
const hadUnusedNonDefaultImports = removeUnusedNonDefaultImports(
importDeclaration
)

if (importDeclaration.value.specifiers.length === 0) {
j(importDeclaration).remove()
return true
}
return hadUnusedDefaultImport || hadUnusedNonDefaultImports
}

root.find(j.ImportDeclaration).forEach(processImportDeclaration)
}

module.exports = mkRemoveIfUnused
33 changes: 33 additions & 0 deletions codemods/use-i18n-hook/example-after.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from 'react'
import { Text, useI18n } from 'cozy-ui/transpiled/react'

const ComponentUsingT = () => {
const { t } = useI18n()

return (
<Padded>
<Title>{t('Title1')}</Title>
<Text>{t('Text1')}</Text>
</Padded>
)
}

const DumbComponentUsingTAndF = () => {
const { t,f } = useI18n()

return (
<Padded>
<Title>{t('Title2')}</Title>
<Text>{f(new Date())}</Text>
</Padded>
)
}

const EnhancedComponentUsingTAndF = DumbComponentUsingTAndF

const DumbDefaultSimpleComponent = () => {
const { t } = useI18n()
return <div>t('Hello')</div>
}

export default compose(hoc1, hoc2)(DumbDefaultSimpleComponent)
28 changes: 28 additions & 0 deletions codemods/use-i18n-hook/example-before.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React from 'react'
import { translate, Text, Modal } from 'cozy-ui/transpiled/react'

import OtherImports from 'other/imports'

const ComponentUsingT = translate()(({ t }) => (
<Padded>
<Title>{t('Title1')}</Title>
<Text>{t('Text1')}</Text>
</Padded>
))

const DumbComponentUsingTAndF = ({ t, f }) => (
<Padded>
<Title>{t('Title2')}</Title>
<Text>{f(new Date())}</Text>
</Padded>
)

const EnhancedComponentUsingTAndF = translate()(DumbComponentUsingTAndF)

const DumbDefaultSimpleComponent = ({ t }) => <div>t('Hello')</div>

export default compose(
hoc1,
hoc2,
translate()
)(DumbDefaultSimpleComponent)
157 changes: 157 additions & 0 deletions codemods/use-i18n-hook/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import makeUtils from '../utils'

const prepend = (arr, item) => {
arr.splice(0, 0, item)
}

const isI18nProp = prop => {
return prop.key && (prop.key.name === 't' || prop.key.name === 'f')
}

const findI18nProps = objPattern => {
if (!objPattern) {
return
}
if (objPattern.type !== 'ObjectPattern') {
return
}
return objPattern.properties
? objPattern.properties.filter(isI18nProp).map(prop => prop.key.name)
: []
}

const findNearest = (path, condition) => {
while (path && !condition(path) && path.parentPath) {
path = path.parentPath
}
return path
}

const findPropObjectPattern = (j, functionBodyPath) => {
const functionBody = functionBodyPath.node
const propsArg = functionBody.params[0]
const propObjPattern =
propsArg && propsArg.type === 'ObjectPattern' ? propsArg : null
if (propObjPattern) {
return { objPattern: propObjPattern, from: 'params' }
}

const bodyPropsDeclarators = j(functionBodyPath).find(j.VariableDeclarator, {
init: {
name: 'props'
}
})
const bodyPropsDeclarator =
bodyPropsDeclarators.length > 0 ? bodyPropsDeclarators.get(0) : 0

if (
bodyPropsDeclarator &&
bodyPropsDeclarator.node.id.type === 'ObjectPattern'
) {
return {
objPattern: bodyPropsDeclarator.node.id,
from: 'body',
declarator: bodyPropsDeclarator
}
}
}

export default function transformer(file, api) {
const j = api.jscodeshift
const utils = makeUtils(j)
const root = j(file.source)

const replaceI18nPropsByHook = arrowFunctionBodyPath => {
const arrowFunctionBody = arrowFunctionBodyPath.node
const objPattern = findPropObjectPattern(j, arrowFunctionBodyPath)
if (!objPattern) {
return
}

const {
objPattern: propObjPattern,
from: objPatternOrigin,
declarator: objPatternDeclarator
} = objPattern

const i18nProps = findI18nProps(propObjPattern)

if (!i18nProps || !i18nProps.length) {
return
}

if (!arrowFunctionBody.body.body || !arrowFunctionBody.body.body.splice) {
arrowFunctionBody.body = j.blockStatement([
j.returnStatement(arrowFunctionBody.body)
])
}

const updatedProperties = propObjPattern.properties.filter(
prop => !isI18nProp(prop)
)

if (
updatedProperties.length === 0 &&
arrowFunctionBody.params.length === 1 &&
objPatternOrigin === 'params'
) {
arrowFunctionBody.params = []
} else if (objPatternOrigin === 'params') {
arrowFunctionBody.params[0].properties = updatedProperties
} else if (updatedProperties.length > 0 && objPatternOrigin === 'body') {
objPatternDeclarator.node.id.properties = updatedProperties
} else if (updatedProperties === 0 && objPatternOrigin === 'body') {
objPatternDeclarator.prune()
}

prepend(arrowFunctionBody.body.body, `const { ${i18nProps} } = useI18n()`)
utils.hoc.removeHOC(arrowFunctionBodyPath, 'translate')

const declarator = findNearest(
arrowFunctionBodyPath,
x => x.node.type === 'VariableDeclarator'
)
const ComponentName = declarator.node.id.name
j(declarator)
.closestScope()
.find(j.Identifier, {
name: ComponentName
})
.forEach(path => {
utils.hoc.removeHOC(path, 'translate')
})

utils.hoc.removeDefaultExportHOC(root, ComponentName, 'translate')
return true
}

const components = root.find(j.ArrowFunctionExpression)

let needToAddUseI18nImport = false
components.forEach(path => {
if (replaceI18nPropsByHook(path)) {
needToAddUseI18nImport = true
}
})

if (needToAddUseI18nImport) {
utils.imports.add(
root,
{
useI18n: true
},
x => {
return (
x.source.value == 'cozy-ui/transpiled/react' ||
x.source.value == 'cozy-ui/react'
)
},
'cozy-ui/transpiled/react'
)
utils.simplifyCompose(root)
utils.imports.removeUnused(root)
return root.toSource()
}

return null
}
Loading

0 comments on commit f0f7f99

Please sign in to comment.