Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Change Log

<a name="2.4.0"></a>
## [2.4.0 (unreleased)](https://github.com/lingui/js-lingui/compare/v2.3.0...v2.4.0) (TBA)

Support for Create React App.

### New Features

* New `lingui init` command which detects project type and install all required packages
* `lingui extract` detects ``create-react-app` projects and extracts messages using
``rect-app`` babel preset

<a name="2.3.0"></a>
## [2.3.0](https://github.com/lingui/js-lingui/compare/v2.2.0...v2.3.0) (2018-07-23)

Expand Down
14 changes: 14 additions & 0 deletions docs/ref/cli.rst
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,20 @@ or locally:
Commands
========

``init``
--------

.. lingui-cli:: init [--dry-run]

Installs all required packages based on project type. Recognized projects are:

- `Create React App <https://github.com/facebook/create-react-app>`_
- General React app

.. lingui-cli-option:: --dry-run

Output commands which would run, but don't execute them.

``add-locale``
--------------

Expand Down
31 changes: 24 additions & 7 deletions docs/tutorials/react.rst
Original file line number Diff line number Diff line change
Expand Up @@ -67,24 +67,41 @@ We're trying to use most of them to show the full power of jsLingui.

Let's start with the three major packages:

``@lingui/cli``
CLI for i18n management and working with message catalogs

``@lingui/react``
React components for translation and formatting

``@lingui/babel-preset-react``
Transforms messages wrapped in components from ``@lingui/react`` to ICU
MessageFormat and validates them

``@lingui/cli``
CLI for working with message catalogs

1. Install ``@lingui/babel-preset-react`` as a development dependency,
``@lingui/react`` as a runtime dependency and ``@lingui/cli`` globally:
1. ``@lingui/cli`` comes with handy :cli:`init` command, which detects the
project type and install all required packages automatically. Feel free to run
it with ``lingui init --dry-run`` option to inspect what commands will be run:

.. code-block:: shell

npm install -g @lingui/cli
npm install --save @lingui/react
npm install --save-dev @lingui/babel-preset-react
lingui init

Yarn is supported as well:

.. code-block:: shell

yarn global add @lingui/cli
lingui init

.. note::

Under the hood it installs ``@lingui/babel-preset-react`` as a development
dependency and ``@lingui/react`` as a runtime dependency (in React projects):

.. code-block:: shell

npm install --save @lingui/react
npm install --save-dev @lingui/babel-preset-react

2. Add ``@lingui/babel-preset-react`` preset to Babel config (e.g: ``.babelrc``):

Expand Down
1 change: 1 addition & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"cli-table": "^0.3.1",
"commander": "^2.16.0",
"glob": "^7.1.2",
"inquirer": "^6.1.0",
"make-plural": "^4.1.1",
"messageformat-parser": "^2.0.0",
"mkdirp": "^0.5.1",
Expand Down
78 changes: 78 additions & 0 deletions packages/cli/src/lingui-init.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// @flow
import path from "path"
import fs from "fs"
import program from "commander"
import { execSync } from "child_process"
import inquirer from "inquirer"
import chalk from "chalk"

import { projectType, detect } from "./api/detect"

function hasYarn() {
return fs.existsSync(path.resolve("yarn.lock"))
}

function makeInstall() {
const withYarn = hasYarn()

return (packageName, dev = false) =>
withYarn
? `yarn add ${dev ? "--dev " : ""}${packageName}`
: `npm install ${dev ? "--save-dev" : "--save"} ${packageName}`
}

export async function installPackages(dryRun: boolean) {
const install = makeInstall()

const type = detect()
const usesReact = type === projectType.CRA || type === projectType.REACT

const packages = [
...(usesReact
? [["@lingui/react"], ["@lingui/babel-preset-react", true]]
: [["@lingui/core"], ["@lingui/babel-preset-js", true]])
].filter(Boolean)

const verbosePackages = packages
.map(([packageName]) => chalk.yellow(packageName))
.join(", ")
const { confirm } = await inquirer.prompt({
type: "confirm",
name: "confirm",
message: `Do you want to install ${verbosePackages}?`
})

if (!confirm) return false

const commands = packages.map(([packageName, dev]) =>
install(packageName, dev)
)
if (dryRun) {
commands.forEach(command => console.log(command))
} else {
commands.forEach(command => execSync(command))
}

return true
}

type CommandInit = {|
dryRun: boolean
|}

export async function command(program: CommandInit) {
await installPackages(program.dryRun)

return true
}

if (require.main === module) {
program
.option(
"--dry-run",
"Output commands that would be run, but don't execute them."
)
.parse(process.argv)

command(program)
}
121 changes: 121 additions & 0 deletions packages/cli/src/lingui-init.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import mockFs from "mock-fs"
import { installPackages } from "./lingui-init"
import { mockConsole } from "./mocks"
import inquirer from "inquirer"

describe("lingui init", function() {
let inquirerPrompt

beforeAll(() => {
inquirerPrompt = inquirer.prompt
inquirer.prompt = jest.fn()
})

afterAll(() => {
inquirer.prompt = inquirerPrompt
})

afterEach(() => {
mockFs.restore()
inquirer.prompt.mockReset()
})

describe("install packages", function() {
beforeEach(() => {
inquirer.prompt = jest.fn(prompt =>
Promise.resolve({ [prompt.name]: true })
)
})

it("should use yarn when available", () => {
mockFs({
"yarn.lock": mockFs.file(),
"package.json": JSON.stringify({
dependencies: {}
})
})

expect.assertions(3)
return mockConsole(console =>
installPackages(true).then(result => {
expect(result).toBeTruthy()
expect(console.log).toBeCalledWith("yarn add @lingui/core")
expect(console.log).toBeCalledWith(
"yarn add --dev @lingui/babel-preset-js"
)
})
)
})

it("should use npm when yarn isn't available", () => {
mockFs({
"package.json": JSON.stringify({
dependencies: {}
})
})

expect.assertions(3)
return mockConsole(console =>
installPackages(true)
.then(result => {
expect(result).toBeTruthy()
expect(console.log).toBeCalledWith(
"npm install --save @lingui/core"
)
expect(console.log).toBeCalledWith(
"npm install --save-dev @lingui/babel-preset-js"
)
})
.catch(error => console.log({ error }))
)
})

it("should detect create-react-app project", () => {
mockFs({
"package.json": JSON.stringify({
dependencies: {
"react-scripts": "2.0.0"
}
})
})

expect.assertions(3)
return mockConsole(console =>
installPackages(true)
.then(result => {
expect(result).toBeTruthy()
expect(console.log).toBeCalledWith(
"npm install --save @lingui/react"
)
expect(console.log).toBeCalledWith(
"npm install --save-dev @lingui/babel-preset-react"
)
})
.catch(error => console.log({ error }))
)
})

it("should install core only for other projects", () => {
mockFs({
"package.json": JSON.stringify({
dependencies: {}
})
})

expect.assertions(3)
return mockConsole(console =>
installPackages(true)
.then(result => {
expect(result).toBeTruthy()
expect(console.log).toBeCalledWith(
"npm install --save @lingui/core"
)
expect(console.log).toBeCalledWith(
"npm install --save-dev @lingui/babel-preset-js"
)
})
.catch(error => console.log({ error }))
)
})
})
})
1 change: 1 addition & 0 deletions packages/cli/src/lingui.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ try {

program
.version(version)
.command("init", "Install all required packages")
.command(
"add-locale [locales...]",
"Add new locale (generate empty message catalogues for this locale)"
Expand Down
16 changes: 13 additions & 3 deletions packages/cli/src/mocks.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
export function mockConsole(testCase, mock = {}) {
function restoreConsole() {
global.console = originalConsole
}

const originalConsole = global.console

const defaults = {
Expand All @@ -11,12 +15,18 @@ export function mockConsole(testCase, mock = {}) {
...mock
}

let result
try {
testCase(global.console)
result = testCase(global.console)
} catch (e) {
global.console = originalConsole
restoreConsole()
throw e
}

global.console = originalConsole
if (result && typeof result.then === "function") {
return result.then(restoreConsole).catch(restoreConsole)
} else {
restoreConsole()
return result
}
}
Loading