Skip to content
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a89baef
feat(core/i18n/t): Precise typing for message parameters
dilame Jan 22, 2024
865dfad
feat(core/i18n/t): take into account escape characters
dilame Jan 22, 2024
2da35cf
feat(core/i18n/t): only allow omit values param object if message str…
dilame Jan 22, 2024
61e92d4
chore(core/i18n/t): use type-fest lib instead of manually declared ut…
dilame Jan 22, 2024
1777509
fix(core/i18n/t): trim interpolation parameter names
dilame Jan 22, 2024
ab1419f
refactor: message descriptor types for core and macro
dilame Jan 23, 2024
7f9e45d
test: add tests for new strict string interpolation typings
dilame Jan 23, 2024
fdd3fd8
refactor: rename type I18nT to I18nTValues
dilame Jan 23, 2024
01fa074
chore: yarn.lock type-fest
dilame Jan 23, 2024
a53d429
refactor: rename descriptor types
dilame Jan 23, 2024
1bf144f
chore: remove unnecessary eslint-ignore
dilame Jan 23, 2024
43edfb2
refactor: rename type I18nT to I18nTValues
dilame Jan 23, 2024
1726d7c
refactor: rename type _ExtractVars to ExtractVars
dilame Jan 23, 2024
a866f4f
refactor(core/i18n.t): use wide record type for values in case of wid…
dilame Jan 23, 2024
60bbaf8
test(core/i18n.t): make the most complex test case even more complex
dilame Jan 23, 2024
dfd9d8e
feat(core/i18n.t/values): add formatters strict typing support
dilame Jan 23, 2024
01d6608
test(core/i18n.t): formatter typings
dilame Jan 23, 2024
64c697b
fix(core/i18n.t/values): replace all escaped symbols instead of just …
dilame Jan 23, 2024
53d904b
test(core/i18n.t/values): ensure all escaped symbols in string are dr…
dilame Jan 23, 2024
9d29f2d
chore(core/i18n.t/values): remove unnecessary condition in type Extra…
dilame Jan 23, 2024
c8cf4d1
chore(core/i18n.t/values): write explanation comments at each line of…
dilame Jan 23, 2024
e42c01e
chore(core/i18n.t/values): write explanation comments at each line of…
dilame Jan 23, 2024
d8463d1
chore(core/i18n/t): combine excessive overloads into one signature
dilame Jan 26, 2024
3fc4562
chore(core/i18n/t): rename parameter 'id' to '_' in overloads
dilame Jan 26, 2024
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
1 change: 1 addition & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
108 changes: 108 additions & 0 deletions packages/core/src/i18n.t.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// Already listed, not sure what else to do
// eslint-disable-next-line import/no-extraneous-dependencies
import { Replace, Simplify, Trim, UnionToIntersection } from "type-fest"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you use webstorm and see warning from eslint from this rule that's probable because webstorm's eslint service should be restarted. It doesn't pick up changes in package.json automatically and continues to show error even if the package is already added. However this is not the case when you run eslint command directly.

import { Formats } from "./i18n"


type DropEscapedBraces<Input extends string> = Replace<Replace<Input, `'{`, ''>, `}'`, ''>;

type ExtractNextBrace<T extends string, Acc extends string = ""> = T extends `${infer Head}${infer Tail}` ?
Head extends "{" | "}" ? [Acc, Head, Tail] : ExtractNextBrace<Tail, `${Acc}${Head}`>
: never;

type ExtractBraceBody<Input extends string, OpenedBrackets extends "{"[] = [], Body extends string = ""> =
string extends Input
?
string
:
Input extends ""
?
[Body, ""]
:
ExtractNextBrace<Input> extends [infer Before extends string, infer Brace, infer Tail extends string]
?
Brace extends "{"
?
ExtractBraceBody<Tail, ["{", ...OpenedBrackets], `${Body}${Before}${Brace}`>
:
OpenedBrackets extends ["{", ...infer Rest extends "{"[]]
?
ExtractBraceBody<Tail, Rest, `${Body}${Before}}`>
:
[`${Body}${Before}`, Tail]
: never
;


type ExtractFormatterMessages<Input extends string> =
string extends Input
?
string[]
:
Input extends ""
?
[]
:
Input extends `${string}{${infer Tail}`
?
ExtractBraceBody<Tail> extends [infer BraceBody extends string, infer Next extends string]
?
[BraceBody, ...ExtractFormatterMessages<Next>]
:
[]
:
[]
;

type _ExtractVars<Input extends string> =
string extends Input
?
{}
:
Input extends ""
?
{}
:
Input extends `${string}{${infer Tail}`
? ExtractBraceBody<Tail> extends [infer BraceBody extends string, infer Next extends string]
?
BraceBody extends `${infer FormatterInput},${infer FormatterTail}`
?
{ [P in Trim<FormatterInput>]: string } & UnionToIntersection<_ExtractVars<ExtractFormatterMessages<FormatterTail>[number]>>
:
{ [P in Trim<BraceBody>]: string } & _ExtractVars<Next>
: {}
: {}
;

export type I18nT<Input extends string> = Simplify<_ExtractVars<DropEscapedBraces<Input>>>;

type MessageDescriptorWithIdAsMessage<Message extends string> =
{} extends I18nT<Message>
? { id: Message }
: { id: Message, values: I18nT<Message> };

type MessageDescriptorWithMessageAsMessage<Message extends string> =
({ id: string }) &
({} extends I18nT<Message>
? { message: Message }
: { message: Message, values: I18nT<Message> });

type MessageDescriptorRest = {
comment?: string
}

export type MessageDescriptor<Message extends string> =
(MessageDescriptorWithIdAsMessage<Message> | MessageDescriptorWithMessageAsMessage<Message>)
& MessageDescriptorRest

export type TFnOptions = {
formats?: Formats
comment?: string
}

export type TFnOptionsWithMessage<Message extends string> = {
message: Message
} & TFnOptions

export type MessageWithNoParams<Message extends string> = DropEscapedBraces<Message> extends `${string}{${string}}${string}` ? never : Message;
1 change: 1 addition & 0 deletions packages/core/src/i18n.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
95 changes: 51 additions & 44 deletions packages/core/src/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,8 @@ 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"
import { I18nT, MessageDescriptor, MessageWithNoParams, TFnOptions, TFnOptionsWithMessage } from "./i18n.t"

export type MessageOptions = {
message?: string
formats?: Formats
comment?: string
}

export type { CompiledMessage }
export type Locale = string
Expand Down Expand Up @@ -40,12 +36,6 @@ export type Messages = Record<string, CompiledMessage>

export type AllMessages = Record<Locale, Messages>

export type MessageDescriptor = {
id: string
comment?: string
message?: string
values?: Record<string, unknown>
}

export type MissingMessageEvent = {
locale: Locale
Expand Down Expand Up @@ -80,10 +70,10 @@ type LoadAndActivateOptions = {
}

export class I18n extends EventEmitter<Events> {
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) {
Expand All @@ -97,17 +87,19 @@ export class I18n extends EventEmitter<Events> {
}
}

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
Expand All @@ -116,13 +108,10 @@ export class I18n extends EventEmitter<Events> {
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] ?? {}
}

/**
Expand Down Expand Up @@ -153,17 +142,10 @@ export class I18n extends EventEmitter<Events> {
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)
Expand Down Expand Up @@ -205,17 +187,29 @@ export class I18n extends EventEmitter<Events> {
}

// method for translation and formatting
_(descriptor: MessageDescriptor): string
_(id: string, values?: Values, options?: MessageOptions): string
_<Message extends string>(id: MessageWithNoParams<Message>): string
_<Message extends string>(id: Message, values: I18nT<Message>, options?: TFnOptions): string
_<Message extends string>(id: string, values: I18nT<Message>, options: TFnOptionsWithMessage<Message>): string
_<Message extends string>(descriptor: MessageDescriptor<Message>): 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
}

Expand Down Expand Up @@ -252,18 +246,31 @@ export class I18n extends EventEmitter<Events> {
)(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)
}

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 {
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down
6 changes: 3 additions & 3 deletions packages/react/src/TransNoContext.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { ComponentType } from "react"

import { formatElements } from "./format"
import type { MessageOptions } from "@lingui/core"
import type { TFnOptions } from "@lingui/core"
import { I18n } from "@lingui/core"

export type TransRenderProps = {
Expand Down Expand Up @@ -32,7 +32,7 @@ export type TransProps = {
message?: string
values?: Record<string, unknown>
components?: { [key: string]: React.ElementType | any }
formats?: MessageOptions["formats"]
formats?: TFnOptions["formats"]
comment?: string
children?: React.ReactNode
} & TransRenderCallbackOrComponent
Expand Down Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -10678,6 +10679,7 @@ __metadata:
strip-ansi: ^6.0.1
swc-node: ^1.0.0
ts-jest: ^29.0.5
type-fest: ^4.9.0
typescript: ^4.9.5
languageName: unknown
linkType: soft
Expand Down Expand Up @@ -15125,6 +15127,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"
Expand Down