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 24, 2023
1 parent bc5f2a4 commit c0a9ef5
Show file tree
Hide file tree
Showing 6 changed files with 151 additions and 4 deletions.
30 changes: 26 additions & 4 deletions packages/babel-plugin-extract-messages/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,11 @@ export default function ({ types: t }: { types: BabelTypes }): PluginObj {
t.isIdentifier(node.object, { name: "i18n" }) &&
t.isIdentifier(node.property, { name: "_" })

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

function hasI18nComment(node: Node): boolean {
return (
node.leadingComments &&
Expand Down Expand Up @@ -226,6 +231,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 +258,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 +308,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 @@ -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,
],
},
]
`;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ 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'});
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 c0a9ef5

Please sign in to comment.