diff --git a/packages/babel-plugin-extract-messages/test/__snapshots__/index.ts.snap b/packages/babel-plugin-extract-messages/test/__snapshots__/index.ts.snap index 1e5fda52f..b2ec7dda0 100644 --- a/packages/babel-plugin-extract-messages/test/__snapshots__/index.ts.snap +++ b/packages/babel-plugin-extract-messages/test/__snapshots__/index.ts.snap @@ -287,6 +287,26 @@ exports[`@lingui/babel-plugin-extract-messages should extract all messages from 48, ], }, + { + comment: undefined, + context: undefined, + id: VO4BJY, + message: [useLingui]: TplLiteral, + origin: [ + js-with-macros.js, + 53, + ], + }, + { + comment: undefined, + context: undefined, + id: ZxxjOE, + message: [useLingui]: Text {0, plural, offset:1 =0 {No books} =1 {1 book} other {# books}}, + origin: [ + js-with-macros.js, + 56, + ], + }, ] `; diff --git a/packages/babel-plugin-extract-messages/test/fixtures/js-with-macros.js b/packages/babel-plugin-extract-messages/test/fixtures/js-with-macros.js index 57083825a..d9dcf8741 100644 --- a/packages/babel-plugin-extract-messages/test/fixtures/js-with-macros.js +++ b/packages/babel-plugin-extract-messages/test/fixtures/js-with-macros.js @@ -1,4 +1,4 @@ -import { t, defineMessage, msg } from "@lingui/macro" +import { t, defineMessage, msg, useLingui } from "@lingui/macro" t`Message` @@ -46,3 +46,17 @@ const defineMessageAlias = msg({ }) const defineMessageAlias2 = msg`TplLiteral` + +function MyComponent() { + const { t } = useLingui() + + t`[useLingui]: TplLiteral` + + // macro nesting + const a = t`[useLingui]: Text ${plural(users.length, { + offset: 1, + 0: "No books", + 1: "1 book", + other: "# books", + })}` +} diff --git a/packages/conf/src/__snapshots__/index.test.ts.snap b/packages/conf/src/__snapshots__/index.test.ts.snap index 31d7fa30f..be831a033 100644 --- a/packages/conf/src/__snapshots__/index.test.ts.snap +++ b/packages/conf/src/__snapshots__/index.test.ts.snap @@ -57,6 +57,8 @@ exports[`@lingui/conf should return default config 1`] = ` TransImportName: Trans, i18nImportModule: @lingui/core, i18nImportName: i18n, + useLinguiImportModule: @lingui/react, + useLinguiImportName: useLingui, }, service: { apiKey: , diff --git a/packages/conf/src/migrations/normalizeRuntimeConfigModule.test.ts b/packages/conf/src/migrations/normalizeRuntimeConfigModule.test.ts index afb54ff59..3842691f8 100644 --- a/packages/conf/src/migrations/normalizeRuntimeConfigModule.test.ts +++ b/packages/conf/src/migrations/normalizeRuntimeConfigModule.test.ts @@ -1,62 +1,77 @@ import { normalizeRuntimeConfigModule } from "./normalizeRuntimeConfigModule" describe("normalizeRuntimeConfigModule", () => { - it("only i18n specified", () => { - const actual = normalizeRuntimeConfigModule({ - runtimeConfigModule: ["../my-i18n", "myI18n"], - }) + test("all defaults", () => { + const actual = normalizeRuntimeConfigModule({ runtimeConfigModule: {} }) - expect(actual.runtimeConfigModule).toStrictEqual({ - TransImportModule: "@lingui/react", - TransImportName: "Trans", - i18nImportModule: "../my-i18n", - i18nImportName: "myI18n", - }) + expect(actual.runtimeConfigModule).toMatchInlineSnapshot(` + { + TransImportModule: @lingui/react, + TransImportName: Trans, + i18nImportModule: @lingui/core, + i18nImportName: i18n, + useLinguiImportModule: @lingui/react, + useLinguiImportName: useLingui, + } + `) }) - it("Trans and i18n specified", () => { + test("i18n specified as legacy shorthand", () => { const actual = normalizeRuntimeConfigModule({ - runtimeConfigModule: { - i18n: ["./custom-i18n", "myI18n"], - Trans: ["./custom-trans", "myTrans"], - }, + runtimeConfigModule: ["../my-i18n", "myI18n"], }) - expect(actual.runtimeConfigModule).toStrictEqual({ - TransImportModule: "./custom-trans", - TransImportName: "myTrans", - i18nImportModule: "./custom-i18n", - i18nImportName: "myI18n", - }) + expect(actual.runtimeConfigModule).toMatchInlineSnapshot(` + { + TransImportModule: @lingui/react, + TransImportName: Trans, + i18nImportModule: ../my-i18n, + i18nImportName: myI18n, + useLinguiImportModule: @lingui/react, + useLinguiImportName: useLingui, + } + `) }) - it("i18n specified as object", () => { + it("All runtime modules specified", () => { const actual = normalizeRuntimeConfigModule({ runtimeConfigModule: { i18n: ["./custom-i18n", "myI18n"], + Trans: ["./custom-trans", "myTrans"], + useLingui: ["./custom-use-lingui", "myLingui"], }, }) - expect(actual.runtimeConfigModule).toStrictEqual({ - TransImportModule: "@lingui/react", - TransImportName: "Trans", - i18nImportModule: "./custom-i18n", - i18nImportName: "myI18n", - }) + expect(actual.runtimeConfigModule).toMatchInlineSnapshot(` + { + TransImportModule: ./custom-trans, + TransImportName: myTrans, + i18nImportModule: ./custom-i18n, + i18nImportName: myI18n, + useLinguiImportModule: ./custom-use-lingui, + useLinguiImportName: myLingui, + } + `) }) - it("Trans specified as object", () => { + it("only module is specified", () => { const actual = normalizeRuntimeConfigModule({ runtimeConfigModule: { - Trans: ["./custom-trans", "myTrans"], + i18n: ["./custom-i18n"], + Trans: ["./custom-trans"], + useLingui: ["./custom-use-lingui"], }, }) - expect(actual.runtimeConfigModule).toStrictEqual({ - TransImportModule: "./custom-trans", - TransImportName: "myTrans", - i18nImportModule: "@lingui/core", - i18nImportName: "i18n", - }) + expect(actual.runtimeConfigModule).toMatchInlineSnapshot(` + { + TransImportModule: ./custom-trans, + TransImportName: Trans, + i18nImportModule: ./custom-i18n, + i18nImportName: i18n, + useLinguiImportModule: ./custom-use-lingui, + useLinguiImportName: useLingui, + } + `) }) }) diff --git a/packages/conf/src/migrations/normalizeRuntimeConfigModule.ts b/packages/conf/src/migrations/normalizeRuntimeConfigModule.ts index ca5bd77e9..f075483eb 100644 --- a/packages/conf/src/migrations/normalizeRuntimeConfigModule.ts +++ b/packages/conf/src/migrations/normalizeRuntimeConfigModule.ts @@ -9,12 +9,18 @@ const getSymbolSource = ( const name = defaults[1] if (Array.isArray(config)) { if (name === "i18n") { - return config + return config as ModuleSrc } return defaults } - return config[name] || defaults + const override = (config as any)[name] as ModuleSrc + + if (!override) { + return defaults + } + + return [override[0], override[1] || name] } export function normalizeRuntimeConfigModule( @@ -28,6 +34,10 @@ export function normalizeRuntimeConfigModule( ["@lingui/react", "Trans"], config.runtimeConfigModule ) + const [useLinguiImportModule, useLinguiImportName] = getSymbolSource( + ["@lingui/react", "useLingui"], + config.runtimeConfigModule + ) return { ...config, @@ -36,6 +46,8 @@ export function normalizeRuntimeConfigModule( i18nImportName, TransImportModule, TransImportName, + useLinguiImportModule, + useLinguiImportName, } satisfies LinguiConfigNormalized["runtimeConfigModule"], } } diff --git a/packages/conf/src/types.ts b/packages/conf/src/types.ts index 46cdf9605..ea21f44e0 100644 --- a/packages/conf/src/types.ts +++ b/packages/conf/src/types.ts @@ -111,7 +111,7 @@ type LocaleObject = { export type FallbackLocales = LocaleObject -type ModuleSource = [string, string?] +type ModuleSource = readonly [module: string, specifier?: string] type CatalogService = { name: string @@ -207,7 +207,9 @@ export type LinguiConfig = { orderBy?: OrderBy pseudoLocale?: string rootDir?: string - runtimeConfigModule?: ModuleSource | { [symbolName: string]: ModuleSource } + runtimeConfigModule?: + | ModuleSource + | Partial> sourceLocale?: string service?: CatalogService experimental?: { @@ -225,5 +227,7 @@ export type LinguiConfigNormalized = Omit< i18nImportName: string TransImportModule: string TransImportName: string + useLinguiImportModule: string + useLinguiImportName: string } } diff --git a/packages/macro/__typetests__/index.test-d.tsx b/packages/macro/__typetests__/index.test-d.tsx index 771507296..a246177e2 100644 --- a/packages/macro/__typetests__/index.test-d.tsx +++ b/packages/macro/__typetests__/index.test-d.tsx @@ -12,6 +12,7 @@ import { Plural, Select, SelectOrdinal, + useLingui, } from "../index" // eslint-disable-next-line import/no-extraneous-dependencies import React from "react" @@ -338,3 +339,17 @@ m = ( other={...} /> ) + +//////////////////////// +//// React useLingui() +//////////////////////// +function MyComponent() { + const { t, i18n } = useLingui() + + expectType(t`Hello world`) + expectType(t({ message: "my message" })) + // @ts-expect-error: you could not pass a custom instance here + t(i18n)({ message: "my message" }) + + expectType(i18n) +} diff --git a/packages/macro/index.d.ts b/packages/macro/index.d.ts index dca81ea01..8c6a84e8d 100644 --- a/packages/macro/index.d.ts +++ b/packages/macro/index.d.ts @@ -1,7 +1,7 @@ // eslint-disable-next-line import/no-extraneous-dependencies import type { ReactNode, VFC, FC } from "react" import type { I18n, MessageDescriptor } from "@lingui/core" -import type { TransRenderCallbackOrComponent } from "@lingui/react" +import type { TransRenderCallbackOrComponent, I18nContext } from "@lingui/react" export type ChoiceOptions = { /** Offset of value when calculating plural forms */ @@ -316,3 +316,36 @@ export const SelectOrdinal: VFC * ``` */ export const Select: VFC + +export function _t(descriptor: MacroMessageDescriptor): string +export function _t( + literals: TemplateStringsArray, + ...placeholders: any[] +): string + +/** + * + * Macro version of useLingui replaces _ function with `t` macro function which is bound to i18n passed from React.Context + * + * Returned `t` macro function has all the same signatures as global `t` + * + * @example + * ``` + * const { t } = useLingui(); + * const message = t`Text`; + * ``` + * + * @example + * ``` + * const { i18n, t } = useLingui(); + * const locale = i18n.locale; + * const message = t({ + * id: "msg.hello", + * comment: "Greetings at the homepage", + * message: `Hello ${name}`, + * }); + * ``` + */ +export function useLingui(): Omit & { + t: typeof _t +} diff --git a/packages/macro/src/index.ts b/packages/macro/src/index.ts index ae0a41ae8..ceaa7af5a 100644 --- a/packages/macro/src/index.ts +++ b/packages/macro/src/index.ts @@ -4,14 +4,7 @@ import { getConfig as loadConfig, LinguiConfigNormalized } from "@lingui/conf" import MacroJS from "./macroJs" import MacroJSX from "./macroJsx" import { NodePath } from "@babel/traverse" -import { - ImportDeclaration, - Identifier, - isImportSpecifier, - isIdentifier, - JSXIdentifier, -} from "@babel/types" - +import * as t from "@babel/types" export type LinguiMacroOpts = { // explicitly set by CLI when running extraction process extract?: boolean @@ -23,6 +16,7 @@ const jsMacroTags = new Set([ "msg", "arg", "t", + "useLingui", "plural", "select", "selectOrdinal", @@ -45,16 +39,23 @@ function getConfig(_config?: LinguiConfigNormalized) { function macro({ references, state, babel, config }: MacroParams) { const opts: LinguiMacroOpts = config as LinguiMacroOpts + const body = state.file.path.node.body const { i18nImportModule, i18nImportName, TransImportModule, TransImportName, + useLinguiImportModule, + useLinguiImportName, } = getConfig(opts.linguiConfig).runtimeConfigModule const jsxNodes = new Set() const jsNodes = new Set() let needsI18nImport = false + let needsUseLinguiImport = false + + // create unique name for all _t, must be outside the loop + const uniq_tIdentifier = state.file.scope.generateUidIdentifier("_t") let nameMap = new Map() Object.keys(references).forEach((tagName) => { @@ -62,15 +63,25 @@ function macro({ references, state, babel, config }: MacroParams) { if (jsMacroTags.has(tagName)) { nodes.forEach((path) => { - nameMap.set(tagName, (path.node as Identifier).name) - jsNodes.add(path.parentPath) + if (tagName !== "useLingui") { + nameMap.set(tagName, (path.node as t.Identifier).name) + jsNodes.add(path.parentPath) + } else { + needsUseLinguiImport = true + nameMap.set("_t", uniq_tIdentifier.name) + processUseLingui( + path, + useLinguiImportName, + uniq_tIdentifier.name + )?.forEach((n) => jsNodes.add(n)) + } }) } else if (jsxMacroTags.has(tagName)) { // babel-plugin-macros return JSXIdentifier nodes. // Which is for every JSX element would be presented twice (opening / close) // Here we're taking JSXElement and dedupe it. nodes.forEach((path) => { - nameMap.set(tagName, (path.node as JSXIdentifier).name) + nameMap.set(tagName, (path.node as t.JSXIdentifier).name) // identifier.openingElement.jsxElement jsxNodes.add(path.parentPath.parentPath) @@ -92,7 +103,9 @@ function macro({ references, state, babel, config }: MacroParams) { nameMap, }) try { - if (macro.replacePath(path)) needsI18nImport = true + macro.replacePath(path) + needsI18nImport = needsI18nImport || macro.needsI18nImport + needsUseLinguiImport = needsUseLinguiImport || macro.needsUseLinguiImport } catch (e) { reportUnsupportedSyntax(path, e as Error) } @@ -110,54 +123,133 @@ function macro({ references, state, babel, config }: MacroParams) { } }) + if (needsUseLinguiImport) { + addImport(babel, body, useLinguiImportModule, useLinguiImportName) + } + if (needsI18nImport) { - addImport(babel, state, i18nImportModule, i18nImportName) + addImport(babel, body, i18nImportModule, i18nImportName) } if (jsxNodes.size) { - addImport(babel, state, TransImportModule, TransImportName) + addImport(babel, body, TransImportModule, TransImportName) } } function reportUnsupportedSyntax(path: NodePath, e: Error) { throw path.buildCodeFrameError( - `Unsupported macro usage. Please check the examples at https://lingui.dev/ref/macro#examples-of-js-macros. + `Unsupported macro usage. Please check the examples at https://lingui.dev/ref/macro#examples-of-js-macros. If you think this is a bug, fill in an issue at https://github.com/lingui/js-lingui/issues - + Error: ${e.message}` ) } +/** + * Pre-process useLingui macro + * 1. Get references to destructured t function for macro processing + * 2. Transform usage to non-macro useLingui + * + * @returns Array of paths to useLingui's t macro + */ +function processUseLingui( + path: NodePath, + useLinguiName: string, + newIdentifier: string +): NodePath[] | null { + if (!path.parentPath.parentPath.isVariableDeclarator()) { + reportUnsupportedSyntax( + path, + new Error( + `\`useLingui\` macro must be used in variable declaration. + + Example: + + const { t } = useLingui() +` + ) + ) + return null + } + + const varDec = path.parentPath.parentPath.node + const _property = t.isObjectPattern(varDec.id) + ? varDec.id.properties.find( + ( + property + ): property is t.ObjectProperty & { + key: t.Identifier + value: t.Identifier + } => + t.isObjectProperty(property) && + t.isIdentifier(property.key) && + t.isIdentifier(property.value) && + property.key.name == "t" + ) + : null + + if (!_property) { + reportUnsupportedSyntax( + path.parentPath.parentPath, + new Error( + `You have to destructure \`t\` when using the \`useLingui\` macro, i.e: + const { t } = useLingui() + or + const { t: _ } = useLingui() + ` + ) + ) + return null + } + + if (t.isIdentifier(path.node)) { + // rename to standard useLingui + path.scope.rename(path.node.name, useLinguiName) + } + + // rename to standard useLingui _ + _property.key.name = "_" + path.scope.rename(_property.value.name, newIdentifier) + + return path.scope + .getBinding(_property.value.name) + ?.referencePaths.filter( + // dont process array expression to allow use in dependency arrays + (path) => !path.parentPath.isArrayExpression() + ) + .map((path) => path.parentPath) +} + function addImport( babel: MacroParams["babel"], - state: MacroParams["state"], + body: t.Statement[], module: string, importName: string ) { const { types: t } = babel - const linguiImport = state.file.path.node.body.find( + const linguiImport = body.find( (importNode) => t.isImportDeclaration(importNode) && importNode.source.value === module && // https://github.com/lingui/js-lingui/issues/777 importNode.importKind !== "type" - ) as ImportDeclaration + ) as t.ImportDeclaration const tIdentifier = t.identifier(importName) // Handle adding the import or altering the existing import if (linguiImport) { if ( - linguiImport.specifiers.findIndex( + !linguiImport.specifiers.find( (specifier) => - isImportSpecifier(specifier) && - isIdentifier(specifier.imported, { name: importName }) - ) === -1 + t.isImportSpecifier(specifier) && + t.isIdentifier(specifier.imported, { name: importName }) + ) ) { linguiImport.specifiers.push(t.importSpecifier(tIdentifier, tIdentifier)) } } else { - state.file.path.node.body.unshift( + body.unshift( t.importDeclaration( [t.importSpecifier(tIdentifier, tIdentifier)], t.stringLiteral(module) diff --git a/packages/macro/src/macroJs.ts b/packages/macro/src/macroJs.ts index 49bedba1d..35650035e 100644 --- a/packages/macro/src/macroJs.ts +++ b/packages/macro/src/macroJs.ts @@ -54,6 +54,9 @@ export default class MacroJs { nameMap: Map nameMapReversed: Map + needsUseLinguiImport = false + needsI18nImport = false + // Positional expressions counter (e.g. for placeholders `Hello {0}, today is {1}`) _expressionIndex = makeCounter() @@ -148,13 +151,62 @@ export default class MacroJs { this.isLinguiIdentifier(path.node.callee, "t") ) { this.replaceTAsFunction(path as NodePath) + this.needsI18nImport = true + return true } + // { t } = useLingui() + // t`Hello!` + if ( + path.isTaggedTemplateExpression() && + this.isLinguiIdentifier(path.node.tag, "_t") + ) { + this.needsUseLinguiImport = true + const tokens = this.tokenizeTemplateLiteral(path.node) + + const descriptor = this.createMessageDescriptorFromTokens( + tokens, + path.node.loc + ) + + const callExpr = this.types.callExpression( + this.types.isIdentifier(path.node.tag) && path.node.tag, + [descriptor] + ) + + path.replaceWith(callExpr) + + return false + } + + // { t } = useLingui() + // t(messageDescriptor) + if ( + path.isCallExpression() && + this.isLinguiIdentifier(path.node.callee, "_t") && + this.types.isExpression(path.node.arguments[0]) + ) { + this.needsUseLinguiImport = true + let descriptor = this.processDescriptor(path.node.arguments[0]) + path.node.arguments = [descriptor] + return false + } + + if ( + this.types.isCallExpression(path.node) && + this.isLinguiIdentifier(path.node.callee, "useLingui") && + this.types.isVariableDeclarator(path.parentPath.node) + ) { + this.needsUseLinguiImport = true + return false + } + const tokens = this.tokenizeNode(path.node) this.replacePathWithMessage(path, tokens) + this.needsI18nImport = true return true } diff --git a/packages/macro/test/index.ts b/packages/macro/test/index.ts index e6cc0902b..4e85eefaf 100644 --- a/packages/macro/test/index.ts +++ b/packages/macro/test/index.ts @@ -40,6 +40,7 @@ const testCases: Record = { "jsx-plural": require("./jsx-plural").default, "jsx-selectOrdinal": require("./jsx-selectOrdinal").default, "js-defineMessage": require("./js-defineMessage").default, + "js-useLingui": require("./js-useLingui").default, } function stripIdPlugin(): PluginObj { @@ -322,4 +323,30 @@ describe("macro", function () { }) }) }) + + describe("useLingui", () => { + it("Should throw if used not in the variable declaration", () => { + const code = ` + import {useLingui} from "@lingui/macro"; + + useLingui() + + ` + expect(transformCode(code)).toThrowError( + "Error: `useLingui` macro must be used in variable declaration." + ) + }) + + it("Should throw if not used with destructuring", () => { + const code = ` + import {useLingui} from "@lingui/macro"; + + const lingui = useLingui() + + ` + expect(transformCode(code)).toThrowError( + "You have to destructure `t` when using the `useLingui` macro" + ) + }) + }) }) diff --git a/packages/macro/test/js-useLingui.ts b/packages/macro/test/js-useLingui.ts new file mode 100644 index 000000000..12fdf9972 --- /dev/null +++ b/packages/macro/test/js-useLingui.ts @@ -0,0 +1,380 @@ +import { TestCase } from "./index" +import { makeConfig } from "@lingui/conf" + +const cases: TestCase[] = [ + { + name: "tagged template literal style", + input: ` +import { useLingui } from '@lingui/macro'; + +function MyComponent() { + const { t } = useLingui(); + const a = t\`Text\`; +} + `, + expected: ` +import { useLingui } from "@lingui/react"; + +function MyComponent() { + const { _: _t } = useLingui(); + const a = _t( + /*i18n*/ + { + id: \"xeiujy\", + message: \"Text\", + } + ); +}`, + }, + { + name: "support renamed destructuring", + input: ` +import { useLingui } from '@lingui/macro'; + +function MyComponent() { + const { t: _ } = useLingui(); + const a = _\`Text\`; +} + `, + expected: ` +import { useLingui } from "@lingui/react"; + +function MyComponent() { + const { _: _t } = useLingui(); + const a = _t( + /*i18n*/ + { + id: \"xeiujy\", + message: \"Text\", + } + ); +}`, + }, + { + name: "should process macro with matching name in correct scopes", + input: ` +import { useLingui } from '@lingui/macro'; + +function MyComponent() { + const { t } = useLingui(); + const a = t\`Text\`; + + { + // here is child scope with own "t" binding, shouldn't be processed + const t = () => {}; + t\`Text\`; + } + { + // here is child scope which should be processed, since 't' relates to outer scope + t\`Text\`; + } +} + `, + expected: ` +import { useLingui } from "@lingui/react"; +function MyComponent() { + const { _: _t } = useLingui(); + const a = _t( + /*i18n*/ + { + id: "xeiujy", + message: "Text", + } + ); + { + // here is child scope with own "t" binding, shouldn't be processed + const t = () => {}; + t\`Text\`; + } + { + // here is child scope which should be processed, since 't' relates to outer scope + _t( + /*i18n*/ + { + id: "xeiujy", + message: "Text", + } + ); + } +} + +`, + }, + { + name: "inserted statement should not clash with existing variables", + input: ` +import { useLingui } from '@lingui/macro'; + +function MyComponent() { + const _t = "i'm here"; + const { t: _ } = useLingui(); + const a = _\`Text\`; +} + `, + expected: ` +import { useLingui } from "@lingui/react"; +function MyComponent() { + const _t = "i'm here"; + const { _: _t2 } = useLingui(); + const a = _t2( + /*i18n*/ + { + id: "xeiujy", + message: "Text", + } + ); +} +`, + }, + { + name: "support nested macro", + input: ` +import { useLingui, plural } from '@lingui/macro'; + +function MyComponent() { + const { t } = useLingui(); + const a = t\`Text \${plural(users.length, { + offset: 1, + 0: "No books", + 1: "1 book", + other: "# books" + })}\`; +} + + `, + expected: ` +import { useLingui } from "@lingui/react"; +function MyComponent() { + const { _: _t } = useLingui(); + const a = _t( + /*i18n*/ + { + id: "hJRCh6", + message: + "Text {0, plural, offset:1 =0 {No books} =1 {1 book} other {# books}}", + values: { + 0: users.length, + }, + } + ); +} +`, + }, + { + name: "support message descriptor", + input: ` +import { useLingui } from '@lingui/macro'; + +function MyComponent() { + const { t } = useLingui(); + const a = t({ message: "Hello", context: "my custom" }); +} + `, + expected: ` + import { useLingui } from "@lingui/react"; + function MyComponent() { + const { _: _t } = useLingui(); + const a = _t( + /*i18n*/ + { + context: "my custom", + message: "Hello", + id: "BYqAaU", + } + ); +}`, + }, + { + name: "support passing t variable as dependency", + input: ` +import { useLingui } from '@lingui/macro'; + +function MyComponent() { + const { t } = useLingui(); + const a = useMemo(() => t\`Text\`, [t]); +} + `, + expected: ` +import { useLingui } from "@lingui/react"; +function MyComponent() { + const { _: _t } = useLingui(); + const a = useMemo( + () => + _t( + /*i18n*/ + { + id: "xeiujy", + message: "Text", + } + ), + [_t] + ); +} +`, + }, + { + name: "transform to standard useLingui statement", + input: ` +import { useLingui } from '@lingui/macro'; + +function MyComponent() { + const { i18n, t } = useLingui(); + + console.log(i18n); + const a = t\`Text\`; +} + `, + expected: ` +import { useLingui } from "@lingui/react"; +function MyComponent() { + const { i18n, _: _t } = useLingui(); + console.log(i18n); + const a = _t( + /*i18n*/ + { + id: "xeiujy", + message: "Text", + } + ); +} +`, + }, + { + name: "work with existing useLingui statement", + input: ` +import { useLingui as useLinguiMacro } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; + +function MyComponent() { + const { _ } = useLingui(); + + console.log(_); + const { t } = useLinguiMacro(); + const a = t\`Text\`; +} + `, + expected: ` +import { useLingui } from "@lingui/react"; +function MyComponent() { + const { _ } = useLingui(); + console.log(_); + const { _: _t } = useLingui(); + const a = _t( + /*i18n*/ + { + id: "xeiujy", + message: "Text", + } + ); +} +`, + }, + + { + // todo: implement this + skip: true, + name: "work with renamed existing useLingui statement", + input: ` +import { useLingui as useLinguiMacro } from '@lingui/macro'; +import { useLingui as useLinguiRenamed } from '@lingui/react'; + +function MyComponent() { + const { _ } = useLinguiRenamed(); + + console.log(_); + const { t } = useLinguiMacro(); + const a = t\`Text\`; +} + `, + expected: ` +import { useLingui as useLinguiRenamed } from '@lingui/react'; +import { useLingui } from "@lingui/react"; +function MyComponent() { + const { _ } = useLinguiRenamed(); + console.log(_); + const { _: _t } = useLingui(); + const a = _t( + /*i18n*/ + { + id: "xeiujy", + message: "Text", + } + ); +} +`, + }, + { + name: "work with multiple react components", + input: ` +import { useLingui } from '@lingui/macro'; + +function MyComponent() { + const { t } = useLingui(); + const a = t\`Text\`; +} + +function MyComponent2() { + const { t } = useLingui(); + const b = t\`Text\`; +}`, + expected: ` +import { useLingui } from "@lingui/react"; +function MyComponent() { + const { _: _t } = useLingui(); + const a = _t( + /*i18n*/ + { + id: "xeiujy", + message: "Text", + } + ); +} +function MyComponent2() { + const { _: _t } = useLingui(); + const b = _t( + /*i18n*/ + { + id: "xeiujy", + message: "Text", + } + ); +} +`, + }, + { + name: "support configuring runtime module import using LinguiConfig.runtimeConfigModule", + macroOpts: { + linguiConfig: makeConfig( + { + runtimeConfigModule: { + useLingui: ["@my/lingui-react", "myUselingui"], + }, + }, + { skipValidation: true } + ), + }, + input: ` +import { useLingui } from '@lingui/macro'; + +function MyComponent() { + const { t } = useLingui(); + const a = t\`Text\`; +} +`, + expected: ` +import { myUselingui } from "@my/lingui-react"; +function MyComponent() { + const { _: _t } = myUselingui(); + const a = _t( + /*i18n*/ + { + id: "xeiujy", + message: "Text", + } + ); +} +`, + }, +] + +export default cases diff --git a/website/docs/introduction.md b/website/docs/introduction.md index 8f6ebc54a..d4754b903 100644 --- a/website/docs/introduction.md +++ b/website/docs/introduction.md @@ -55,9 +55,11 @@ Low-level React API is very similar to react-intl and the message format is the ```jsx import React from "react"; -import { t, Trans, Plural } from "@lingui/macro"; +import { Trans, Plural, useLingui } from "@lingui/macro"; export default function Lingui({ numUsers, name = "You" }) { + const { t } = useLingui(); + return (

diff --git a/website/docs/ref/conf.md b/website/docs/ref/conf.md index 70d00a8d1..85564521f 100644 --- a/website/docs/ref/conf.md +++ b/website/docs/ref/conf.md @@ -403,7 +403,7 @@ import { myI18n } from "./custom-i18n-config"; // "runtimeConfigModule": ["./custom-i18n-config", "myI18n"] ``` -In some advanced cases you may also need to change the module from which [Trans](/docs/ref/macro.md#trans) is imported. To do that, pass an object to `runtimeConfigModule`: +In some advanced cases you may also need to change the module from which [Trans](/docs/ref/macro.md#trans) or [useLingui](/docs/ref/macro.md#useLingui) is imported. To do that, pass an object to `runtimeConfigModule`: ```jsx // If you import `i18n` object from custom module like this: @@ -413,6 +413,7 @@ import { Trans, i18n } from "./custom-config"; // "runtimeConfigModule": { // i18n: ["./custom-config", "i18n"], // Trans: ["./custom-config", "Trans"] +// useLingui: ["./custom-useLingui", "myUseLingui"] // } ``` diff --git a/website/docs/ref/macro.md b/website/docs/ref/macro.md index a6798e773..0805ce9b8 100644 --- a/website/docs/ref/macro.md +++ b/website/docs/ref/macro.md @@ -306,7 +306,7 @@ const msg = /*i18n*/ { /> ``` -## JS macros +## Core macros These macros can be used in any context (e.g. outside JSX). All JS macros are transformed into a _Message Descriptor_ wrapped inside of [`i18n._`](/docs/ref/core.md#i18n._) call. @@ -779,7 +779,7 @@ const message = /*i18n*/ { ::: -## JSX Macros +## React Macros ### `Trans` @@ -1015,3 +1015,42 @@ Use ` ``` + +### `useLingui` + +:::note +`useLingui` is available from `@lingui/macro@4.8.0` and not available yet in the `@lingui/swc-plugin` +::: + +Gives access to a [`t`](/docs/ref/macro.md#t) macro bound to the local i18n object passed from React context. + +It returns an object with the following content: + +| Key | Type | Description | +| ------------------ | --------------------- | --------------------------------------------------------------------- | +| `i18n` | `I18n` | the `I18` object instance that you passed to `I18nProvider` | +| `t` | `t` | reference to `t` macro, described above | +| `defaultComponent` | `React.ComponentType` | the same `defaultComponent` you passed to `I18nProvider`, if provided | + +```jsx +import { useLingui } from "@lingui/macro"; + +function MyComponent() { + const { t } = useLingui(); + const a = t`Text`; +} + +// ↓ ↓ ↓ ↓ ↓ ↓ +import { useLingui } from "@lingui/react"; + +function MyComponent() { + const { _ } = useLingui(); + const a = _( + /*i18n*/ + { + id: "xeiujy", + message: "Text", + } + ); +} +``` diff --git a/website/docs/ref/react.md b/website/docs/ref/react.md index 62642d316..21e0db101 100644 --- a/website/docs/ref/react.md +++ b/website/docs/ref/react.md @@ -135,6 +135,23 @@ const CurrentLocale = () => { }; ``` +:::tip +There is a [macro version](/ref/macro#uselingui) of the `useLingui` hook which supports all features of the [`t` macro](/docs/ref/macro.md#t) and uses the runtime `useLingui` hook (from `@lingui/react`) under the hood. + +```jsx +import { useLingui } from "@lingui/macro"; + +const CurrentLocale = () => { + const { t } = useLingui(); + + const userName = "Tim"; + return {t`Hello ${userName}`}; +}; +``` + +You also can safely use the returned `t` function in a dependency array of React hooks. +::: + ## Components The `@lingui/react` package provides `Trans` component to render translations. However, you're more likely to use [macros](/docs/ref/macro.md) instead because they are more convenient and easier to use. diff --git a/website/docs/tutorials/javascript.md b/website/docs/tutorials/javascript.md index 33df358d9..3730a81f2 100644 --- a/website/docs/tutorials/javascript.md +++ b/website/docs/tutorials/javascript.md @@ -58,7 +58,7 @@ i18n.activate("en"); ## Localizing your app -Once that is done, we can go ahead and use it! Wrap you text in [`t`](/docs/ref/macro.md#t) macro and pass it to [`i18n._()`](/docs/ref/core.md#i18n._) method: +Once that is done, we can go ahead and use it! Wrap your text in [`t`](/docs/ref/macro.md#t) macro: ```js import { t } from "@lingui/macro"; diff --git a/website/docs/tutorials/react-native.md b/website/docs/tutorials/react-native.md index 72830f5c3..3dd24cdf1 100644 --- a/website/docs/tutorials/react-native.md +++ b/website/docs/tutorials/react-native.md @@ -117,12 +117,14 @@ We're importing the default `i18n` object from `@lingui/core`. Read more about t Translating the heading is done. Now, let's translate the `title` prop in the `; -} - -export function LoginLogoutButtons(props) { - return ( -
- Log in} /> - Log out} /> -
- ); -} -``` - -If you need the prop to be displayed as a string-only translation, you can pass a message tagged with the [`msg`](/docs/ref/macro.md#definemessage) macro: - -```jsx -import { msg } from "@lingui/macro"; -import { useLingui } from "@lingui/react"; - -export default function ImageWithCaption(props) { - return {props.caption}; -} - -export function HappySad(props) { - const { _ } = useLingui(); - - return ( -
- - -
- ); -} -``` - ### Picking a message based on a variable Sometimes you need to pick between different messages to display, depending on the value of a variable. For example, imagine you have a numeric "status" code that comes from an API, and you need to display a message representing the current status. @@ -277,3 +248,22 @@ export function Welcome() { return
{welcome}
; } ``` + +:::note +Note on [`useLingui`](/ref/macro#uselingui) macro usage. The `t` function destructured from this hook, behaves the same way as `_` from the runtime [`useLingui`](/ref/react#uselingui) counterpart, so you can safely use it in the dependency array. + +```ts +import { useLingui } from "@lingui/macro"; + +export function Welcome() { + const { t } = useLingui(); + + const welcome = useMemo(() => { + return t`Welcome!`; + }, [t]); + + return
{welcome}
; +} +``` + +::: diff --git a/website/docs/tutorials/react.md b/website/docs/tutorials/react.md index ab64a0fe0..a4457f82b 100644 --- a/website/docs/tutorials/react.md +++ b/website/docs/tutorials/react.md @@ -309,6 +309,45 @@ It isn't necessary to extract/translate messages one by one. This usually happen For more info about CLI, checkout the [CLI tutorial](/docs/tutorials/cli.md). +## Non-JSX Translation + +So far we learned how to translate string inside a JSX element, but what if we want to translate something that is not inside a JSX? Or pass a translation as a prop to another component? + +We have this piece of code in our example: + +```js +const markAsRead = () => { + alert("Marked as read."); +}; +``` + +To translate it, we will use the `useLingui` macro hook: + +```js +import { useLingui } from '@lingui/macro'; + +... + +const { t } = useLingui(); + +const markAsRead = () => { + alert(t`Marked as read.`); +}; +``` + +Now the `Marked as read.` message would be picked up by extractor, and available for translation in the catalog. + +You could also pass variables and use any other macro in the message. + +```jsx +const { t } = useLingui(); + +const markAsRead = () => { + const userName = "User1234"; + alert(t`Hello {userName}, your messages marked as read!`); +}; +``` + ## Formatting Let's move on to another paragraph in our project. This paragraph has some variables, some HTML and components inside: @@ -663,16 +702,15 @@ After all modifications, the final component with i18n looks like this: ```jsx title="src/Inbox.js" import React from "react"; -import { Trans, Plural } from "@lingui/macro"; -import { useLingui } from "@lingui/react"; +import { Trans, Plural, useLingui } from "@lingui/macro"; export default function Inbox() { - const { i18n } = useLingui(); + const { i18n, t } = useLingui(); const messages = [{}, {}]; const messagesCount = messages.length; const lastLogin = new Date(); const markAsRead = () => { - alert("Marked as read."); + alert(t`Marked as read.`); }; return (