Repository: lingui/js-lingui
Pull request #1139 (2021-09-27 11:36:43)

feat(macro): Add support for passing custom i18n instance
This allows developers that work in concurrent rendering environments
to use a Lingui instance that is locale-specific, without risking global
Lingui to be switched to another locale.

In React, `<I18nProvider>` may be used to provide a locale-specific
Lingui instance, and it can be retrieved by using `useLingui`. After
that, the instance can be passed as argument to the `t` macro and
it will be used instead of the global instance, ensuring deterministic
behaviour.

This addresses concerns raised in https://github.com/lingui/js-lingui/issues/901, https://github.com/lingui/js-lingui/issues/578

Documentation, TypeScript definition changes and (more) tests are 
left to do,  but the main goal of this PR is to receive feedback on the 
approach and to see if this is on the right track.


## Example

<details><summary>Click to expand usage example</summary>


```tsx
import { I18n } from "@lingui/core";
import { t } from "@lingui/macro";
import { useLingui } from "@lingui/react";
import React, { FC, useCallback } from "react";
import * as Yup from "yup";
import { Field, Form, Formik } from "formik";

function buildYupValidationSchema(i18n: I18n) {
  return Yup.object().shape({
    // 3. Use the `t` macro to translate with the custom instance
    name: Yup.string().required(t(i18n)`Name is required`),
    description: Yup.string().required(t(i18n)`Description is required`),
  });
}

export const ExampleForm: FC = () => {
  // 1. Retrieve the i18n instance from the context
  const { i18n } = useLingui();

  // 2. Pass the i18n instance to functions that need it
  const validationSchema = buildYupValidationSchema(i18n);

  const handleSubmit = useCallback((values) => alert(JSON.stringify(values, null, 2)), []);

  return (
    <Formik
      initialValues={{ name: "", description: "" }}
      validationSchema={validationSchema}
      onSubmit={handleSubmit}
    >
      {({ isSubmitting }) => (
        <Form>
          <Field name="name" type="text" />
          <Field name="description" type="text" />
          <button type="submit" disabled={isSubmitting}>
            Submit
          </button>
        </Form>
      )}
    </Formik>
  );
};
```

</details>

vercel[bot]: This pull request is being automatically deployed with Vercel ([learn more](https://vercel.link/github-learn-more)).  
To see the status of your deployment, click below or on the icon next to each commit.

🔍 Inspect: [https://vercel.com/lingui-js/js-lingui/FQEBiWStBx1W7moBUaUPiTRK8mMx](https://vercel.com/lingui-js/js-lingui/FQEBiWStBx1W7moBUaUPiTRK8mMx)  
✅ Preview: *In Progress*


codecov[bot]: # [Codecov](https://codecov.io/gh/lingui/js-lingui/pull/1139?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lingui) Report
> Merging [#1139](https://codecov.io/gh/lingui/js-lingui/pull/1139?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lingui) (fcd017b) into [main](https://codecov.io/gh/lingui/js-lingui/commit/1c6378daa84f77757f99f6e592f6cb0e1fe02ebd?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lingui) (1c6378d) will **increase** coverage by `0.18%`.
> The diff coverage is `100.00%`.

> :exclamation: Current head fcd017b differs from pull request most recent head 54cd669. Consider uploading reports for the commit 54cd669 to get more accurate results
[![Impacted file tree graph](https://codecov.io/gh/lingui/js-lingui/pull/1139/graphs/tree.svg?width=650&height=150&src=pr&token=CuJ1VD5BY5&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lingui)](https://codecov.io/gh/lingui/js-lingui/pull/1139?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lingui)

```diff
@@            Coverage Diff             @@
##             main    #1139      +/-   ##
==========================================
+ Coverage   82.09%   82.28%   +0.18%     
==========================================
  Files          56       56              
  Lines        1687     1699      +12     
  Branches      462      466       +4     
==========================================
+ Hits         1385     1398      +13     
  Misses        176      176              
+ Partials      126      125       -1     
```


| [Impacted Files](https://codecov.io/gh/lingui/js-lingui/pull/1139?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lingui) | Coverage Δ | |
|---|---|---|
| [packages/macro/src/index.ts](https://codecov.io/gh/lingui/js-lingui/pull/1139/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lingui#diff-cGFja2FnZXMvbWFjcm8vc3JjL2luZGV4LnRz) | `81.66% <100.00%> (+0.31%)` | :arrow_up: |
| [packages/macro/src/macroJs.ts](https://codecov.io/gh/lingui/js-lingui/pull/1139/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lingui#diff-cGFja2FnZXMvbWFjcm8vc3JjL21hY3JvSnMudHM=) | `92.36% <100.00%> (+1.38%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/lingui/js-lingui/pull/1139?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lingui).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lingui)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/lingui/js-lingui/pull/1139?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lingui). Last update [ea43886...54cd669](https://codecov.io/gh/lingui/js-lingui/pull/1139?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lingui). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lingui).

semoal: Looks incredibly good Jeroen, not much more to say from my part that we should adjust TypeScript definitions and document this nicely and we're done for shipping this asap :)
jeroenvisser101: @semoal thanks! I was looking at the types, and found some time to add more documentation to them, along with usage examples that show up when using them in VSCode or other editors with language server integrations:

![image](https://user-images.githubusercontent.com/1941348/134955136-63949404-b6dc-45d3-ae7b-e3429b1a3f05.png)

Let me know if there are parts where the wording may differ from the wording used in other places in Lingui, because that could cause confusion so I'd prefer to match them :)

I'll work on the documentation tonight 👍 

PS. I was also still thinking about an additional improvement, to allow a `t`-like macro that works like `defineMessage`, which would enable you to do: ```i18n._(dt`Hello ${world}`)```. It wouldn't differ much from ```defineMessage({message: `Hello ${world}`})```, and so I'll think about it a bit more, but if you have thoughts/ideas about it, I'm happy to hear them :)
jeroenvisser101: I think the docs are ready to go, they are short, but I also don't think a lot more is to be said about it. Going into it with examples may be useful, but I worried that they would be too specific to one implementation (e.g. React SSR) and not useful for other use-cases, maybe even misleading. Wdyt?

Patch:
diff --git a/docs/ref/macro.rst b/docs/ref/macro.rst
index 3d955dc4..0b0b8d39 100644
--- a/docs/ref/macro.rst
+++ b/docs/ref/macro.rst
@@ -77,7 +77,7 @@ transformed into ``i18n._`` call.
 
    By default, the ``i18n`` object is imported from ``@lingui/core``.
    If you use a custom instance of ``i18n`` object, you need to set
-   :conf:`runtimeConfigModule`
+   :conf:`runtimeConfigModule` or pass a custom instance to :jsmacro:`t`.
 
 The only exception is :jsmacro:`defineMessage` which is transformed into
 message descriptor. In other words, the message isn't translated directly
@@ -122,6 +122,16 @@ Examples of JS macros
 +-------------------------------------------------------------+--------------------------------------------------------------------+
 | .. code-block:: js                                          | .. code-block:: js                                                 |
 |                                                             |                                                                    |
+|    t(customI18n)`Refresh inbox`                             |    /*i18n*/                                                        |
+|                                                             |    customI18n._("Refresh inbox")                                   |
++-------------------------------------------------------------+--------------------------------------------------------------------+
+| .. code-block:: js                                          | .. code-block:: js                                                 |
+|                                                             |                                                                    |
+|    t(customI18n)`Attachment ${name} saved`                  |    /*i18n*/                                                        |
+|                                                             |    customI18n._("Attachment {name} saved", { name })               |
++-------------------------------------------------------------+--------------------------------------------------------------------+
+| .. code-block:: js                                          | .. code-block:: js                                                 |
+|                                                             |                                                                    |
 |    plural(count, {                                          |    /*i18n*/                                                        |
 |       one: "Message",                                       |    i18n._("{count, plural, one {Message} other {Messages}}", {     |
 |       other: "Messages"                                     |      count                                                         |
@@ -184,7 +194,7 @@ into a *Message Descriptor* wrapped inside of ``i18n._`` call.
 
    By default, the ``i18n`` object is imported from ``@lingui/core``.
    If you use a custom instance of ``i18n`` object, you need to set
-   :conf:`runtimeConfigModule`
+   :conf:`runtimeConfigModule` or pass a custom instance to :jsmacro:`t`.
 
 *Message Descriptor* is an object with message ID, default message and other parameters.
 ``i18n._`` accepts message descriptors and performs translation and formatting:
@@ -265,6 +275,22 @@ other expressions are referenced by numeric index:
      0: new Date()
    });
 
+Optionally, a custom ``i18n`` instance can be passed that can be used
+instead of the global instance:
+
+.. code-block:: jsx
+
+   import { t } from "@lingui/macro"
+   import { i18n } from "./lingui"
+   const message = t(i18n)`Hello World`
+
+   // ↓ ↓ ↓ ↓ ↓ ↓
+
+   import { i18n } from "./lingui"
+   const message =
+   /*i18n*/
+   i18n._("Hello World")
+
 It's also possible to pass custom ``id`` and ``comment`` for translators by
 calling ``t`` macro with a message descriptor:
 
diff --git a/packages/macro/global.d.ts b/packages/macro/global.d.ts
index 674266eb..25a92329 100644
--- a/packages/macro/global.d.ts
+++ b/packages/macro/global.d.ts
@@ -1,13 +1,68 @@
-declare module '@lingui/macro' {
-  import type { MessageDescriptor } from "@lingui/core"
+declare module "@lingui/macro" {
+  import type { MessageDescriptor, I18n } from "@lingui/core"
 
   export type BasicType = {
     id?: string
     comment?: string
   }
 
+  /**
+   * Translates a message descriptor
+   *
+   * @example
+   * ```
+   * import { t } from "@lingui/macro";
+   * const message = t({
+   *   id: "msg.hello",
+   *   comment: "Greetings at the homepage",
+   *   message: `Hello ${name}`,
+   * });
+   * ```
+   *
+   * @example
+   * ```
+   * import { t } from "@lingui/macro";
+   * const message = t({
+   *   id: "msg.plural",
+   *   message: plural(value, { one: "...", other: "..." }),
+   * });
+   * ```
+   *
+   * @param messageDescriptior The descriptor to translate
+   */
+  export function t(messageDescriptior: MessageDescriptor): string
+
+  /**
+   * Translates a template string using the global I18n instance
+   *
+   * @example
+   * ```
+   * import { t } from "@lingui/macro";
+   * const message = t`Hello ${name}`;
+   * ```
+   */
+  export function t(
+    literals: TemplateStringsArray,
+    ...placeholders: any[]
+  ): string
+
+  /**
+   * Translates a template string using a given I18n instance
+   *
+   * @example
+   * ```
+   * import { t } from "@lingui/macro";
+   * import { I18n } from "@lingui/core";
+   * const i18n = new I18n({
+   *   locale: "nl",
+   *   messages: { "Hello {0}": "Hallo {0}" },
+   * });
+   * const message = t(i18n)`Hello ${name}`;
+   * ```
+   */
   export function t(
-    literals: TemplateStringsArray | MessageDescriptor,
+    i18n: I18n,
+    literals: TemplateStringsArray,
     ...placeholders: any[]
   ): string
 
@@ -21,20 +76,107 @@ declare module '@lingui/macro' {
     other?: T
   } & UnderscoreDigit<T>
 
-  export function plural(arg: number | string, options: ChoiceOptions & BasicType): string
+  /**
+   * Pluralize a message
+   *
+   * @example
+   * ```
+   * import { plural } from "@lingui/macro";
+   * const message = plural(count, {
+   *   one: "# Book",
+   *   other: "# Books",
+   * });
+   * ```
+   *
+   * @param value Determines the plural form
+   * @param options Object with available plural forms
+   */
+  export function plural(
+    value: number | string,
+    options: ChoiceOptions & BasicType
+  ): string
+
+  /**
+   * Pluralize a message using ordinal forms
+   *
+   * Similar to `plural` but instead of using cardinal plural forms,
+   * it uses ordinal forms.
+   *
+   * @example
+   * ```
+   * import { selectOrdinal } from "@lingui/macro";
+   * const message = selectOrdinal(count, {
+   *    one: "1st",
+   *    two: "2nd",
+   *    few: "3rd",
+   *    other: "#th",
+   * });
+   * ```
+   *
+   * @param value Determines the plural form
+   * @param options Object with available plural forms
+   */
   export function selectOrdinal(
-    arg: number | string,
+    value: number | string,
     options: ChoiceOptions & BasicType
   ): string
-  export function select(arg: string, choices: Record<string, string> & BasicType): string
+
+  /**
+   * Selects a translation based on a value
+   *
+   * Select works like a switch statement. It will
+   * select one of the forms in `options` object which
+   * key matches exactly `value`.
+   *
+   * @example
+   * ```
+   * import { select } from "@lingui/macro";
+   * const message = select(gender, {
+   *    male: "he",
+   *    female: "she",
+   *    other: "they",
+   * });
+   * ```
+   *
+   * @param value The key of choices to use
+   * @param choices
+   */
+  export function select(
+    value: string,
+    choices: Record<string, string> & BasicType
+  ): string
+
+  /**
+   * Defines multiple messages for extraction
+   */
   export function defineMessages<M extends Record<string, MessageDescriptor>>(
     messages: M
   ): M
-  export function defineMessage(descriptor: MessageDescriptor): MessageDescriptor
+
+  /**
+   * Define a message for later use
+   *
+   * `defineMessage` can be used to add comments for translators,
+   * or to override the message ID.
+   *
+   * @example
+   * ```
+   * import { defineMessage } from "@lingui/macro";
+   * const message = defineMessage({
+   *    comment: "Greetings on the welcome page",
+   *    message: `Welcome, ${name}!`,
+   * });
+   * ```
+   *
+   * @param descriptor The message descriptor
+   */
+  export function defineMessage(
+    descriptor: MessageDescriptor
+  ): MessageDescriptor
 
   export type ChoiceProps = {
     value?: string | number
-  }  & ChoiceOptions<string>
+  } & ChoiceOptions<string>
 
   /**
    * The types should be changed after this PR is merged
@@ -60,4 +202,4 @@ declare module '@lingui/macro' {
   export const Plural: any
   export const Select: any
   export const SelectOrdinal: any
-}
\ No newline at end of file
+}
diff --git a/packages/macro/index.d.ts b/packages/macro/index.d.ts
index 46e10a36..0a0ffe3e 100644
--- a/packages/macro/index.d.ts
+++ b/packages/macro/index.d.ts
@@ -1,12 +1,7 @@
 import type { ReactElement, ComponentType, ReactNode } from "react"
-import type { MessageDescriptor } from "@lingui/core"
+import type { MessageDescriptor, I18n } from "@lingui/core"
 import type { TransRenderProps } from "@lingui/react"
 
-export function t(
-  literals: TemplateStringsArray | MessageDescriptor,
-  ...placeholders: any[]
-): string
-
 export type UnderscoreDigit<T = string> = { [digit: string]: T }
 export type ChoiceOptions<T = string> = {
   offset?: number
@@ -17,15 +12,156 @@ export type ChoiceOptions<T = string> = {
   other?: T
 } & UnderscoreDigit<T>
 
-export function plural(arg: number | string, options: ChoiceOptions): string
+/**
+ * Translates a message descriptor
+ *
+ * @example
+ * ```
+ * import { t } from "@lingui/macro";
+ * const message = t({
+ *   id: "msg.hello",
+ *   comment: "Greetings at the homepage",
+ *   message: `Hello ${name}`,
+ * });
+ * ```
+ *
+ * @example
+ * ```
+ * import { t } from "@lingui/macro";
+ * const message = t({
+ *   id: "msg.plural",
+ *   message: plural(value, { one: "...", other: "..." }),
+ * });
+ * ```
+ *
+ * @param messageDescriptior The descriptor to translate
+ */
+export function t(messageDescriptior: MessageDescriptor): string
+
+/**
+ * Translates a template string using the global I18n instance
+ *
+ * @example
+ * ```
+ * import { t } from "@lingui/macro";
+ * const message = t`Hello ${name}`;
+ * ```
+ */
+export function t(
+  literals: TemplateStringsArray,
+  ...placeholders: any[]
+): string
+
+/**
+ * Translates a template string using a given I18n instance
+ *
+ * @example
+ * ```
+ * import { t } from "@lingui/macro";
+ * import { I18n } from "@lingui/core";
+ * const i18n = new I18n({
+ *   locale: "nl",
+ *   messages: { "Hello {0}": "Hallo {0}" },
+ * });
+ * const message = t(i18n)`Hello ${name}`;
+ * ```
+ */
+export function t(
+  i18n: I18n,
+  literals: TemplateStringsArray,
+  ...placeholders: any[]
+): string
+
+/**
+ * Pluralize a message
+ *
+ * @example
+ * ```
+ * import { plural } from "@lingui/macro";
+ * const message = plural(count, {
+ *   one: "# Book",
+ *   other: "# Books",
+ * });
+ * ```
+ *
+ * @param value Determines the plural form
+ * @param options Object with available plural forms
+ */
+export function plural(value: number | string, options: ChoiceOptions): string
+
+/**
+ * Pluralize a message using ordinal forms
+ *
+ * Similar to `plural` but instead of using cardinal plural forms,
+ * it uses ordinal forms.
+ *
+ * @example
+ * ```
+ * import { selectOrdinal } from "@lingui/macro";
+ * const message = selectOrdinal(count, {
+ *    one: "1st",
+ *    two: "2nd",
+ *    few: "3rd",
+ *    other: "#th",
+ * });
+ * ```
+ *
+ * @param value Determines the plural form
+ * @param options Object with available plural forms
+ */
 export function selectOrdinal(
-  arg: number | string,
+  value: number | string,
   options: ChoiceOptions
 ): string
-export function select(arg: string, choices: Record<string, string>): string
+
+/**
+ * Selects a translation based on a value
+ *
+ * Select works like a switch statement. It will
+ * select one of the forms in `options` object which
+ * key matches exactly `value`.
+ *
+ * @example
+ * ```
+ * import { select } from "@lingui/macro";
+ * const message = select(gender, {
+ *    male: "he",
+ *    female: "she",
+ *    other: "they",
+ * });
+ * ```
+ *
+ * @param value The key of choices to use
+ * @param choices
+ */
+export function select(value: string, choices: ChoiceOptions): string
+
+/**
+ * Defines multiple messages for extraction
+ *
+ * @see {@link defineMessage} for more details
+ */
 export function defineMessages<M extends Record<string, MessageDescriptor>>(
   messages: M
 ): M
+
+/**
+ * Define a message for later use
+ *
+ * `defineMessage` can be used to add comments for translators,
+ * or to override the message ID.
+ *
+ * @example
+ * ```
+ * import { defineMessage } from "@lingui/macro";
+ * const message = defineMessage({
+ *    comment: "Greetings on the welcome page",
+ *    message: `Welcome, ${name}!`,
+ * });
+ * ```
+ *
+ * @param descriptor The message descriptor
+ */
 export function defineMessage(descriptor: MessageDescriptor): MessageDescriptor
 
 export type TransProps = {
diff --git a/packages/macro/src/index.ts b/packages/macro/src/index.ts
index 5163c7f6..e22e4de2 100644
--- a/packages/macro/src/index.ts
+++ b/packages/macro/src/index.ts
@@ -30,6 +30,7 @@ const [TransImportModule, TransImportName = "Trans"] = getSymbolSource("Trans")
 function macro({ references, state, babel }) {
   const jsxNodes = []
   const jsNodes = []
+  let needsI18nImport = false
 
   Object.keys(references).forEach((tagName) => {
     const nodes = references[tagName]
@@ -53,7 +54,7 @@ function macro({ references, state, babel }) {
   jsNodes.filter(isRootPath(jsNodes)).forEach((path) => {
     if (alreadyVisited(path)) return
     const macro = new MacroJS(babel, { i18nImportName })
-    macro.replacePath(path)
+    if (macro.replacePath(path)) needsI18nImport = true
   })
 
   jsxNodes.filter(isRootPath(jsxNodes)).forEach((path) => {
@@ -62,7 +63,7 @@ function macro({ references, state, babel }) {
     macro.replacePath(path)
   })
 
-  if (jsNodes.length) {
+  if (needsI18nImport) {
     addImport(babel, state, i18nImportModule, i18nImportName)
   }
 
diff --git a/packages/macro/src/macroJs.ts b/packages/macro/src/macroJs.ts
index 3c5de09d..d840f4e4 100644
--- a/packages/macro/src/macroJs.ts
+++ b/packages/macro/src/macroJs.ts
@@ -31,7 +31,8 @@ export default class MacroJs {
 
   replacePathWithMessage = (
     path: NodePath,
-    { id, message, values, comment }
+    { id, message, values, comment },
+    linguiInstance?: babelTypes.Identifier
   ) => {
     const args = []
     const options = []
@@ -75,7 +76,7 @@ export default class MacroJs {
 
     const newNode = this.types.callExpression(
       this.types.memberExpression(
-        this.types.identifier(this.i18nImportName),
+        linguiInstance ?? this.types.identifier(this.i18nImportName),
         this.types.identifier("_")
       ),
       args
@@ -89,13 +90,42 @@ export default class MacroJs {
     path.replaceWith(newNode)
   }
 
-  replacePath = (path: NodePath) => {
+  // Returns a boolean indicating if the replacement requires i18n import
+  replacePath = (path: NodePath): boolean => {
     // reset the expression counter
     this._expressionIndex = makeCounter()
 
     if (this.isDefineMessage(path.node)) {
       this.replaceDefineMessage(path)
-      return
+      return true
+    }
+
+    // t(i18nInstance)`Message` -> i18nInstance._('Message')
+    if (
+      this.types.isCallExpression(path.node) &&
+      this.types.isTaggedTemplateExpression(path.parentPath.node) &&
+      this.types.isIdentifier(path.node.arguments[0]) &&
+      this.isIdentifier(path.node.callee, "t")
+    ) {
+      // Use the first argument as i18n instance instead of the default i18n instance
+      const i18nInstance = path.node.arguments[0]
+      const tokens = this.tokenizeNode(path.parentPath.node)
+
+      const messageFormat = new ICUMessageFormat()
+      const {
+        message: messageRaw,
+        values,
+        id,
+        comment,
+      } = messageFormat.fromTokens(tokens)
+      const message = normalizeWhitespace(messageRaw)
+
+      this.replacePathWithMessage(
+        path.parentPath,
+        { id, message, values, comment },
+        i18nInstance
+      )
+      return false
     }
 
     if (
@@ -103,7 +133,7 @@ export default class MacroJs {
       this.isIdentifier(path.node.callee, "t")
     ) {
       this.replaceTAsFunction(path)
-      return
+      return true
     }
 
     const tokens = this.tokenizeNode(path.node)
@@ -118,6 +148,8 @@ export default class MacroJs {
     const message = normalizeWhitespace(messageRaw)
 
     this.replacePathWithMessage(path, { id, message, values, comment })
+
+    return true
   }
 
   /**
@@ -267,7 +299,11 @@ export default class MacroJs {
           // if it's an unicode we keep the cooked value because it's the parsed value by babel (without unicode chars)
           // This regex will detect if a string contains unicode chars, when they're we should interpolate them
           // why? because platforms like react native doesn't parse them, just doing a JSON.parse makes them UTF-8 friendly
-          const value = /\\u[a-fA-F0-9]{4}|\\x[a-fA-F0-9]{2}/g.test(text.value.raw) ? text.value.cooked : text.value.raw
+          const value = /\\u[a-fA-F0-9]{4}|\\x[a-fA-F0-9]{2}/g.test(
+            text.value.raw
+          )
+            ? text.value.cooked
+            : text.value.raw
           if (value === "") return null
 
           return {
@@ -361,7 +397,7 @@ export default class MacroJs {
   /**
    * We clean '//\` ' to just '`'
    */
-  clearBackslashes(value: string) {
+  clearBackslashes(value: string) {
     // if not we replace the extra scaped literals
     return value.replace(/\\`/g, "`")
   }


Test patch:
diff --git a/packages/macro/src/macroJs.test.ts b/packages/macro/src/macroJs.test.ts
index 558c8519..6284e7db 100644
--- a/packages/macro/src/macroJs.test.ts
+++ b/packages/macro/src/macroJs.test.ts
@@ -20,6 +20,18 @@ describe("js macro", () => {
       ])
     })
 
+    it("with custom lingui instance", () => {
+      const macro = createMacro()
+      const exp = parseExpression("t(i18n)`Message`")
+      const tokens = macro.tokenizeTemplateLiteral(exp)
+      expect(tokens).toEqual([
+        {
+          type: "text",
+          value: "Message",
+        },
+      ])
+    })
+
     it("message with named argument", () => {
       const macro = createMacro()
       const exp = parseExpression("t`Message ${name}`")
@@ -82,50 +94,52 @@ describe("js macro", () => {
 
     it("message with unicode \\u chars is interpreted by babel", () => {
       const macro = createMacro()
-      const exp = parseExpression('t`Message \\u0020`')
+      const exp = parseExpression("t`Message \\u0020`")
       const tokens = macro.tokenizeTemplateLiteral(exp)
       expect(tokens).toEqual([
         {
           type: "text",
-          value: 'Message  ',
+          value: "Message  ",
         },
       ])
     })
 
     it("message with unicode \\x chars is interpreted by babel", () => {
       const macro = createMacro()
-      const exp = parseExpression('t`Bienvenue\\xA0!`')
+      const exp = parseExpression("t`Bienvenue\\xA0!`")
       const tokens = macro.tokenizeTemplateLiteral(exp)
       expect(tokens).toEqual([
         {
           type: "text",
-          // Looks like an empty space, but it isn't 
-          value: 'Bienvenue !',
+          // Looks like an empty space, but it isn't
+          value: "Bienvenue !",
         },
       ])
     })
 
     it("message with double scaped literals it's stripped", () => {
       const macro = createMacro()
-      const exp = parseExpression('t\`Passing \\`${argSet}\\` is not supported.\`')
+      const exp = parseExpression(
+        "t`Passing \\`${argSet}\\` is not supported.`"
+      )
       const tokens = macro.tokenizeTemplateLiteral(exp)
       expect(tokens).toEqual([
         {
           type: "text",
-          value: 'Passing `',
+          value: "Passing `",
         },
         {
           name: "argSet",
           type: "arg",
           value: {
             end: 20,
-            loc:  {
-              end:  {
+            loc: {
+              end: {
                 column: 20,
                 line: 1,
               },
               identifierName: "argSet",
-              start:  {
+              start: {
                 column: 14,
                 line: 1,
               },
@@ -298,7 +312,7 @@ describe("js macro", () => {
           female: "she",
           male: "he",
           offset: undefined,
-          other: "they"
+          other: "they",
         }),
         type: "arg",
         value: {
@@ -317,7 +331,7 @@ describe("js macro", () => {
           name: "gender",
           start: 7,
           type: "Identifier",
-        }
+        },
       })
     })
   })
diff --git a/packages/macro/test/js-t.ts b/packages/macro/test/js-t.ts
index da0b7ee2..6da76684 100644
--- a/packages/macro/test/js-t.ts
+++ b/packages/macro/test/js-t.ts
@@ -12,6 +12,20 @@ export default [
           i18n._("Expression assignment")
     `,
   },
+  {
+    name: "Macro is used in expression assignment, with custom lingui instance",
+    input: `
+        import { t } from '@lingui/macro';
+        import { i18n } from './lingui';
+        const a = t(i18n)\`Expression assignment\`;
+    `,
+    expected: `
+        import { i18n } from './lingui';
+        const a =
+          /*i18n*/
+          i18n._("Expression assignment")
+    `,
+  },
   {
     name: "Variables are replaced with named arguments",
     input: `