Skip to content
Closed
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
22 changes: 21 additions & 1 deletion packages/core/__typetests__/index.tst.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import { i18n } from "@lingui/core"
import { i18n } from "../src"
import { expect } from "tstyche"

const supportedValues = {
deadline: new Date(),
total: BigInt(1),
active: false,
optional: null,
missing: undefined,
}

expect(i18n._("message.id")).type.toBe<string>()
expect(
i18n._({
Expand All @@ -15,6 +23,18 @@ expect(
{ message: "Hello {name}", comment: "", formats: {} },
),
).type.toBe<string>()
expect(i18n._).type.toBeCallableWith("message.id", supportedValues)
expect(i18n._).type.not.toBeCallableWith("message.id", {
payload: { nested: true },
})
expect(i18n._).type.toBeCallableWith({
id: "message.id",
values: supportedValues,
})
expect(i18n._).type.not.toBeCallableWith({
id: "message.id",
values: { payload: { nested: true } },
})
expect(i18n._).type.not.toBeCallableWith(
// cannot use message descriptor together with rest of params
{
Expand Down
7 changes: 7 additions & 0 deletions packages/core/macro/__typetests__/index.tst.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ const name = "Jack"
const user = { name: "Jack" }
const i18n: I18n = null as any
const numberValue = 2
const bigintValue = BigInt(1)
const deadline = new Date()

// simple
expect(t`Hello world`).type.toBe<string>()
Expand All @@ -28,6 +30,9 @@ expect(t`Hello ${ph({ name: user.name })}`).type.toBe<string>()

// allow numbers
expect(t`Hello ${numberValue}`).type.toBe<string>()
expect(
t`Hello ${bigintValue} ${deadline} ${{ active: false }}`,
).type.toBe<string>()

// with custom i18n
expect(t(i18n)`With custom i18n instance`).type.toBe<string>()
Expand Down Expand Up @@ -98,6 +103,8 @@ expect(
// allow numbers
expect(msg`Hello ${numberValue}`).type.toBe<MessageDescriptor>()
expect(defineMessage`Hello ${numberValue}`).type.toBe<MessageDescriptor>()
expect(ph({ deadline })).type.toBe<string>()
expect(ph).type.not.toBeCallableWith({ payload: { nested: true } })

expect(
defineMessage({
Expand Down
24 changes: 16 additions & 8 deletions packages/core/macro/index.d.mts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import type { I18n, MessageDescriptor } from "@lingui/core"
import type {
I18n,
MessageDescriptor,
MessagePlaceholderValue,
} from "@lingui/core"

export type ChoiceOptions = {
/** Offset of value when calculating plural forms */
Expand Down Expand Up @@ -56,10 +60,11 @@ export type MacroMessageDescriptor = (
export function t(descriptor: MacroMessageDescriptor): string

export type LabeledExpression<T> = Record<string, T>
export type LabeledMessagePlaceholder =
LabeledExpression<MessagePlaceholderValue>
export type MessagePlaceholder =
| string
| number
| LabeledExpression<string | number>
| MessagePlaceholderValue
| LabeledMessagePlaceholder

/**
* Translates a template string using the global I18n instance
Expand Down Expand Up @@ -109,6 +114,9 @@ export function t(i18n: I18n): {
(descriptor: MacroMessageDescriptor): string
}

export type PluralValue = number | string | LabeledExpression<number | string>
export type SelectValue = string | LabeledExpression<string>

/**
* Pluralize a message
*
Expand All @@ -125,7 +133,7 @@ export function t(i18n: I18n): {
* @param options Object with available plural forms
*/
export function plural(
value: number | string | LabeledExpression<number | string>,
value: PluralValue,
options: ChoiceOptions
): string

Expand All @@ -150,7 +158,7 @@ export function plural(
* @param options Object with available plural forms
*/
export function selectOrdinal(
value: number | string | LabeledExpression<number | string>,
value: PluralValue,
options: ChoiceOptions
): string

Expand Down Expand Up @@ -181,7 +189,7 @@ type SelectOptions = {
* @param choices
*/
export function select(
value: string | LabeledExpression<string>,
value: SelectValue,
choices: SelectOptions
): string

Expand Down Expand Up @@ -232,4 +240,4 @@ export const msg: typeof defineMessage
/**
* Helps to define a name for a variable in the message
*/
export function ph(def: LabeledExpression<string | number>): string
export function ph(def: LabeledMessagePlaceholder): string
13 changes: 11 additions & 2 deletions packages/core/src/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,16 @@ export type Formats = Record<
Intl.DateTimeFormatOptions | Intl.NumberFormatOptions
>

export type Values = Record<string, unknown>
export type MessagePlaceholderValue =
| string
| number
| bigint
| boolean
| null
| undefined
| Date

export type Values = Record<string, MessagePlaceholderValue>

export type UncompiledMessage = string
export type Messages = Record<string, UncompiledMessage | CompiledMessage>
Expand All @@ -37,7 +46,7 @@ export type MessageDescriptor = {
id: string
comment?: string
message?: string
values?: Record<string, unknown>
values?: Values
}

export type MissingMessageEvent = {
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ export { setupI18n, I18n } from "./i18n"
export type {
AllMessages,
MessageDescriptor,
MessagePlaceholderValue,
Messages,
Locale,
Locales,
MessageOptions,
Values,
} from "./i18n"

// Default i18n object
Expand Down
20 changes: 20 additions & 0 deletions packages/react/__typetests__/index.tst.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { expect } from "tstyche"
import type { TransProps } from "../src"

type Values = NonNullable<TransProps["values"]>

expect({
name: "Tim",
count: 1,
total: BigInt(1),
active: false,
deadline: new Date(),
optional: null,
missing: undefined,
richName: <strong>Tim</strong>,
parts: ["Mr. ", <strong key="1">Tim</strong>],
}).type.toBeAssignableTo<Values>()

expect({
payload: { nested: true },
}).type.not.toBeAssignableTo<Values>()
12 changes: 12 additions & 0 deletions packages/react/macro/__typetests__/index.tst.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,15 @@ m = (
m = <Trans id="custom.id" comment="comment" context="context" />

m = <Trans>Hello {{ username: user.name }}</Trans>
m = <Trans>Hello {{ deadline: new Date() }}</Trans>
m = (
<Trans>
{
// @ts-expect-error: nested objects are not supported as interpolation values
{ payload: { nested: true } }
}
</Trans>
)

m = (
<Trans>
Expand Down Expand Up @@ -139,6 +148,9 @@ m = <Select value={gender} male="..." female=".." other={"..."} />

m = <Select value={{ gender }} _male="..." _female=".." other={"..."} />

// @ts-expect-error: labeled value for Select should be string only
m = <Select value={{ gender: 1 }} _male="..." _female=".." other={"..."} />

// should support JSX in props
m = (
<Select
Expand Down
17 changes: 11 additions & 6 deletions packages/react/macro/index.d.mts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import type { ReactNode } from "react"
import type { TransRenderCallbackOrComponent, I18nContext } from "@lingui/react"
import type {
TransRenderCallbackOrComponent,
I18nContext,
} from "@lingui/react"
import type {
MacroMessageDescriptor,
LabeledExpression,
LabeledMessagePlaceholder,
PluralValue,
SelectValue,
} from "@lingui/core/macro"

type CommonProps = TransRenderCallbackOrComponent & {
Expand All @@ -11,13 +16,13 @@ type CommonProps = TransRenderCallbackOrComponent & {
context?: string
}

type TransChildren = ReactNode | LabeledExpression<string | number>
type TransChildren = ReactNode | LabeledMessagePlaceholder
type TransProps = {
children: TransChildren | TransChildren[]
} & CommonProps

type PluralChoiceProps = {
value: string | number | LabeledExpression<string | number>
value: PluralValue
/** Offset of value when calculating plural forms */
offset?: number
zero?: ReactNode
Expand All @@ -33,7 +38,7 @@ type PluralChoiceProps = {
} & CommonProps

type SelectChoiceProps = {
value: string | LabeledExpression<string | number>
value: SelectValue
/** Catch-all option */
other: ReactNode
[option: `_${string}`]: ReactNode
Expand Down Expand Up @@ -135,4 +140,4 @@ export function useLingui(): Omit<I18nContext, "_"> & {
(descriptor: MacroMessageDescriptor): string
(literals: TemplateStringsArray, ...placeholders: any[]): string
}
}
}
58 changes: 58 additions & 0 deletions packages/react/src/Trans.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,64 @@ describe("Trans component", () => {
expect(translation).toEqual("1,00 €")
})

it("should preserve Date and bigint values for i18n interpolation", () => {
const deadline = new Date("2014-12-06T17:40:00.000Z")
const translation = html(
<Trans
id="msg.scalars"
message="It starts on {deadline}, count {total} <0>{name}</0>"
values={{
deadline,
total: BigInt(1),
name: "John",
}}
components={{
0: <span />,
}}
/>,
)

expect(translation).toEqual(
`It starts on ${deadline.toString()}, count 1 <span>John</span>`,
)
})

it.each([
["false", false],
["true", true],
["null", null],
["undefined", undefined],
] as const)(
"should omit %s values during Trans interpolation",
(_label, value) => {
const translation = html(
<Trans
id="msg.empty"
message="Value {value}"
values={{
value,
}}
/>,
)

expect(translation).toEqual("Value ")
},
)

it("should preserve __proto__ values for i18n interpolation", () => {
const translation = html(
<Trans
id="msg.proto"
message="Hello {__proto__}"
values={{
["__proto__"]: "John",
}}
/>,
)

expect(translation).toEqual("Hello John")
})

it("should render plural", () => {
const render = (count: number) =>
html(
Expand Down
Loading
Loading