Skip to content

Commit

Permalink
feat(core): add new core api for node/js usage without macros
Browse files Browse the repository at this point in the history
  • Loading branch information
Christoffer Jahren committed Mar 27, 2023
1 parent bc5f2a4 commit cf60285
Show file tree
Hide file tree
Showing 8 changed files with 190 additions and 9 deletions.
46 changes: 42 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,15 @@ export default function ({ types: t }: { types: BabelTypes }): PluginObj {
t.isIdentifier(node.object, { name: "i18n" }) &&
t.isIdentifier(node.property, { name: "_" })

function isNodeJSI18nMethod(node: Node) {
console.log("core", { localCoreI18nName })
return (
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 +186,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 +247,25 @@ export default function ({ types: t }: { types: BabelTypes }): PluginObj {

const firstArgument = path.node.arguments[0]

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

if (
isNodeJSI18nMethod(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 +274,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 +324,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 @@

const msg = i18n._('Message')

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

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

const withNodeMessageDescriptor = 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 withAliasedNodeMessageDescriptor = 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
84 changes: 84 additions & 0 deletions packages/core/src/i18n.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
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 @@ -262,6 +262,12 @@ export class I18n extends EventEmitter<Events> {
)(values, formats)
}

// Alias for _ to be used in node/js without macro setting
// uses message descriptors only
t(id: MessageDescriptor, values: Values | undefined = {}) {
return this._(id, values, {})
}

date(value: string | Date, format?: Intl.DateTimeFormatOptions): string {
return date(this._locales || this._locale, value, format)
}
Expand Down
24 changes: 24 additions & 0 deletions website/docs/ref/core.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down

0 comments on commit cf60285

Please sign in to comment.