Skip to content
Open
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Lingui is an easy yet powerful internationalization (i18n) framework for global

- **Unopinionated** - Integrate Lingui into your existing workflow. It supports message keys as well as auto-generated messages. Translations are stored either in JSON or standard PO files, which are supported in almost all translation tools.

- **Lightweight and optimized** - Core library is less than [2 kB gzipped](https://bundlephobia.com/result?p=@lingui/core), React components are additional [1.4 kB gzipped](https://bundlephobia.com/result?p=@lingui/react).
- **Lightweight and optimized** - Core library is less than [2 kB gzipped](https://bundlephobia.com/result?p=@lingui/core), React components are additional [1.3 kB gzipped](https://bundlephobia.com/result?p=@lingui/react).

- **Active community** - Join the growing [community of developers](https://lingui.dev/community) who are using Lingui to build global products.

Expand Down
7 changes: 6 additions & 1 deletion babel.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ module.exports = {
},
],
"@babel/preset-typescript",
"@babel/preset-react",
[
"@babel/preset-react",
{
runtime: "automatic",
},
],
],
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@
},
{
"path": "./packages/react/dist/index.mjs",
"limit": "2 kB",
"limit": "1.5 kB",
"ignore": [
"react"
]
Expand Down
3 changes: 3 additions & 0 deletions packages/react/build.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,8 @@ export default defineBuildConfig({
}
},
},
esbuild: {
jsx: "automatic",
},
},
})
2 changes: 1 addition & 1 deletion packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@
"peerDependencies": {
"@lingui/babel-plugin-lingui-macro": "5.7.0",
"babel-plugin-macros": "2 || 3",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
Copy link
Collaborator

Choose a reason for hiding this comment

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

technically speaking, this is a breaking change. Nothing wrong with that. But I say.. let's bump this to 18 and stop caring about the old versions

Copy link
Contributor Author

@yslpn yslpn Dec 19, 2025

Choose a reason for hiding this comment

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

I agree with you. If we upgraded React, we could use useSyncExternalStore to sync react state with i18n state. It's a simpler and more performant option than using useEffect.

But I need other contributors' agreement that we're ready to upgrade React to version 18.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@andrii-bodnar @timofei-iatsenko

What do you think about raising the React version to 18?

"react": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@lingui/babel-plugin-lingui-macro": {
Expand Down
27 changes: 17 additions & 10 deletions packages/react/src/I18nProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,28 @@
import React, { ComponentType } from "react"
import {
createContext,
useCallback,
useContext,
useEffect,
useRef,
useState,
} from "react"
import type { I18n } from "@lingui/core"
import type { TransRenderProps } from "./TransNoContext"

export type I18nContext = {
i18n: I18n
_: I18n["_"]
defaultComponent?: ComponentType<TransRenderProps>
defaultComponent?: React.ComponentType<TransRenderProps>
}

export type I18nProviderProps = Omit<I18nContext, "_"> & {
children?: React.ReactNode
}

export const LinguiContext = React.createContext<I18nContext | null>(null)
export const LinguiContext = createContext<I18nContext | null>(null)

export const useLinguiInternal = (devErrorMessage?: string): I18nContext => {
const context = React.useContext(LinguiContext)
const context = useContext(LinguiContext)

if (process.env.NODE_ENV !== "production") {
if (context == null) {
Expand All @@ -31,12 +38,12 @@ export function useLingui(): I18nContext {
return useLinguiInternal()
}

export function I18nProvider({
export const I18nProvider = ({
i18n,
defaultComponent,
children,
}: I18nProviderProps) {
const latestKnownLocale = React.useRef<string | undefined>(i18n.locale)
}: I18nProviderProps) => {
const latestKnownLocale = useRef<string | undefined>(i18n.locale)
/**
* We can't pass `i18n` object directly through context, because even when locale
* or messages are changed, i18n object is still the same. Context provider compares
Expand All @@ -48,7 +55,7 @@ export function I18nProvider({
*
* We can't use useMemo hook either, because we want to recalculate value manually.
*/
const makeContext = React.useCallback(
const makeContext = useCallback(
() => ({
i18n,
defaultComponent,
Expand All @@ -57,7 +64,7 @@ export function I18nProvider({
[i18n, defaultComponent]
)

const [context, setContext] = React.useState<I18nContext>(makeContext())
const [context, setContext] = useState<I18nContext>(makeContext())

/**
* Subscribe for locale/message changes
Expand All @@ -66,7 +73,7 @@ export function I18nProvider({
* data (active locale, catalogs). When new messages are loaded or locale is changed
* we need to trigger re-rendering of LinguiContext.Consumers.
*/
React.useEffect(() => {
useEffect(() => {
const updateContext = () => {
latestKnownLocale.current = i18n.locale
setContext(makeContext())
Expand Down
5 changes: 2 additions & 3 deletions packages/react/src/Trans.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import React from "react"

import { useLinguiInternal } from "./I18nProvider"
import { TransNoContext, type TransProps } from "./TransNoContext"

Expand All @@ -10,5 +8,6 @@ export function Trans(props: TransProps): React.ReactElement<any, any> | null {
Attempted to render message: ${props.message} id: ${props.id}. Make sure this component is rendered inside a I18nProvider.`
}
const lingui = useLinguiInternal(errMessage)
return React.createElement(TransNoContext, { ...props, lingui })

return <TransNoContext {...props} lingui={lingui} />
}
28 changes: 14 additions & 14 deletions packages/react/src/TransNoContext.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import React, { ComponentType, ReactNode } from "react"

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

export type TransRenderProps = {
id: string
Expand All @@ -13,14 +10,14 @@ export type TransRenderProps = {

export type TransRenderCallbackOrComponent =
| {
component?: undefined
component?: never
render?:
| ((props: TransRenderProps) => React.ReactElement<any, any>)
| null
}
| {
component?: React.ComponentType<TransRenderProps> | null
render?: undefined
render?: never
}

export type TransProps = {
Expand All @@ -40,7 +37,10 @@ export type TransProps = {
*/
export function TransNoContext(
props: TransProps & {
lingui: { i18n: I18n; defaultComponent?: ComponentType<TransRenderProps> }
lingui: {
i18n: I18n
defaultComponent?: React.ComponentType<TransRenderProps>
}
}
): React.ReactElement<any, any> | null {
const {
Expand Down Expand Up @@ -70,7 +70,7 @@ export function TransNoContext(
}

const FallbackComponent: React.ComponentType<TransRenderProps> =
defaultComponent || RenderFragment
defaultComponent || RenderChildren

const i18nProps: TransRenderProps = {
id,
Expand All @@ -94,7 +94,8 @@ export function TransNoContext(
console.error(
`Invalid value supplied to prop \`component\`. It must be a React component, provided ${component}`
)
return React.createElement(FallbackComponent, i18nProps, translation)

return <FallbackComponent {...i18nProps}>{translation}</FallbackComponent>
}

// Rendering using a render prop
Expand All @@ -107,12 +108,11 @@ export function TransNoContext(
const Component: React.ComponentType<TransRenderProps> =
component || FallbackComponent

return React.createElement(Component, i18nProps, translation)
return <Component {...i18nProps}>{translation}</Component>
}

const RenderFragment = ({ children }: TransRenderProps) => {
// cannot use React.Fragment directly because we're passing in props that it doesn't support
return <React.Fragment>{children}</React.Fragment>
const RenderChildren = ({ children }: TransRenderProps) => {
return children
}

const getInterpolationValuesAndComponents = (props: TransProps) => {
Expand Down Expand Up @@ -148,7 +148,7 @@ const getInterpolationValuesAndComponents = (props: TransProps) => {
}
const index = Object.keys(components).length
// react components, arrays, falsy values, all should be processed as JSX children
components[index] = <>{valueForKey as ReactNode}</>
components[index] = <>{valueForKey}</>
values[key] = `<${index}/>`
})
return { values, components }
Expand Down
1 change: 0 additions & 1 deletion packages/react/src/TransRsc.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { TransProps, TransNoContext } from "./TransNoContext"
import React from "react"
import { getI18n } from "./server"

export function TransRsc(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from "react"
import { cloneElement } from "react"

// match <tag>paired</tag> and <tag/> unpaired tags
const tagRe = /<([a-zA-Z0-9]+)>([\s\S]*?)<\/\1>|<([a-zA-Z0-9]+)\/>/
Expand Down Expand Up @@ -65,15 +65,15 @@ function formatElements(
}

// ignore problematic element but push its children and elements after it
element = React.createElement(React.Fragment)
element = <></>
}

if (Array.isArray(element)) {
element = React.createElement(React.Fragment, {}, element)
element = <>{element}</>
}

tree.push(
React.cloneElement(
cloneElement(
element,
{ key: uniqueId() },

Expand Down
1 change: 1 addition & 0 deletions packages/react/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
"noUncheckedIndexedAccess": true,
"isolatedModules": true,
"forceConsistentCasingInFileNames": true,
"jsx": "react-jsx"
}
}
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"compilerOptions": {
"module": "commonjs",
"jsx": "react",
"jsx": "react-jsx",
"noEmit": true,
"baseUrl": "./",
"pretty": true,
Expand Down
2 changes: 1 addition & 1 deletion website/docs/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ Integrate Lingui into your existing workflow. It supports explicit message keys

### Lightweight and Optimized

Core library is less than [2 kB gzipped](https://bundlephobia.com/result?p=@lingui/core), React components are additional [1.4 kB gzipped](https://bundlephobia.com/result?p=@lingui/react).
Core library is less than [2 kB gzipped](https://bundlephobia.com/result?p=@lingui/core), React components are additional [1.3 kB gzipped](https://bundlephobia.com/result?p=@lingui/react).

### AI Translations Ready

Expand Down
2 changes: 1 addition & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3256,7 +3256,7 @@ __metadata:
peerDependencies:
"@lingui/babel-plugin-lingui-macro": 5.7.0
babel-plugin-macros: 2 || 3
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react: ^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta:
"@lingui/babel-plugin-lingui-macro":
optional: true
Expand Down