Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add js/node core translation api for usage without macros #1564

Merged
merged 1 commit into from
Mar 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 38 additions & 4 deletions packages/babel-plugin-extract-messages/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ function extractFromObjectExpression(

export default function ({ types: t }: { types: BabelTypes }): PluginObj {
let localTransComponentName: string
let localCoreI18nName: string

function isTransComponent(node: Node) {
return (
Expand All @@ -147,6 +148,11 @@ export default function ({ types: t }: { types: BabelTypes }): PluginObj {
t.isIdentifier(node.object, { name: "i18n" }) &&
t.isIdentifier(node.property, { name: "_" })

const isI18nTMethod = (node: Node) =>
t.isMemberExpression(node) &&
t.isIdentifier(node.object, { name: localCoreI18nName }) &&
t.isIdentifier(node.property, { name: "t" })

function hasI18nComment(node: Node): boolean {
return (
node.leadingComments &&
Expand Down Expand Up @@ -176,6 +182,17 @@ export default function ({ types: t }: { types: BabelTypes }): PluginObj {
// If there's no alias, consider it was imported as Trans.
localTransComponentName = importDeclarations["Trans"] || "Trans"
}

const coreImportDeclarations: Record<string, string> = {}
if (moduleName === "@lingui/core") {
node.specifiers.forEach((specifier) => {
specifier = specifier as ImportSpecifier
coreImportDeclarations[(specifier.imported as Identifier).name] =
specifier.local.name
})

localCoreI18nName = coreImportDeclarations["i18n"] || "i18n"
}
},

// Extract translation from <Trans /> component.
Expand Down Expand Up @@ -226,6 +243,25 @@ export default function ({ types: t }: { types: BabelTypes }): PluginObj {

const firstArgument = path.node.arguments[0]

let props: Record<string, unknown> = {}

if (
isI18nTMethod(path.node.callee) &&
t.isObjectExpression(firstArgument)
) {
props = {
...extractFromObjectExpression(t, firstArgument, ctx.file.hub, [
"id",
"message",
"comment",
"context",
]),
}

collectMessage(path, props, ctx)
return
}

// support `i18n._` calls written by users in form i18n._(id, variables, descriptor)
// without explicit annotation with comment
// calls generated by macro has a form i18n._(/*i18n*/ {descriptor}) and
Expand All @@ -234,7 +270,7 @@ export default function ({ types: t }: { types: BabelTypes }): PluginObj {
isI18nMethod(path.node.callee) && !firstArgument?.leadingComments
if (!hasComment && !isNonMacroI18n) return

let props: Record<string, unknown> = {
props = {
id: getTextFromExpression(
t,
firstArgument as Expression,
Expand Down Expand Up @@ -284,9 +320,7 @@ export default function ({ types: t }: { types: BabelTypes }): PluginObj {

// Extract message descriptors
ObjectExpression(path, ctx) {
if (!hasI18nComment(path.node)) {
return
}
if (!hasI18nComment(path.node)) return

const props = extractFromObjectExpression(t, path.node, ctx.file.hub, [
"id",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ exports[`@lingui/babel-plugin-extract-messages CallExpression i18n._() should ex
message: undefined,
origin: [
js-call-expression.js,
1,
2,
],
},
{
Expand All @@ -19,7 +19,7 @@ exports[`@lingui/babel-plugin-extract-messages CallExpression i18n._() should ex
message: undefined,
origin: [
js-call-expression.js,
3,
4,
],
},
{
Expand All @@ -29,7 +29,7 @@ exports[`@lingui/babel-plugin-extract-messages CallExpression i18n._() should ex
message: Message with id,
origin: [
js-call-expression.js,
5,
6,
],
},
{
Expand All @@ -39,7 +39,7 @@ exports[`@lingui/babel-plugin-extract-messages CallExpression i18n._() should ex
message: undefined,
origin: [
js-call-expression.js,
7,
8,
],
},
{
Expand All @@ -49,7 +49,22 @@ exports[`@lingui/babel-plugin-extract-messages CallExpression i18n._() should ex
message: undefined,
origin: [
js-call-expression.js,
9,
10,
],
},
]
`;

exports[`@lingui/babel-plugin-extract-messages CallExpression i18n._() should extract messages from i18n.t aliased expression 1`] = `
[
{
comment: Your comment,
context: undefined,
id: your.id,
message: Your Id Message,
origin: [
node-call-expression-aliased.js,
3,
],
},
]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

j4hr3n marked this conversation as resolved.
Show resolved Hide resolved
const msg = i18n._('Message')

const withDescription = i18n._('Description', {}, { comment: "description"});
Expand All @@ -8,3 +9,4 @@ const withValues = i18n._('Values {param}', { param: param });

const withContext = i18n._('Some id', {},{ context: 'Context1'});

const withTMessageDescriptor = i18n.t({ id: 'my.id', message: 'My Id Message', comment: 'My comment'});
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { i18n as lingui } from '@lingui/core';

const withAliasedTMessageDescriptor = lingui.t({ id: 'your.id', message: 'Your Id Message', comment: 'Your comment'});
7 changes: 7 additions & 0 deletions packages/babel-plugin-extract-messages/test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,13 @@ import { Trans } from "@lingui/react";
})
})

it("should extract messages from i18n.t aliased expression", () => {
expectNoConsole(() => {
const messages = transform("node-call-expression-aliased.js")
expect(messages).toMatchSnapshot()
})
})

it("Should not rise warning when translation from variable", () => {
const code = `
i18n._(message);
Expand Down
85 changes: 85 additions & 0 deletions packages/core/src/i18n.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,91 @@ 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}", values: { 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}", values: { name: "Fred" } })).toEqual(
"Missing Fred"
)
expect(
i18n.t({
id: "Missing with default",
message: "Missing {name}",
values: { 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({
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,12 @@ export class I18n extends EventEmitter<Events> {
)(values, formats)
}

// Alternative to _. Can be used in node/js without macros
// uses message descriptor only
t(descriptor: MessageDescriptor) {
return this._(descriptor)
}

date(value: string | Date, format?: Intl.DateTimeFormatOptions): string {
return date(this._locales || this._locale, value, format)
}
Expand Down
22 changes: 22 additions & 0 deletions website/docs/ref/core.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,28 @@ i18n._("My name is {name}", { name: "Tom" })
i18n._("msg.id", { name: "Tom" }, { message: "My name is {name}" })
```

### `i18n.t(messageDescriptor) (experimental)` {#i18n.t}

A small wrapper on the core translation meant for NodeJS/JS usage without macros. It uses the core `_` method, but currently only accepts message descriptor. This API is prone to breaking changes.

`messageDescriptor` is an object of message parameters.

```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}", values: { 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", values: { 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.
Expand Down