From 1d90bbf7e1d19d7adb9ce9af6708bb16af82d6f5 Mon Sep 17 00:00:00 2001 From: Christoffer Jahren Date: Fri, 24 Mar 2023 08:19:45 +0100 Subject: [PATCH] feat(core): add separate node/js translation api for usage without macros --- lerna.json | 2 +- .../src/index.ts | 25 ++---- .../test/__snapshots__/index.ts.snap | 10 +++ .../test/fixtures/js-call-expression.js | 2 +- packages/core/src/i18n.test.ts | 84 +++++++++++++++++++ packages/core/src/i18n.ts | 4 +- website/docs/ref/core.md | 24 ++++++ 7 files changed, 130 insertions(+), 21 deletions(-) diff --git a/lerna.json b/lerna.json index 99f59e230..ae8526f9f 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "4.0.12", + "version": "4.0.0-next.3", "packages": [ "packages/*" ], diff --git a/packages/babel-plugin-extract-messages/src/index.ts b/packages/babel-plugin-extract-messages/src/index.ts index 0164f8a08..ec9c727d5 100644 --- a/packages/babel-plugin-extract-messages/src/index.ts +++ b/packages/babel-plugin-extract-messages/src/index.ts @@ -150,7 +150,7 @@ export default function ({ types: t }: { types: BabelTypes }): PluginObj { const isNodeJSI18nMethod = (node: Node) => t.isMemberExpression(node) && t.isIdentifier(node.object, { name: "i18n" }) && - t.isIdentifier(node.property, { name: "nodeTranslate" }) + t.isIdentifier(node.property, { name: "t" }) function hasI18nComment(node: Node): boolean { return ( @@ -225,9 +225,6 @@ export default function ({ types: t }: { types: BabelTypes }): PluginObj { }, CallExpression(path, ctx) { - console.log("call expr", { - isNodie: isNodeJSI18nMethod(path.node.callee), - }) const hasComment = [path.node, path.parent].some((node) => hasI18nComment(node) ) @@ -236,23 +233,17 @@ export default function ({ types: t }: { types: BabelTypes }): PluginObj { let props: Record = {} - if (isNodeJSI18nMethod(path.node.callee)) { - console.log("arg which is obj expr", { - ...path.node.arguments.map((arg) => t.isObjectExpression(arg)), - }) - } - if ( isNodeJSI18nMethod(path.node.callee) && - t.isObjectExpression(path.node.arguments[0]) + t.isObjectExpression(firstArgument) ) { props = { - ...extractFromObjectExpression( - t, - path.node.arguments[0], - ctx.file.hub, - ["id", "message", "comment", "context"] - ), + ...extractFromObjectExpression(t, firstArgument, ctx.file.hub, [ + "id", + "message", + "comment", + "context", + ]), } collectMessage(path, props, ctx) 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 51760bd42..d4f85a33b 100644 --- a/packages/babel-plugin-extract-messages/test/__snapshots__/index.ts.snap +++ b/packages/babel-plugin-extract-messages/test/__snapshots__/index.ts.snap @@ -52,6 +52,16 @@ exports[`@lingui/babel-plugin-extract-messages CallExpression i18n._() should ex 9, ], }, + { + comment: My comment, + context: undefined, + id: my.id, + message: My Id Message, + origin: [ + js-call-expression.js, + 11, + ], + }, ] `; diff --git a/packages/babel-plugin-extract-messages/test/fixtures/js-call-expression.js b/packages/babel-plugin-extract-messages/test/fixtures/js-call-expression.js index 33b212f99..b75593df7 100644 --- a/packages/babel-plugin-extract-messages/test/fixtures/js-call-expression.js +++ b/packages/babel-plugin-extract-messages/test/fixtures/js-call-expression.js @@ -8,4 +8,4 @@ const withValues = i18n._('Values {param}', { param: param }); const withContext = i18n._('Some id', {},{ context: 'Context1'}); -const withNodeMessageDescriptor = i18n.nodeTranslate({ id: 'my.id', message: 'My Id Message', comment: 'My comment'}); +const withNodeMessageDescriptor = i18n.t({ id: 'my.id', message: 'My Id Message', comment: 'My comment'}); diff --git a/packages/core/src/i18n.test.ts b/packages/core/src/i18n.test.ts index 074842397..eeb21b981 100644 --- a/packages/core/src/i18n.test.ts +++ b/packages/core/src/i18n.test.ts @@ -286,6 +286,90 @@ describe("I18n", () => { }) }) + it(".t should format message from catalog", () => { + const messages = { + Hello: "Salut", + "My name is {name}": "Je m'appelle {name}", + } + + const i18n = setupI18n({ + locale: "fr", + messages: { fr: messages }, + }) + + expect(i18n.t({ id: "Hello" })).toEqual("Salut") + expect(i18n.t({ id: "My name is {name}" }, { name: "Fred" })).toEqual( + "Je m'appelle Fred" + ) + + // missing { name } + expect(i18n.t({ id: "My name is {name}" })).toEqual("Je m'appelle") + + // Untranslated message + expect(i18n.t({ id: "Missing message" })).toEqual("Missing message") + expect(i18n.t({ id: "Missing {name}" }, { name: "Fred" })).toEqual( + "Missing Fred" + ) + expect( + i18n.t( + { id: "Missing with default", message: "Missing {name}" }, + { name: "Fred" } + ) + ).toEqual("Missing Fred") + }) + + it(".t allow escaping syntax characters", () => { + const messages = { + "My ''name'' is '{name}'": "Mi ''nombre'' es '{name}'", + } + + const i18n = setupI18n({ + locale: "es", + messages: { es: messages }, + }) + + expect(i18n.t({ id: "My ''name'' is '{name}'" })).toEqual( + "Mi 'nombre' es {name}" + ) + }) + + it(".t shouldn't compile messages in production", () => { + const messages = { + Hello: "Salut", + "My name is {name}": "Je m'appelle {name}", + } + + mockEnv("production", () => { + const { setupI18n } = require("@lingui/core") + const i18n = setupI18n({ + locale: "fr", + messages: { fr: messages }, + }) + + expect(i18n.t({ id: "My name is {name}" }, { name: "Fred" })).toEqual( + "Je m'appelle {name}" + ) + }) + }) + + it(".t should emit missing event for missing translation", () => { + const i18n = setupI18n({ + locale: "en", + messages: { en: { exists: "exists" } }, + }) + + const handler = jest.fn() + i18n.on("missing", handler) + i18n.t({ id: "exists" }) + expect(handler).toHaveBeenCalledTimes(0) + i18n.t({ id: "missing" }) + expect(handler).toHaveBeenCalledTimes(1) + expect(handler).toHaveBeenCalledWith({ + id: "missing", + locale: "en", + }) + }) + describe("params.missing - handling missing translations", () => { it("._ should return custom string for missing translations", () => { const i18n = setupI18n({ diff --git a/packages/core/src/i18n.ts b/packages/core/src/i18n.ts index 6ccb1276c..0f782a4a2 100644 --- a/packages/core/src/i18n.ts +++ b/packages/core/src/i18n.ts @@ -262,9 +262,9 @@ export class I18n extends EventEmitter { )(values, formats) } - // Alias for _ to be used in node without macro setting + // Alias for _ to be used in node/js without macro setting // uses message descriptors only - nodeTranslate(id: MessageDescriptor, values: Values | undefined = {}) { + t(id: MessageDescriptor, values: Values | undefined = {}) { return this._(id, values, {}) } diff --git a/website/docs/ref/core.md b/website/docs/ref/core.md index f4f909b72..8887b09e7 100644 --- a/website/docs/ref/core.md +++ b/website/docs/ref/core.md @@ -153,6 +153,30 @@ i18n._("My name is {name}", { name: "Tom" }) i18n._("msg.id", { name: "Tom" }, { message: "My name is {name}" }) ``` +### `i18n.t(messageDescriptor, values)` {#i18n.t} + +A small wrapper on the core translation meant for NodeJS/JS usage without macros. + +`messageDescriptor` is an object of message parameters. + +`values` is an object of variables used in translated message. + +```ts +import { i18n } from "@lingui/core" + +// Simple message +i18n.t({ id: "Hello" }) + +// Simple message using custom ID +i18n.t({ id: "msg.hello", message: "Hello"}) + +// Message with variable +i18n.t({ id: "My name is {name}" }, { name: "Tom"}); + +// Message with comment, custom ID and variable +i18n.t({ id: "msg.name", message: "My name is {name}", comment: "Message showing the passed in name" }, { name: "Tom"}); +``` + ### `i18n.date(value: string | Date[, format: Intl.DateTimeFormatOptions])` {#i18n.date} > **Returns**: Formatted date string Format a date using the conventional format for the active language.