Repository: lingui/js-lingui (ts)

Issue:
React "two children with same key" warning from `<Trans>` macro
**Describe the bug**
When using the `<Trans>` macro, if the language string contains two instances of the same variable, _and_ that variable's value is a JSX element, Lingui generates two sibling elements with the same `key` prop. This causes React to complain, because the `key` prop is required to be unique for sibling elements.

**To Reproduce**
Here is a Storybook story that reproduces the issue:

```
import React from 'react';
import { Trans } from '@lingui/macro';
import { storiesOf } from '@storybook/react';
import { I18nProvider } from '@lingui/react';

const foo = <span>bar</span>;

storiesOf('lingui', module).add('two children with the same key', () => (
  <I18nProvider language="en">
    <Trans>
      {foo} {foo}
    </Trans>
  </I18nProvider>
));
```

**Expected behavior**
The story displays "bar bar" when viewed in the browser, with no warnings or errors in the JS console.

**Actual behavior**
The story displays correctly in the browser, but it generates React elements with the same key of `0`, and React prints a warning about that to the JS console:

```
Warning: Encountered two children with the same key, `0`. Keys
should be unique so that components maintain their identity across
updates. Non-unique keys may cause children to be duplicated
and/or omitted — the behavior is unsupported and could change in a
future version.
    in Render (created by Trans)
    in Trans (created by I18n)
    in I18n (created by WithI18n)
    in WithI18n (at lingui.stories.tsx:10)
    in I18nProvider (at lingui.stories.tsx:9)
    in storyFn
    in ErrorBoundary
```
When the rendered components are viewed using the React Dev Tools browser plugin, I can see this hierarchy of components:

* `WithI18n`
    * `I18n`
        * `Trans`
            * `Render`
                * `span key="0"`
                * `span key="0"`

**Additional context**
- jsLingui version 2.9.2
- Babel version 7.9.0
- Create React App (react-scripts 3.4.3)


Pull request:
fix: fix children with same key, uniqueId
Fixes #757 

It's using a simpe lodash uniqueId function, just a const to keep the past id's 👍🏻 
I've introduced a little test to avoid a regression to this

Patch:
diff --git a/packages/core/src/context.ts b/packages/core/src/context.ts
index 2e747a96..2f31ebc1 100644
--- a/packages/core/src/context.ts
+++ b/packages/core/src/context.ts
@@ -1,4 +1,4 @@
-import { Locales } from "./i18n"
+import { CompiledMessage, Locales } from "./i18n"
 import { date, number } from "./formats"
 import { isString, isFunction } from "./essentials"
 
@@ -69,7 +69,7 @@ function context({ locale, locales, values, formats, localeData }) {
 }
 
 export function interpolate(
-  translation: string | Array<string | [string, string?, Object?]>,
+  translation: CompiledMessage,
   locale: string,
   locales: Locales,
   localeData: Object
diff --git a/packages/core/src/dev/compile.ts b/packages/core/src/dev/compile.ts
index d8f822e7..93d045a4 100644
--- a/packages/core/src/dev/compile.ts
+++ b/packages/core/src/dev/compile.ts
@@ -1,5 +1,6 @@
 import { parse } from "messageformat-parser"
 import { isString } from "../essentials"
+import { CompiledMessage } from "../i18n"
 
 // [Tokens] -> (CTX -> String)
 function processTokens(tokens) {
@@ -48,7 +49,7 @@ function processTokens(tokens) {
 // Message -> (Params -> String)
 export default function compile(
   message: string
-): Array<string | [string, string?, Object?]> | string {
+): CompiledMessage {
   try {
     return processTokens(parse(message))
   } catch (e) {
diff --git a/packages/core/src/i18n.ts b/packages/core/src/i18n.ts
index ed35c19b..f30930b8 100644
--- a/packages/core/src/i18n.ts
+++ b/packages/core/src/i18n.ts
@@ -20,7 +20,7 @@ export type AllLocaleData = Record<Locale, LocaleData>
 
 export type CompiledMessage =
   | string
-  | Array<string | [string, string?, (string | Object)?]>
+  | Array<string | Array<string | (string | undefined) | Record<string, unknown>>>;
 
 export type Messages = Record<string, CompiledMessage>
 
diff --git a/packages/react/src/format.ts b/packages/react/src/format.ts
index c60dedb7..2c5f814a 100644
--- a/packages/react/src/format.ts
+++ b/packages/react/src/format.ts
@@ -36,6 +36,7 @@ function formatElements(
   value: string,
   elements: { [key: string]: React.ReactElement<any> } = {}
 ): string | Array<any> {
+  const uniqueId = makeCounter(0, '$lingui$')
   const parts = value.replace(nlRe, "").split(tagRe)
 
   // no inline elements, return
@@ -67,7 +68,7 @@ function formatElements(
     tree.push(
       React.cloneElement(
         element,
-        { key: index },
+        { key: uniqueId() },
 
         // format children for pair tags
         // unpaired tags might have children if it's a component passed as a variable
@@ -105,4 +106,6 @@ function getElements(parts) {
   )
 }
 
+const makeCounter = (count = 0, prefix = "") => () => `${prefix}_${count++}`
+
 export { formatElements }


Test patch:
diff --git a/packages/react/src/format.test.tsx b/packages/react/src/format.test.tsx
index a38f1e3d..09d835ac 100644
--- a/packages/react/src/format.test.tsx
+++ b/packages/react/src/format.test.tsx
@@ -76,4 +76,11 @@ describe("formatElements", function () {
     expect(html(formatElements("text<0/>", {0: <span />})))
         .toEqual("text<span></span>")
   })
+
+  it("should create two children with different keys", function() {
+    const cleanPrefix = (str: string): number => Number.parseInt(str.replace("$lingui$_", ""), 10)
+    const childElements = formatElements("<div><0/><0/></div>", { 0: <span>hi</span> }) as Array<any>
+    const childKeys = childElements.map(el => el?.key).filter(Boolean);
+    expect(cleanPrefix(childKeys[0])).toBeLessThan(cleanPrefix(childKeys[1]))
+  })
 })