Repository: lingui/js-lingui (ts)

Issue:
One default message for multiple keys
**Is your feature request related to a problem? Please describe.**
I have many forms in my application. Some of them have email field. Now if I want to specify a default message and custom key I need to use `<Trans id="auth.email">Email</Trans>` every time.

If i use `<Trans id="auth.email">Email</Trans>`, I can't use `<Trans id="auth.email" />` anymore because extract command will throw error:
```
Error: Encountered different defaults for message auth.email
src/components/auth/ForgottenPasswordForm.js:27 undefined
src/components/auth/LogInForm.js:36 Email
(...)
```

**Describe the solution you'd like**
I'd like that once default not-undefined value is specified, it overrides all `undefined` ones. This will simplify translations and minimize code repetition.



Pull request:
Don't warn about duplicate default message if it's empty
I couldn't run test on my PC so I'll use this CI for this now.

Fixes #490

Patch:
diff --git a/docs/index.rst b/docs/index.rst
index 0c8c0c28..95364cf0 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -113,6 +113,7 @@ Documentation contents
    :maxdepth: 1
    :caption: Releases
 
+   Migration from 2.x to 3.x <releases/migration-3>
    Migration from 1.x to 2.x <releases/migration-2>
    Migration from 0.x to 1.x <releases/migration-1>
 
diff --git a/docs/releases/migration-3.rst b/docs/releases/migration-3.rst
new file mode 100644
index 00000000..35363369
--- /dev/null
+++ b/docs/releases/migration-3.rst
@@ -0,0 +1,272 @@
+********************************
+Migration guide from 2.x to 3.x
+********************************
+
+Backward incompatible changes
+=============================
+
+Minimal required versions are:
+
+- Node.js: 8.x
+- React: 16.8
+- Babel: 6
+
+@lingui/react
+-------------
+
+- ``<I18n>`` render-prop component and ``withI18n`` high-order component were removed in favor of :js:func:`useLingui` hook.
+
+- In :component:`Trans`, ``defaults`` prop was renamed to ``message`` and ``description`` to ``comment``.
+
+- In :component:`Trans`, ``components`` is now an object, not an array. When using the low level API,
+  it allows to name the component placeholders:
+
+  .. code-block:: jsx
+
+     <Trans id="Read <a>the docs</a>!" components={{a: <a href="/docs" />}} />
+
+- ``NumberFormat`` and ``DateFormat`` components were removed. Use ``date`` and ``number`` formats
+  from ``@lingui/core`` package instead.
+
+Removed :component:`I18nProvider` declarative API
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+LinguiJS started as a React library. After `@lingui/core` package was introduced,
+there were two ways how to switch active locales and manage catalogs in React: either
+using :component:`I18nProvider` declarative API or using `setupI18n` imperative API.
+
+In the same spirit as ``react-apollo`` and ``react-redux``, the :component:`I18nProvider`
+is simplified and accepts ``i18n`` manager, which must be created manually:
+
+.. code-block:: diff
+
+   - import { I18nProvider } from '@lingui/react'
+   + import { setupI18n, I18nProvider } from '@lingui/react'
+     import catalogEn from './locale/en/messages.js'
+
+   + const i18n = setupI18n()
+   + i18n.load('en', catalogEn)
+   + i18n.activate('en')
+
+     function App() {
+        return (
+   -       <I18nProvider language="en" catalogs={{ en: catalogEn }}>
+   +       <I18nProvider i18n={i18n}>
+              <App />
+           </I18nProvider>
+        )
+     }
+
+@lingui/core
+------------
+
+- ``i18n.t``, ``i18n.plural``, ``i18n.select`` and ``i18n.selectOrdinal`` methods were
+  removed in favor of macros.
+- Signature of ``i18n._`` method has changed. The third parameter now accepts default
+  message in ``message`` prop, instead of ``defaults``:
+
+  .. code-block:: diff
+
+     - i18n._('Welcome / Greetings', { name: 'Joe' }, { defaults: "Hello {name}" })
+     + i18n._('Welcome / Greetings', { name: 'Joe' }, { message: "Hello {name}" })
+
+- ``i18n._`` also accepts a message descriptor as a first parameter:
+
+  .. code-block:: diff
+
+     i18n._({
+       id: string,
+       message?: string,
+       comment?: string
+     })
+
+`i18n.load` loads a catalog for a single locale
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+``i18n`` manager is the single source of truth and there's no need to keep all catalogs
+loaded outside this object. To make loading easier, `i18n.load` now accepts catalog
+for a single locale. If previous behavior was useful for you (e.g. when loading all
+message catalogs in SSR), use `i18n.loadAll` instead.
+
+.. code-block:: diff
+
+     import { setupI18n } from '@lingui/core'
+     import catalogEn from './locale/en/messages.js'
+
+     export const i18n = setupI18n()
+   - i18n.load({ en: catalogEn })
+   + i18n.load('en', catalogEn)
+
+.. note::
+
+   Use `i18n.loadAll` to load all catalogs at once:
+
+   .. code-block:: jsx
+
+      // i18n.js
+      import { setupI18n } from '@lingui/core'
+      import catalogEn from './locale/en/messages.js'
+
+      export const i18n = setupI18n()
+      i18n.loadAll({ en: catalogEn })
+
+@lingui/macro
+-------------
+
+- :jsmacro:`plural`, :jsmacro:`select` and :jsmacro:`selectOrdinal` accepts value as a first parameter:
+
+  .. code-block:: diff
+
+     - plural({ value, one: "# book", other: "# books" })
+     + plural(value, { one: "# book", other: "# books" })
+
+@lingui/cli
+-----------
+
+- command ``lingui init`` was removed
+
+Whitespace and HTML entities
+----------------------------
+
+Whitespace handling in plugins had few bugs. By fixing them, there might be few
+backward incompatible changes. It's advised to run :cli:`extract` and inspect
+changes in catalogs (if any).
+
+1. Don't keep spaces before ``{variables}`` in JSX. This is how React handles whitespaces
+   in JSX. Leading whitespace is always removed:
+
+   .. code-block:: jsx
+
+      <Trans>
+         &quot;
+         {variable}
+         &quot;
+      </Trans>
+
+      // Becomes: &quot;{variable}&quot;
+
+2. Keep forced newlines. Sometimes it's useful to keep newlines in JSX. If that's your
+   case, you need to force it in the same was as spaces are forced before variables
+   or elements:
+
+   .. code-block:: jsx
+
+      <Trans>
+         1. Item{"\n"}
+         2. Item
+      </Trans>
+
+      // Becomes: 1. Item\n2. Item
+
+3. Keep HTML entities. HTML entities are replaced with characters in Babel. They are now
+   kept in message catalogs and replaced only when rendered:
+
+   .. code-block:: jsx
+
+      <Trans>&quot;Hello&quot;</Trans>
+
+      // Becomes: &quot;Hello&quot;
+
+Plugins/Presets
+---------------
+
+Plugins are replaced with macros. Presets are removed completely because they aren't
+needed anymore.
+
+1. Uninstall plugins/presets, remove them from Babel config and replace them with
+   ``macros``:
+
+   .. code-block:: shell
+
+      npm uninstall @lingui/babel-preset-react
+      npm install --dev @lingui/macro babel-plugin-macros
+
+   .. code-block:: diff
+
+      {
+         "presets": [
+      -      "@lingui/babel-preset-react"
+         ],
+         "plugins": [
+      +      "macros",
+         ]
+      }
+
+2. Import :jsxmacro:`Trans`, :jsxmacro:`Plural`, :jsxmacro:`Select` and
+   :jsxmacro:`SelectOrdinal` from ``@lingui/macro``:
+
+   .. code-block:: diff
+
+      - import { Trans } from "@lingui/react"
+      + import { Trans } from "@lingui/macro"
+
+   .. note::
+
+      If you used :component:`Trans` component without children, then keep the import
+      from ``@lingui/react``:
+
+      .. code-block:: jsx
+
+         import { Trans } from "@lingui/react"
+
+         const CustomID = () => <Trans id="msg.id" />
+         const DynamicID = () => <Trans id={msgId} />
+
+3. :js:meth:`i18n.t`, :js:meth:`i18n.plural`, :js:meth:`i18n.select` and
+   :js:meth:`i18n.selectOrdinal` methods are removed and replaced with macros.
+
+   Message descriptor created by macro must be passed to :js:meth:`i18n._` method:
+
+   .. code-block:: diff
+
+        import { setupI18n } from "@lingui/core"
+      + import { t } from "@lingui/macro"
+
+        const i18n = setupI18n()
+
+      - i18n.t`Hello World`
+      + i18n._(t`Hello World`)
+
+
+
+New features
+============
+
+`i18n.loadAll`
+--------------
+
+`i18n.loadAll` method was formerly named `i18n.load`. It loads all available catalogs
+at once. Useful in Node.js environments when we don't need to load catalogs one by one.
+
+.. code-block:: jsx
+
+   import { setupI18n } from "@lingui/core"
+
+   export const i18n = setupI18n()
+   i18n.loadAll({
+     en: require("./locale/en/messages"),
+     cs: require("./locale/cs/messages")
+   })
+
+`i18n.willActivate`
+-------------------
+
+`willActivate(locale: string)` event is called when locale change is requested using
+`i18n.activate`. It may return a promise. In that case, locale is activated after the
+promise is resolved.
+
+Here's an example with dynamic import in webpack:
+
+.. code-block:: jsx
+
+   const i18n = setupI18n()
+   i18n.willActivate(locale => {
+     /* webpackMode: "lazy", webpackChunkName: "i18n-[index]" */
+     return import(`@lingui/loader!./locales/${locale}/messages.po`)
+   })
+
+`i18n.didActivate`
+------------------
+
+`didActivate` is called after the locale is activated.
+
diff --git a/docs/rfc/001_macros_message_descriptors.rst b/docs/rfc/001_macros_message_descriptors.rst
index 1e7bb869..c166bb4a 100644
--- a/docs/rfc/001_macros_message_descriptors.rst
+++ b/docs/rfc/001_macros_message_descriptors.rst
@@ -20,15 +20,15 @@ writing format by hand are following:
      const today = new Date()
 
      // With macro
-     t`Hello ${name}, today is ${date(today)}`
+     i18n._(t`Hello ${name}, today is ${date(today)}`)
 
      // Without macro
-     i18n('Hello {name}, today is {today, date}', { name, date })
+     i18n._('Hello {name}, today is {today, date}', { name, today })
 
      // For the sake of completeness, macro is transformed into this code:
-     // i18n({
+     // i18n._({
      //   id: 'Hello {name}, today is {today, date}',
-     //   values: { name, date }
+     //   values: { name, today }
      //  })
 
 - Messages are validated and type-checked. Generated message is always syntactically
@@ -42,14 +42,13 @@ writing format by hand are following:
      const value = 42
 
      // With macro
-     t`Hello ${name}, you have ${plural({
-       value,
+     i18n._(t`Hello ${name}, you have ${plural(value, {
        one: '# unread message',
        other: '# unread messages'
-     })}`
+     })}`)
 
      // Without macro
-     i18n(
+     i18n._(
        'Hello {name}, you have {value, plural, one {# unread message} other {# unread messages}}',
        { name, date }
      )
@@ -65,18 +64,18 @@ writing format by hand are following:
      const name = "Joe"
      const today = new Date()
 
-     t`Hello ${name}, today is ${date(today)}`
+     i18n._(t`Hello ${name}, today is ${date(today)}`)
 
      // Lingui configration { messageFormat: 'icu' }
      // ↓ ↓ ↓ ↓ ↓ ↓
-     // i18n({
+     // i18n._({
      //   id: 'Hello {name}, today is {today, date}',
      //   values: { name, date }
      //  })
 
      // Lingui configration { messageFormat: 'fluent' }
      // ↓ ↓ ↓ ↓ ↓ ↓
-     // i18n({
+     // i18n._({
      //   id: 'Hello { $name }, today is { DATETIME($today) }',
      //   values: { name, date }
      //  })
@@ -89,11 +88,6 @@ writing format by hand are following:
 Message definitions
 ===================
 
-``t``, ``plural``, ``select`` and ``selectOrdinal`` macros are alternatives
-for ``Trans``, ``Plural``, ``Select`` and ``SelectOrdinal`` macros used in JSX.
-The former can be used anywhere where string output is required, while
-the latter are used in JSX context.
-
 Tagged template literals
 ------------------------
 
@@ -116,28 +110,25 @@ plural forms (``plural``, ``selectOrdinal``) or categories (``select``):
 
    import { plural, select } from '@lingui/macro'
 
-   plural({
-      value,
+   plural(value, {
       one: "# book",
       other: "# books"
    })
 
-   select({
-      value,
+   select(value, {
       male: "he",
       female: "she",
       other: "they"
    })
 
-It's possible to arbitrary nest macros. ``t`` macro isn't required
+It's possible to arbitrary nest formatters. ``t`` macro isn't required
 for nested template literals:
 
 .. code-block:: jsx
 
-   import { plural } from '@lingui/macro'
+   import { t, plural } from '@lingui/macro'
 
-   plural({
-      value,
+   plural(value, {
       one: `${name} has # book`,
       other: `${name} has # books`
    })
@@ -150,7 +141,7 @@ First argument is value to be formatted, the second is optional format:
 
 .. code-block:: jsx
 
-   import { date, number } from `@lingui/macro`
+   import { t, date, number } from `@lingui/macro`
 
    // default format
    t`Today is ${date(today)}`
@@ -161,92 +152,42 @@ First argument is value to be formatted, the second is optional format:
 Custom ID and comments for translators
 --------------------------------------
 
-If ``t`` macro is used as a function, then it's called with a message descriptor.
-It's possible to override message ID or add comments for translators by adding
-``id`` or ``comment`` respectively to this object.
+All macros above can be wrapped inside ``defineMessage`` macro
+to provide ``comment`` for translators or to override the message ``id``:
 
 .. code-block:: jsx
 
-   import { t } from '@lingui/macro'
+   import { defineMessage } from '@lingui/macro'
 
    // Message is used as an ID
-   t({
-      message: `Default message`,
+   defineMessage({
+      message: "Default message",
       comment: "Comment for translators"
    })
 
    // Custom ID
-   t({
-      id: "msg.id",
-      message: `Default message`,
-      comment: "Comment for translators"
-   })
-
-   // Custom ID, without default message
-   t({
+   defineMessage({
       id: "msg.id",
-      comment: "Comment for translators"
-   })
-
-``plural`` and other formatters are already called with object as a first parameter.
-``id`` and ``comment`` props can be added there:
-
-.. code-block:: jsx
-
-   import { plural } from '@lingui/macro'
-
-   plural({
-      id: "msg.plural",
       comment: "Comment for translators",
-
-      value,
-      one: "# book",
-      other: "# books"
-   })
-
-``date`` and ``number`` don't accept ``id`` nor ``comment``.
-
-Transformation
-==============
-
-Each macro is transformed into a message descriptor
-wrapped into ``i18n._`` function:
-
-.. code-block:: jsx
-
-   import { t } from '@lingui/macro'
-
-   t({
-      id: "msg.id",
-      message: `Default message`
-      comment: "Comment for translators"
-   })
-
-   // ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
-
-   import i18n from '@lingui/core'
-
-   i18n._({
-      id: "msg.id",
-      message: `Default message`,
-      comment: "Comment for translators"
+      message: "Default message"
    })
 
 Lazy translations
 -----------------
 
 Lazy translations are useful when we need to define a message, but translate it later.
-This was previously achieved using ``i18Mark``. Now we can use `.lazy` variant of macros:
+This was previously achieved using ``i18Mark``. Now we can use the same macros:
 
 .. code-block:: jsx
 
+   // The API of `t` and `lazy` are the same.
    import { t } from `@lingui/macro`
 
    // define the message
-   const msg = t.lazy`Default message`
+   const msg = t`Default message`
 
    // translate it
-   const translation = msg()
+   const msg = i18n._(msg)`
 
 Lazy translations are usually defined in different scope than evaluated. Parameters
 are therefore unknown, but we still need to know their names, so we can create placeholders
@@ -254,15 +195,35 @@ in MessageFormat. ``arg`` macro is used exactly for that:
 
 .. code-block:: jsx
 
-   import { plural, arg } from "@lingui/macro"
+   import { plural, arg } from `@lingui/macro`
 
-   const books = plural.lazy({
-      value: arg('count'),
+   // Macro
+   const books = plural(arg('count'), {
       one: '# book',
       other: '# books'
    })
 
-   const translation = books({ count: 42 })
+   i18n._(books, { count: 42 })
+
+Extracting messages
+===================
+
+Messages are extracted from code already transformed by macros. This makes macros
+completely optional and extraction will work also with message descriptors created
+manually.
+
+Extract script will look for  a ``i18n`` comments, which are automatically added by macros:
+
+.. code-block:: js
+
+   t`Message`
+
+   // ↓ ↓ ↓ ↓ ↓ ↓
+   /*i18n*/{
+     id: 'Message'
+   }
+
+An object after such comment is considered as message descriptor and extracted.
 
 Summary
 =======
@@ -279,13 +240,13 @@ Feature request from #258:
 
 .. code-block:: jsx
 
-   import { lazy, arg } from `@lingui/macro`
+   import { defineMessages, t, arg } from `@lingui/macro`
 
-   export default {
-      yes: lazy`Yes`,
-      no: lazy`No`,
-      cancel: lazy`Cancel`,
-      confirmDelete: lazy`Do you really want to delete ${arg("filename")}?`
-   }
+   export default defineMessages({
+      yes: "Yes",
+      no: "No",
+      cancel: "Cancel",
+      confirmDelete: t`Do you really want to delete ${arg("filename")}?`
+   })
 
 .. _Fluent: https://projectfluent.org/
diff --git a/package.json b/package.json
index bf4085b8..3643ebbd 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "private": true,
-  "version": "2.8.0",
+  "version": "2.8.1",
   "author": {
     "name": "Tomáš Ehrlich",
     "email": "tomas.ehrlich@gmail.com"
diff --git a/packages/cli/src/api/extract.js b/packages/cli/src/api/extract.js
index 1ba83568..c87ede91 100644
--- a/packages/cli/src/api/extract.js
+++ b/packages/cli/src/api/extract.js
@@ -22,7 +22,7 @@ type ExtractOptions = {
  * should be the same (raise an error if defaults are different).
  */
 function mergeMessage(msgId, prev, next) {
-  if (prev.defaults !== next.defaults) {
+  if (prev.defaults && next.defaults && prev.defaults !== next.defaults) {
     throw new Error(
       `Encountered different defaults for message ${chalk.yellow(msgId)}` +
         `\n${chalk.yellow(prettyOrigin(prev.origin))} ${prev.defaults}` +
@@ -32,6 +32,7 @@ function mergeMessage(msgId, prev, next) {
 
   return {
     ...next,
+    defaults: prev.defaults || next.defaults,
     origin: R.concat(prev.origin, next.origin)
   }
 }


Test patch:
diff --git a/packages/cli/src/api/__snapshots__/extract.test.js.snap b/packages/cli/src/api/__snapshots__/extract.test.js.snap
index d42edaec..0fdbf9a1 100644
--- a/packages/cli/src/api/__snapshots__/extract.test.js.snap
+++ b/packages/cli/src/api/__snapshots__/extract.test.js.snap
@@ -57,6 +57,24 @@ Object {
 }
 `;
 
+exports[`collect should use defined default 1`] = `
+Object {
+  msg.id: Object {
+    defaults: Second default,
+    origin: Array [
+      Array [
+        onlyOneDefault/First.js,
+        2,
+      ],
+      Array [
+        onlyOneDefault/Second.js,
+        5,
+      ],
+    ],
+  },
+}
+`;
+
 exports[`order should order messages alphabetically 1`] = `
 Object {
   en: Object {
diff --git a/packages/cli/src/api/extract.test.js b/packages/cli/src/api/extract.test.js
index 25a691ac..0076d82d 100644
--- a/packages/cli/src/api/extract.test.js
+++ b/packages/cli/src/api/extract.test.js
@@ -135,6 +135,21 @@ describe("collect", function() {
             origin: [["diffDefaults/Second.js", 5]]
           }
         })
+      },
+
+      // test case for when only one defaults message is specified
+      onlyOneDefault: {
+        "First.js.json": JSON.stringify({
+          "msg.id": {
+            origin: [["onlyOneDefault/First.js", 2]]
+          }
+        }),
+        "Second.js.json": JSON.stringify({
+          "msg.id": {
+            defaults: "Second default",
+            origin: [["onlyOneDefault/Second.js", 5]]
+          }
+        })
       }
     })
   })
@@ -162,6 +177,13 @@ describe("collect", function() {
       }).toThrowErrorMatchingSnapshot()
     }
   })
+
+  it("should use defined default", function() {
+    const { collect } = require("./extract")
+    const catalog = collect("onlyOneDefault")
+    mockFs.restore()
+    expect(catalog).toMatchSnapshot()
+  })
 })
 
 describe("cleanObsolete", function() {