Repository: lingui/js-lingui
Pull request #501 (2019-05-16 18:19:31)

Optional --config argument for advanced configuration scenarios
## Overview

I have an advanced configuration scenario that requires multiple locale targets and with multiple source directories. I want to end up with multiple catalogs that are loaded in different circumstances.

Presently, I work around the obvious config issue by swapping out my .linguirc file in between runs of `lingui extract` and `lingui compile`. This isn't ideal and I thought lingui might benefit from a CLI  option to specify a configuration file.

This change allows a `--config <path>` option on add-locale, compile, and extract commands. If the path exists, I use `cosmiconfig#loadSync` instead of the normal config file probe.

If you find this change welcome, I can work more to add details to the docs.

## Tradeoffs

- Adding a new option could make each command to be perceived as slightly more complex


codecov[bot]: # [Codecov](https://codecov.io/gh/lingui/js-lingui/pull/501?src=pr&el=h1) Report
> :exclamation: No coverage uploaded for pull request base (`master@c91de56`). [Click here to learn what that means](https://docs.codecov.io/docs/error-reference#section-missing-base-commit).
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/lingui/js-lingui/pull/501/graphs/tree.svg?width=650&token=CuJ1VD5BY5&height=150&src=pr)](https://codecov.io/gh/lingui/js-lingui/pull/501?src=pr&el=tree)

```diff
@@            Coverage Diff            @@
##             master     #501   +/-   ##
=========================================
  Coverage          ?   91.13%           
=========================================
  Files             ?       51           
  Lines             ?     1421           
  Branches          ?        0           
=========================================
  Hits              ?     1295           
  Misses            ?      126           
  Partials          ?        0
```


| [Impacted Files](https://codecov.io/gh/lingui/js-lingui/pull/501?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [packages/cli/src/lingui-extract.js](https://codecov.io/gh/lingui/js-lingui/pull/501/diff?src=pr&el=tree#diff-cGFja2FnZXMvY2xpL3NyYy9saW5ndWktZXh0cmFjdC5qcw==) | `69.23% <ø> (ø)` | |
| [packages/conf/src/index.js](https://codecov.io/gh/lingui/js-lingui/pull/501/diff?src=pr&el=tree#diff-cGFja2FnZXMvY29uZi9zcmMvaW5kZXguanM=) | `97.14% <100%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/lingui/js-lingui/pull/501?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/lingui/js-lingui/pull/501?src=pr&el=footer). Last update [c91de56...451f3a6](https://codecov.io/gh/lingui/js-lingui/pull/501?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

codecov[bot]: # [Codecov](https://codecov.io/gh/lingui/js-lingui/pull/501?src=pr&el=h1) Report
> :exclamation: No coverage uploaded for pull request base (`master@c91de56`). [Click here to learn what that means](https://docs.codecov.io/docs/error-reference#section-missing-base-commit).
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/lingui/js-lingui/pull/501/graphs/tree.svg?width=650&token=CuJ1VD5BY5&height=150&src=pr)](https://codecov.io/gh/lingui/js-lingui/pull/501?src=pr&el=tree)

```diff
@@            Coverage Diff            @@
##             master     #501   +/-   ##
=========================================
  Coverage          ?   91.13%           
=========================================
  Files             ?       51           
  Lines             ?     1421           
  Branches          ?        0           
=========================================
  Hits              ?     1295           
  Misses            ?      126           
  Partials          ?        0
```


| [Impacted Files](https://codecov.io/gh/lingui/js-lingui/pull/501?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [packages/cli/src/lingui-extract.js](https://codecov.io/gh/lingui/js-lingui/pull/501/diff?src=pr&el=tree#diff-cGFja2FnZXMvY2xpL3NyYy9saW5ndWktZXh0cmFjdC5qcw==) | `69.23% <ø> (ø)` | |
| [packages/conf/src/index.js](https://codecov.io/gh/lingui/js-lingui/pull/501/diff?src=pr&el=tree#diff-cGFja2FnZXMvY29uZi9zcmMvaW5kZXguanM=) | `97.14% <100%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/lingui/js-lingui/pull/501?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/lingui/js-lingui/pull/501?src=pr&el=footer). Last update [c91de56...451f3a6](https://codecov.io/gh/lingui/js-lingui/pull/501?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

tricoder42: Hey @brandonc, thank you! This is handy! I've added one paragraph to docs and it's ready for release.

Could you please take a look at [proposal](https://lingui.js.org/rfc/002_catalogs.html) for new configuration for catalog? I'm gonna finish the PR once I merge #499 and it would be great to get a feedback from you, because you use multiple source directories and multiple locale targets. 

Patch:
diff --git a/docs/ref/cli.rst b/docs/ref/cli.rst
index e52498d7..c83373ea 100644
--- a/docs/ref/cli.rst
+++ b/docs/ref/cli.rst
@@ -44,6 +44,14 @@ Install
          }
       }
 
+Global options
+==============
+
+.. lingui-cli-option:: --config <config>
+
+Path to LinguiJS configuration file. If not set, the default file
+is loaded as described in :doc:`LinguiJS configuration </ref/conf>` reference.
+
 Commands
 ========
 
diff --git a/packages/cli/src/lingui-add-locale.js b/packages/cli/src/lingui-add-locale.js
index bcffa218..3be4428a 100644
--- a/packages/cli/src/lingui-add-locale.js
+++ b/packages/cli/src/lingui-add-locale.js
@@ -41,6 +41,7 @@ if (require.main === module) {
         "directory from your localeDir (e.g. ./locale/en)"
     )
     .arguments("<locale...>")
+    .option("--config <path>", "Path to the config file")
     .option("--format <format>", "Format of message catalog")
     .on("--help", function() {
       console.log("\n  Examples:\n")
diff --git a/packages/cli/src/lingui-compile.js b/packages/cli/src/lingui-compile.js
index 1b4f2979..8ae5fe23 100644
--- a/packages/cli/src/lingui-compile.js
+++ b/packages/cli/src/lingui-compile.js
@@ -117,6 +117,7 @@ if (require.main === module) {
     .description(
       "Add compile message catalogs and add language data (plurals) to compiled bundle."
     )
+    .option("--config <path>", "Path to the config file")
     .option("--strict", "Disable defaults for missing translations")
     .option("--verbose", "Verbose output")
     .option("--format <format>", "Format of message catalog")
diff --git a/packages/cli/src/lingui-extract.js b/packages/cli/src/lingui-extract.js
index 371d9156..73907b50 100644
--- a/packages/cli/src/lingui-extract.js
+++ b/packages/cli/src/lingui-extract.js
@@ -134,6 +134,7 @@ export default function command(
 
 if (require.main === module) {
   program
+    .option("--config <path>", "Path to the config file")
     .option("--overwrite", "Overwrite translations for source locale")
     .option("--clean", "Remove obsolete translations")
     .option(
diff --git a/packages/conf/src/index.js b/packages/conf/src/index.js
index a1175403..6a688861 100644
--- a/packages/conf/src/index.js
+++ b/packages/conf/src/index.js
@@ -1,4 +1,5 @@
 const path = require("path")
+const fs = require("fs")
 const chalk = require("chalk")
 const cosmiconfig = require("cosmiconfig")
 const { validate } = require("jest-validate")
@@ -74,10 +75,30 @@ const configValidation = {
   comment: "See https://lingui.js.org/ref/conf.html for a list of valid options"
 }
 
+function configFilePathFromArgs() {
+  const configIndex = process.argv.indexOf("--config")
+
+  if (
+    configIndex >= 0 &&
+    process.argv.length > configIndex &&
+    fs.existsSync(process.argv[configIndex + 1])
+  ) {
+    return process.argv[configIndex + 1]
+  }
+
+  return null
+}
+
 export function getConfig({ cwd } = {}) {
-  const defaultRootDir = cwd || process.cwd()
   const configExplorer = cosmiconfig("lingui")
-  const result = configExplorer.searchSync(defaultRootDir)
+  const defaultRootDir = cwd || process.cwd()
+  const configPath = configFilePathFromArgs()
+
+  const result =
+    configPath == null
+      ? configExplorer.searchSync(defaultRootDir)
+      : configExplorer.loadSync(configPath)
+
   const raw = { ...defaultConfig, ...(result ? result.config : {}) }
 
   validate(raw, configValidation)


Test patch:
diff --git a/packages/cli/src/api/pseudoLocalize.test.js b/packages/cli/src/api/pseudoLocalize.test.js
index eef9b0e8..e354e919 100644
--- a/packages/cli/src/api/pseudoLocalize.test.js
+++ b/packages/cli/src/api/pseudoLocalize.test.js
@@ -35,8 +35,12 @@ describe("PseudoLocalization", () => {
       pseudoLocalize("{value, plural, one {# book} other {# books}}")
     ).toEqual("{value, plural, one {# ƀōōķ} other {# ƀōōķś}}")
     expect(
-      pseudoLocalize("{count, plural, one {{countString} book} other {{countString} books}}")
-    ).toEqual("{count, plural, one {{countString} ƀōōķ} other {{countString} ƀōōķś}}")
+      pseudoLocalize(
+        "{count, plural, one {{countString} book} other {{countString} books}}"
+      )
+    ).toEqual(
+      "{count, plural, one {{countString} ƀōōķ} other {{countString} ƀōōķś}}"
+    )
   })
 
   it("shouldn't pseudolocalize variables", () => {
diff --git a/packages/conf/src/index.test.js b/packages/conf/src/index.test.js
index eda7a647..8e822139 100644
--- a/packages/conf/src/index.test.js
+++ b/packages/conf/src/index.test.js
@@ -1,4 +1,29 @@
 import { getConfig, replaceRootDir } from "@lingui/conf"
+import cosmiconfig from "cosmiconfig"
+
+const mockExplorer = {
+  searchSync: jest.fn(),
+  loadSync: jest.fn()
+}
+
+jest.mock("cosmiconfig", function() {
+  return function() {
+    return mockExplorer
+  }
+})
+
+jest.mock("fs", function() {
+  return {
+    existsSync: function() {
+      return true
+    }
+  }
+})
+
+beforeEach(function() {
+  cosmiconfig().loadSync.mockClear()
+  cosmiconfig().searchSync.mockClear()
+})
 
 describe("lingui-conf", function() {
   it("should return default config", function() {
@@ -27,4 +52,26 @@ describe("lingui-conf", function() {
     expect(config.localeDir).toEqual("/Root")
     expect(config.srcPathDirs).toEqual(["/Root", "rootDir"])
   })
+
+  it("searches for a config file", function() {
+    getConfig()
+    expect(cosmiconfig().searchSync).toHaveBeenCalled()
+  })
+
+  describe("with --config command line argument", function() {
+    beforeEach(function() {
+      process.argv.push("--config")
+      process.argv.push("./lingui/myconfig")
+    })
+
+    afterEach(function() {
+      process.argv.splice(process.argv.length - 2, 2)
+    })
+
+    it("allows specific config file to be loaded", function() {
+      getConfig()
+      expect(cosmiconfig().searchSync).not.toHaveBeenCalled()
+      expect(cosmiconfig().loadSync).toHaveBeenCalledWith("./lingui/myconfig")
+    })
+  })
 })