diff --git a/examples/nextjs-swc/src/components/Switcher.tsx b/examples/nextjs-swc/src/components/Switcher.tsx index 2b43020ba..b1c345ffe 100644 --- a/examples/nextjs-swc/src/components/Switcher.tsx +++ b/examples/nextjs-swc/src/components/Switcher.tsx @@ -1,12 +1,14 @@ import { useRouter } from 'next/router' import { useState } from 'react' import { t, msg } from '@lingui/macro' -import { MessageDescriptor } from '@lingui/core/src' +import { I18nTDescriptorByMessage, I18nTDescriptorById } from '@lingui/core/src' import { useLingui } from '@lingui/react' type LOCALES = 'en' | 'sr' | 'es' | 'pseudo' -const languages: { [key: string]: MessageDescriptor } = { +const languages: { + [key: string]: I18nTDescriptorByMessage | I18nTDescriptorById +} = { en: msg`English`, sr: msg`Serbian`, es: msg`Spanish` diff --git a/packages/core/__typetests__/index.test-d.tsx b/packages/core/__typetests__/index.test-d.tsx index 93b59a2c3..36180a18b 100644 --- a/packages/core/__typetests__/index.test-d.tsx +++ b/packages/core/__typetests__/index.test-d.tsx @@ -2,7 +2,77 @@ import { expectType } from "tsd" import { i18n } from "@lingui/core" +// @ts-expect-error at least one parameter should be presented +expectType(i18n._()) expectType(i18n._("message.id")) +expectType(i18n._("Hello, '{name}'")) +expectType(i18n._("Hello, '{name}' '{lastName}'")) +expectType(i18n._("Hello, '{name}'", {})) +expectType( + i18n._("Hello, {lastName} '{name}', you have {count} messages", { + count: "1", + lastName: "LastName", + }) +) +expectType(i18n._("Hello, } { ")) +expectType(i18n._("Hello, {{{vvv} ", {})) +expectType(i18n._("Hello, {}", { "": "Name" })) +expectType(i18n._("Hello, {name}", { name: "Name" })) +// @ts-expect-error cannot call without a parameter object +expectType(i18n._("Hello, {name}")) +i18n._("You have {n, number} unread messages", { n: 10 }) +// @ts-expect-error n should be of type "number" +i18n._("You have {n, number} unread messages", { n: "hello" }) +// @ts-expect-error cannot call with incorrect parameters object +expectType(i18n._("Hello {name}", { username: "Name" })) +expectType( + // @ts-expect-error cannot call with incorrect parameters object + i18n._("Hello {name}", { name: "Name", lastName: "Slivaev" }) +) +expectType( + i18n._("Hello {name}, you have {count} messages", { + name: "Dmitry", + count: "5", + }) +) +expectType( + // @ts-expect-error cannot call with incorrect parameters object for formatter + i18n._("{numBooks, plural, one {# book} other {# books}}", {}) +) + +expectType( + i18n._( + ` +{username} have { + numBooks, plural, + one { + { + numArticles, plural, + one { + 1 book and 1 article + } other { + 1 book and {numArticles, number} articles, good job, {name} ! + } + } + } other { + {numArticles, plural, + one { + {numBooks, number} books and 1 article + } other { + numBooks, number} books and {numArticles, number} articles + } + } + } +}. Wish you a good {what}`, + { + numBooks: 1, + numArticles: 1, + username: "username", + name: "name", + what: "luck", + } + ) +) expectType( i18n._({ id: "message.id", @@ -16,6 +86,15 @@ expectType( { message: "Hello {name}", comment: "", formats: {} } ) ) + +expectType( + // @ts-expect-error cannot call with incorrect parameters object + i18n._( + "message.id", + { username: "Tim" }, + { message: "Hello {name}", comment: "", formats: {} } + ) +) expectType( i18n._( // @ts-expect-error you could not use message descriptor together with rest of params @@ -36,6 +115,62 @@ expectType( }) ) +// @ts-expect-error id or message should be presented +expectType(i18n.t({})) +expectType(i18n.t({ id: "Hello" })) +expectType(i18n.t({ id: "Hello", message: "Hello" })) + +expectType( + i18n.t({ + id: "Hello {name}", + values: { + name: "Name", + }, + }) +) + +expectType( + i18n.t({ + id: "message.id", + message: "Hello {name}", + values: { + name: "Name", + }, + }) +) + +expectType( + // @ts-expect-error cannot call with incorrect parameters object + i18n.t({ + id: "Hello {name}", + values: { + username: "Name", + }, + }) +) + +expectType( + // @ts-expect-error cannot call with incorrect parameters object + i18n.t({ + id: "message.id", + message: "Hello {name}", + values: { + username: "Name", + }, + }) +) + +expectType( + // @ts-expect-error if "message" is presented – values should be typed according to messages instead of id + i18n.t({ + id: "Hello, {username}", + message: "Hello {name}", + values: { + username: "Name", + }, + }) +) + expectType( i18n.t( "message.id", diff --git a/packages/core/package.json b/packages/core/package.json index 6d5974c6a..c040e8009 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -55,6 +55,7 @@ "dependencies": { "@babel/runtime": "^7.20.13", "@lingui/message-utils": "4.7.0", + "type-fest": "^4.9.0", "unraw": "^3.0.0" }, "devDependencies": { diff --git a/packages/core/src/formats.ts b/packages/core/src/formats.ts index 9aac7f9a5..32aab35de 100644 --- a/packages/core/src/formats.ts +++ b/packages/core/src/formats.ts @@ -1,5 +1,6 @@ import { isString } from "./essentials" import { Locales } from "./i18n" +import { PluralFormatterOptions } from "./formatter" /** Memoized cache */ const cache = new Map() @@ -40,15 +41,11 @@ export function number( return formatter.format(value) } -export type PluralOptions = { [key: string]: Intl.LDMLPluralRule } & { - offset: number - other: string -} export function plural( locales: Locales, ordinal: boolean, value: number, - { offset = 0, ...rules }: PluralOptions + { offset = 0, ...rules }: PluralFormatterOptions ): string { const _locales = normalizeLocales(locales) diff --git a/packages/core/src/formatter.ts b/packages/core/src/formatter.ts new file mode 100644 index 000000000..6c612d6fd --- /dev/null +++ b/packages/core/src/formatter.ts @@ -0,0 +1,41 @@ +export type Formats = Record< + string, + Intl.DateTimeFormatOptions | Intl.NumberFormatOptions +> +export type PluralFormatterOptions = { [key: string]: Intl.LDMLPluralRule } & { + offset: number + other: string +} +export type PluralFormatter = ( + value: number, + cases: PluralFormatterOptions +) => string +export type SelectOrdinalFormatter = ( + value: number, + cases: PluralFormatterOptions +) => string + +export type SelectFormatter = ( + value: string, + rules: Record +) => string + +export type NumberFormatter = ( + value: number, + format: string | Intl.NumberFormatOptions +) => string + +export type DateFormatter = ( + value: string, + format: string | Intl.DateTimeFormatOptions +) => string + +export type UndefinedFormatter = (value: unknown) => unknown +export type FormatterMap = { + plural: PluralFormatter + selectordinal: SelectOrdinalFormatter + select: SelectFormatter + number: NumberFormatter + date: DateFormatter + undefined: UndefinedFormatter +} diff --git a/packages/core/src/i18n.t.ts b/packages/core/src/i18n.t.ts new file mode 100644 index 000000000..e28efe57c --- /dev/null +++ b/packages/core/src/i18n.t.ts @@ -0,0 +1,148 @@ +import { Replace, Simplify, Trim, UnionToIntersection } from "type-fest" +import { Formats, FormatterMap } from "./formatter" + +type DropEscapedBraces = Replace< + Replace, + `}'`, + "", + { all: true } +> + +type ExtractNextBrace< + T extends string, + Acc extends string = "" +> = T extends `${infer Head}${infer Tail}` + ? Head extends "{" | "}" + ? [Acc, Head, Tail] + : ExtractNextBrace + : never + +/** + * Takes a string literal and recursively extracts the content inside curly braces. + * It expects that opening curly brace is omitted. + * @example `name}` -> 'name' + * @example `plural, one {just one} other {many}} and much more` -> 'plural, one {just one} other {many}' + */ +type ExtractBraceBody< + Input extends string, // The string to be processed. + OpenedBraceStack extends "{"[] = [], // A stack to keep track of opened curly braces. + Body extends string = "" // Accumulator to build up the content inside the braces. +> = string extends Input // Check if Input is a wide 'string' type rather than a string literal. + ? string // If it is, return the wide 'string' type. + : Input extends "" // Check if the input string is empty. + ? [Body, ""] // If it is, return the accumulated Body and an empty string as a tuple. + : ExtractNextBrace extends [ + // Use ExtractNextBrace to get the next brace and the parts of the string before and after it. + infer Before extends string, + infer Brace, + infer After extends string + ] + ? Brace extends "{" // Check if the extracted brace is an opening brace '{'. + ? ExtractBraceBody< + // If it is, recursively call ExtractBraceBody... + After, // ...with the rest of the string... + ["{", ...OpenedBraceStack], // ...pushing '{' onto the stack... + `${Body}${Before}${Brace}` // ...and appending the Before part and the brace to the Body. + > + : // Otherwise, if Brace is not an opening '{' but a closing one '}' + OpenedBraceStack extends ["{", ...infer Rest extends "{"[]] // Check if there are any unmatched opening braces in the stack. + ? ExtractBraceBody< + // If there are, recursively call ExtractBraceBody... + After, // ...with the rest of the string... + Rest, // ...popping the last '{' from the stack... + `${Body}${Before}}` // ...and appending the Before part and the closing brace '}' to the Body. + > + : // If the brace is a closing brace '}' and there are no unmatched opening braces... + [`${Body}${Before}`, After] // ...return the content inside the braces (Body + Before) and the rest of the string (After) as a tuple. + : never // If the string does not match the expected pattern, return 'never'. + +type ExtractFormatterMessages = string extends Input + ? string[] + : Input extends "" + ? [] + : Input extends `${string}{${infer Tail}` + ? ExtractBraceBody extends [ + infer BraceBody extends string, + infer Next extends string + ] + ? [BraceBody, ...ExtractFormatterMessages] + : [] + : [] + +type ExtractFormatterId = + Input extends `${infer FormatterId},${string}` + ? Trim + : Trim + +type IsExistedFormatter = + FormatterId extends keyof FormatterMap ? FormatterId : never + +type FormatterInputType = + FormatterMap[IsExistedFormatter] extends infer FormatterFn + ? FormatterFn extends (input: infer InputType, ...args: any[]) => any + ? InputType + : never + : never + +/** + * Takes a string literal and extracts interpolation variables from it. + * @example `My name is {name}, i'm from {city}` -> {name: string} | {city: string} + * @return union of objects with one property each + */ +type ExtractVars = string extends Input // Check if the type of Input is the general 'string' type and not a string literal. + ? Record // If it is, return a wide type Record to represent any object. + : // Start processing the literal string. + Input extends `${string}{${infer Tail}` // Check if the Input string contains '{', indicating the start of an expression. + ? // Extract the content inside the curly braces and the rest of the string after the closing curly brace. + ExtractBraceBody extends [ + infer BraceBody extends string, + infer Next extends string + ] + ? BraceBody extends `${infer FormatterInput},${infer FormatterTail}` // Check if the content inside the braces is a formatter. + ? // Process the formatter content. + | { + // Create a type with a property where the key is trimmed FormatterInput (which is var name in this case), and the value is determined by the formatter's type. + [P in Trim]: FormatterInputType< + ExtractFormatterId + > + } + | ExtractVars[number]> // Recursively process messages inside the formatter. + | ExtractVars // Recursively process the rest of the string after the current variable. + : // If not a formatter, create a type with a property where the key is the whole trimmed BraceBody and the value is a string + { [P in Trim]: string } | ExtractVars // and recursively process the rest of the string. + : {} // If the string does not contain a valid structure, return an empty object type (possibly we can return an error here, because this branch indicates parsing error) + : {} // If the Input string does not contain '{', return an empty object type. + +export type I18nTValues = Simplify< + UnionToIntersection>> +> + +type I18nTDescriptorBasic = { + comment?: string +} + +export type I18nTDescriptorById = I18nTDescriptorBasic & + ({} extends I18nTValues + ? { id: Message } + : { id: Message; values: I18nTValues }) + +export type I18nTDescriptorByMessage = ({ + id: string +} & I18nTDescriptorBasic) & + ({} extends I18nTValues + ? { message: Message } + : { message: Message; values: I18nTValues }) + +export type I18nTOptions = { + formats?: Formats + comment?: string +} + +export type I18nTOptionsWithMessage = { + message: Message +} & I18nTOptions + +export type I18nTMessageWithNoParams = + DropEscapedBraces extends `${string}{${string}}${string}` + ? never + : Message diff --git a/packages/core/src/i18n.test.ts b/packages/core/src/i18n.test.ts index 5deccfe63..db3616cc0 100644 --- a/packages/core/src/i18n.test.ts +++ b/packages/core/src/i18n.test.ts @@ -206,6 +206,7 @@ describe("I18n", () => { expect(i18n.t("Hello")).toEqual("Salut") // missing { name } + // @ts-expect-error expect(i18n._("My name is {name}")).toEqual("Je m'appelle") // Untranslated message diff --git a/packages/core/src/i18n.ts b/packages/core/src/i18n.ts index 55250958f..3ca4ee68f 100644 --- a/packages/core/src/i18n.ts +++ b/packages/core/src/i18n.ts @@ -1,23 +1,22 @@ import { interpolate, UNICODE_REGEX } from "./interpolate" -import { isString, isFunction } from "./essentials" +import { isFunction, isString } from "./essentials" import { date, defaultLocale, number } from "./formats" import { EventEmitter } from "./eventEmitter" -import { compileMessage } from "@lingui/message-utils/compileMessage" import type { CompiledMessage } from "@lingui/message-utils/compileMessage" - -export type MessageOptions = { - message?: string - formats?: Formats - comment?: string -} +import { compileMessage } from "@lingui/message-utils/compileMessage" +import { + I18nTDescriptorById, + I18nTDescriptorByMessage, + I18nTMessageWithNoParams, + I18nTOptions, + I18nTOptionsWithMessage, + I18nTValues, +} from "./i18n.t" +import { Formats } from "./formatter" export type { CompiledMessage } export type Locale = string export type Locales = Locale | Locale[] -export type Formats = Record< - string, - Intl.DateTimeFormatOptions | Intl.NumberFormatOptions -> export type Values = Record @@ -40,13 +39,6 @@ export type Messages = Record export type AllMessages = Record -export type MessageDescriptor = { - id: string - comment?: string - message?: string - values?: Record -} - export type MissingMessageEvent = { locale: Locale id: string @@ -80,10 +72,10 @@ type LoadAndActivateOptions = { } export class I18n extends EventEmitter { - private _locale: Locale = "" - private _locales?: Locales - private _localeData: AllLocaleData = {} - private _messages: AllMessages = {} + /** + * Alias for {@see I18n._} + */ + t: I18n["_"] = this._.bind(this) private _missing?: MissingHandler constructor(params: setupI18nProps) { @@ -97,17 +89,19 @@ export class I18n extends EventEmitter { } } + private _locale: Locale = "" + get locale() { return this._locale } + private _locales?: Locales + get locales() { return this._locales } - get messages(): Messages { - return this._messages[this._locale] ?? {} - } + private _localeData: AllLocaleData = {} /** * @deprecated this has no effect. Please remove this from the code. Deprecated in v4 @@ -116,13 +110,10 @@ export class I18n extends EventEmitter { return this._localeData[this._locale] ?? {} } - private _loadLocaleData(locale: Locale, localeData: LocaleData) { - const maybeLocaleData = this._localeData[locale] - if (!maybeLocaleData) { - this._localeData[locale] = localeData - } else { - Object.assign(maybeLocaleData, localeData) - } + private _messages: AllMessages = {} + + get messages(): Messages { + return this._messages[this._locale] ?? {} } /** @@ -153,17 +144,10 @@ export class I18n extends EventEmitter { this.emit("change") } - private _load(locale: Locale, messages: Messages) { - const maybeMessages = this._messages[locale] - if (!maybeMessages) { - this._messages[locale] = messages - } else { - Object.assign(maybeMessages, messages) - } - } - load(allMessages: AllMessages): void + load(locale: Locale, messages: Messages): void + load(localeOrMessages: AllMessages | Locale, messages?: Messages): void { if (typeof localeOrMessages == "string" && typeof messages === "object") { // load('en', catalog) @@ -205,17 +189,39 @@ export class I18n extends EventEmitter { } // method for translation and formatting - _(descriptor: MessageDescriptor): string - _(id: string, values?: Values, options?: MessageOptions): string + _( + _: + | I18nTMessageWithNoParams + | I18nTDescriptorByMessage + | I18nTDescriptorById + ): string + _( + _: string, + values: I18nTValues, + options: I18nTOptionsWithMessage + ): string + _( + _: Message, + values: I18nTValues, + options?: I18nTOptions + ): string _( - id: MessageDescriptor | string, + id: + | { id: string; message?: string; values?: Values; comment?: string } + | string, values?: Values, - options?: MessageOptions + options?: { + formats?: Formats + comment?: string + message?: string + } ): string { let message = options?.message if (!isString(id)) { values = id.values || values - message = id.message + if ("message" in id) { + message = id.message + } id = id.id } @@ -252,11 +258,6 @@ export class I18n extends EventEmitter { )(values, options?.formats) } - /** - * Alias for {@see I18n._} - */ - t: I18n["_"] = this._.bind(this) - date(value: string | Date, format?: Intl.DateTimeFormatOptions): string { return date(this._locales || this._locale, value, format) } @@ -264,6 +265,24 @@ export class I18n extends EventEmitter { number(value: number, format?: Intl.NumberFormatOptions): string { return number(this._locales || this._locale, value, format) } + + private _loadLocaleData(locale: Locale, localeData: LocaleData) { + const maybeLocaleData = this._localeData[locale] + if (!maybeLocaleData) { + this._localeData[locale] = localeData + } else { + Object.assign(maybeLocaleData, localeData) + } + } + + private _load(locale: Locale, messages: Messages) { + const maybeMessages = this._messages[locale] + if (!maybeMessages) { + this._messages[locale] = messages + } else { + Object.assign(maybeMessages, messages) + } + } } function setupI18n(params: setupI18nProps = {}): I18n { diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index a2261e833..7dc5f3ab9 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,14 +1,14 @@ export { setupI18n, I18n } from "./i18n" +export * from "./i18n.t" + export type { AllMessages, - MessageDescriptor, Messages, AllLocaleData, LocaleData, Locale, Locales, - MessageOptions, } from "./i18n" // Default i18n object diff --git a/packages/core/src/interpolate.ts b/packages/core/src/interpolate.ts index cc6318537..b675e6ad3 100644 --- a/packages/core/src/interpolate.ts +++ b/packages/core/src/interpolate.ts @@ -1,7 +1,8 @@ -import { CompiledMessage, Formats, Locales, Values } from "./i18n" -import { date, number, plural, type PluralOptions } from "./formats" +import { CompiledMessage, Locales, Values } from "./i18n" +import { date, number, plural } from "./formats" import { isString } from "./essentials" import { unraw } from "unraw" +import { Formats, FormatterMap, PluralFormatterOptions } from "./formatter" export const UNICODE_REGEX = /\\u[a-fA-F0-9]{4}|\\x[a-fA-F0-9]{2}/g @@ -9,7 +10,7 @@ const getDefaultFormats = ( locale: string, passedLocales?: Locales, formats: Formats = {} -) => { +): FormatterMap => { const locales = passedLocales || locale const style = (format: string | T): T => { @@ -26,14 +27,14 @@ const getDefaultFormats = ( } return { - plural: (value: number, cases: PluralOptions) => { + plural: (value: number, cases: PluralFormatterOptions) => { const { offset = 0 } = cases const message = plural(locales, false, value, cases) return replaceOctothorpe(value - offset, message) }, - selectordinal: (value: number, cases: PluralOptions) => { + selectordinal: (value: number, cases: PluralFormatterOptions) => { const { offset = 0 } = cases const message = plural(locales, true, value, cases) @@ -53,7 +54,7 @@ const getDefaultFormats = ( ): string => date(locales, value, style(format)), undefined: undefinedFormatter, - } as const + } } const selectFormatter = (value: string, rules: Record) => diff --git a/packages/macro/__typetests__/index.test-d.tsx b/packages/macro/__typetests__/index.test-d.tsx index 771507296..c6f516a43 100644 --- a/packages/macro/__typetests__/index.test-d.tsx +++ b/packages/macro/__typetests__/index.test-d.tsx @@ -1,5 +1,5 @@ import { expectType } from "tsd" -import type { MessageDescriptor, I18n } from "@lingui/core" +import type { I18nTDescriptorByMessage, I18n } from "@lingui/core" import { t, @@ -49,8 +49,8 @@ t({ comment: "", context: "" }) // @ts-expect-error id or message should be presented t({}) -// @ts-expect-error `values` is invalid field for macro message descriptor t({ + // @ts-expect-error `values` is invalid field for macro message descriptor id: "custom.id", comment: "Hello", context: "context", @@ -69,7 +69,7 @@ expectType( }) ) -expectType( +expectType>( defineMessage({ id: "custom.id", comment: "Hello", @@ -77,7 +77,7 @@ expectType( message: "Hello world", }) ) -expectType( +expectType>( msg({ id: "custom.id", comment: "Hello", @@ -85,11 +85,11 @@ expectType( message: "Hello world", }) ) -expectType(defineMessage`Message`) -expectType(msg`Message`) +expectType>(defineMessage`Message`) +expectType>(msg`Message`) // @ts-expect-error id or message should be presented -expectType(defineMessage({})) +expectType>(defineMessage({})) /////////////////// //// Plural ////// diff --git a/packages/macro/global.d.ts b/packages/macro/global.d.ts index 3473c7249..bf2da5817 100644 --- a/packages/macro/global.d.ts +++ b/packages/macro/global.d.ts @@ -2,22 +2,28 @@ // https://github.com/lingui/js-lingui/issues/936 // @ts-ignore declare module "@lingui/macro" { - import type { MessageDescriptor, I18n } from "@lingui/core" - - type MacroMessageDescriptor = ( - | { - id: string - message?: string - } - | { - id?: string - message: string - } - ) & { + import type { + I18n, + I18nTDescriptorById, + I18nTDescriptorByMessage, + } from "@lingui/core" + + type MacroMessageDescriptorBasics = { comment?: string context?: string } + type MacroMessageDescriptorWithIdAsMessage = + MacroMessageDescriptorBasics & { + id: Message + } + + type MacroMessageDescriptorWithMessageAsMessage = + MacroMessageDescriptorBasics & { + id?: string + message: Message + } + export type BasicType = { id?: string comment?: string @@ -47,7 +53,12 @@ declare module "@lingui/macro" { * * @param descriptor The message descriptor to translate */ - export function t(descriptor: MacroMessageDescriptor): string + export function t( + descriptor: MacroMessageDescriptorWithMessageAsMessage + ): string + export function t( + descriptor: MacroMessageDescriptorWithIdAsMessage + ): string /** * Translates a template string using the global I18n instance @@ -90,7 +101,12 @@ declare module "@lingui/macro" { */ export function t(i18n: I18n): { (literals: TemplateStringsArray, ...placeholders: any[]): string - (descriptor: MacroMessageDescriptor): string + ( + descriptor: MacroMessageDescriptorWithMessageAsMessage + ): string + ( + descriptor: MacroMessageDescriptorWithIdAsMessage + ): string } export type UnderscoreDigit = { [digit: string]: T } @@ -190,9 +206,12 @@ declare module "@lingui/macro" { * * @param descriptor The message descriptor */ - export function defineMessage( - descriptor: MacroMessageDescriptor - ): MessageDescriptor + export function defineMessage( + descriptor: MacroMessageDescriptorWithMessageAsMessage + ): I18nTDescriptorByMessage + export function defineMessage( + descriptor: MacroMessageDescriptorWithIdAsMessage + ): I18nTDescriptorById export type ChoiceProps = { value?: string | number diff --git a/packages/macro/index.d.ts b/packages/macro/index.d.ts index dca81ea01..777d96ee9 100644 --- a/packages/macro/index.d.ts +++ b/packages/macro/index.d.ts @@ -1,6 +1,10 @@ // eslint-disable-next-line import/no-extraneous-dependencies import type { ReactNode, VFC, FC } from "react" -import type { I18n, MessageDescriptor } from "@lingui/core" +import type { + I18n, + I18nTDescriptorById, + I18nTDescriptorByMessage, +} from "@lingui/core" import type { TransRenderCallbackOrComponent } from "@lingui/react" export type ChoiceOptions = { @@ -18,20 +22,21 @@ export type ChoiceOptions = { [digit: `${number}`]: string } -type MacroMessageDescriptor = ( - | { - id: string - message?: string - } - | { - id?: string - message: string - } -) & { +type MacroTDescriptorBase = { comment?: string context?: string } +type MacroTDescriptorById = MacroTDescriptorBase & { + id: Message +} + +type MacroTDescriptorByMessage = + MacroTDescriptorBase & { + id?: string + message: Message + } + /** * Translates a message descriptor * @@ -56,7 +61,12 @@ type MacroMessageDescriptor = ( * * @param descriptor The message descriptor to translate */ -export function t(descriptor: MacroMessageDescriptor): string +export function t( + descriptor: MacroTDescriptorByMessage +): string +export function t( + descriptor: MacroTDescriptorById +): string /** * Translates a template string using the global I18n instance @@ -99,7 +109,10 @@ export function t( */ export function t(i18n: I18n): { (literals: TemplateStringsArray, ...placeholders: any[]): string - (descriptor: MacroMessageDescriptor): string + ( + descriptor: MacroTDescriptorByMessage + ): string + (descriptor: MacroTDescriptorById): string } /** @@ -189,9 +202,12 @@ export function select(value: string, choices: SelectOptions): string * * @param descriptor The message descriptor */ -export function defineMessage( - descriptor: MacroMessageDescriptor -): MessageDescriptor +export function defineMessage( + descriptor: MacroTDescriptorByMessage +): I18nTDescriptorByMessage +export function defineMessage( + descriptor: MacroTDescriptorById +): I18nTDescriptorById /** * Define a message for later use @@ -208,7 +224,7 @@ export function defineMessage( export function defineMessage( literals: TemplateStringsArray, ...placeholders: any[] -): MessageDescriptor +): I18nTDescriptorByMessage /** * Define a message for later use diff --git a/packages/react/src/TransNoContext.tsx b/packages/react/src/TransNoContext.tsx index 4eed28db9..3e8dcecc2 100644 --- a/packages/react/src/TransNoContext.tsx +++ b/packages/react/src/TransNoContext.tsx @@ -1,7 +1,7 @@ import React, { ComponentType } from "react" import { formatElements } from "./format" -import type { MessageOptions } from "@lingui/core" +import type { I18nTOptions } from "@lingui/core" import { I18n } from "@lingui/core" export type TransRenderProps = { @@ -32,7 +32,7 @@ export type TransProps = { message?: string values?: Record components?: { [key: string]: React.ElementType | any } - formats?: MessageOptions["formats"] + formats?: I18nTOptions["formats"] comment?: string children?: React.ReactNode } & TransRenderCallbackOrComponent @@ -90,7 +90,7 @@ export function TransNoContext( const _translation: string = i18n && typeof i18n._ === "function" - ? i18n._(id, values, { message, formats }) + ? i18n._(id, values, { message, formats } as any) : id // i18n provider isn't loaded at all const translation = _translation diff --git a/yarn.lock b/yarn.lock index f0ef67f2e..0d05779e4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3299,6 +3299,7 @@ __metadata: "@babel/runtime": ^7.20.13 "@lingui/jest-mocks": "*" "@lingui/message-utils": 4.7.0 + type-fest: ^4.9.0 unbuild: 2.0.0 unraw: ^3.0.0 languageName: unknown @@ -15125,6 +15126,13 @@ __metadata: languageName: node linkType: hard +"type-fest@npm:^4.9.0": + version: 4.9.0 + resolution: "type-fest@npm:4.9.0" + checksum: 73383de23237b399a70397a53101152548846d919aebcc7d8733000c6c354dc2632fe37c4a70b8571b79fdbfa099e2d8304c5ac56b3254780acff93e4c7a797f + languageName: node + linkType: hard + "typed-array-length@npm:^1.0.4": version: 1.0.4 resolution: "typed-array-length@npm:1.0.4"