Skip to content

Commit 8232e13

Browse files
author
Timofei Iatsenko
authored
feature(react): separate entry point for RSC (lingui#1743)
1 parent 4af4448 commit 8232e13

6 files changed

Lines changed: 167 additions & 151 deletions

File tree

packages/react/src/I18nProvider.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React, { ComponentType, FunctionComponent } from "react"
22
import type { I18n } from "@lingui/core"
3-
import type { TransRenderProps } from "./Trans"
3+
import type { TransRenderProps } from "./TransNoContext"
44

55
export type I18nContext = {
66
i18n: I18n

packages/react/src/Trans.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
import { setupI18n } from "@lingui/core"
1010
import { mockConsole } from "@lingui/jest-mocks"
1111
import { PropsWithChildren } from "react"
12-
import { TransNoContext } from "./Trans"
12+
import { TransNoContext } from "./TransNoContext"
1313

1414
describe("Trans component", () => {
1515
/*

packages/react/src/Trans.tsx

Lines changed: 2 additions & 147 deletions
Original file line numberDiff line numberDiff line change
@@ -1,154 +1,9 @@
1-
import React, { ComponentType } from "react"
1+
import React from "react"
22

33
import { useLingui } from "./I18nProvider"
4-
import { formatElements } from "./format"
5-
import type { MessageOptions } from "@lingui/core"
6-
import { I18n } from "@lingui/core"
7-
8-
export type TransRenderProps = {
9-
id: string
10-
translation: React.ReactNode
11-
children: React.ReactNode
12-
message?: string | null
13-
isTranslated: boolean
14-
}
15-
16-
export type TransRenderCallbackOrComponent =
17-
| {
18-
component?: undefined
19-
render?:
20-
| ((props: TransRenderProps) => React.ReactElement<any, any>)
21-
| null
22-
}
23-
| {
24-
component?: React.ComponentType<TransRenderProps> | null
25-
render?: undefined
26-
}
27-
28-
export type TransProps = {
29-
id: string
30-
message?: string
31-
values?: Record<string, unknown>
32-
components?: { [key: string]: React.ElementType | any }
33-
formats?: MessageOptions["formats"]
34-
comment?: string
35-
children?: React.ReactNode
36-
} & TransRenderCallbackOrComponent
4+
import { TransNoContext, TransProps } from "./TransNoContext"
375

386
export function Trans(props: TransProps): React.ReactElement<any, any> | null {
397
const lingui = useLingui()
408
return React.createElement(TransNoContext, { ...props, lingui })
419
}
42-
43-
/**
44-
* Version of `<Trans>` component without using a Provider/Context React feature.
45-
* Primarily made for support React Server Components (RSC)
46-
*
47-
* @experimental the api of this component is not stabilized yet.
48-
*/
49-
export function TransNoContext(
50-
props: TransProps & {
51-
lingui: { i18n: I18n; defaultComponent?: ComponentType<TransRenderProps> }
52-
}
53-
): React.ReactElement<any, any> | null {
54-
const {
55-
render,
56-
component,
57-
id,
58-
message,
59-
formats,
60-
lingui: { i18n, defaultComponent },
61-
} = props
62-
63-
const values = { ...props.values }
64-
const components = { ...props.components }
65-
66-
if (values) {
67-
/*
68-
Related discussion: https://github.com/lingui/js-lingui/issues/183
69-
70-
Values *might* contain React elements with static content.
71-
They're replaced with <INDEX /> placeholders and added to `components`.
72-
73-
Example:
74-
Translation: Hello {name}
75-
Values: { name: <strong>Jane</strong> }
76-
77-
It'll become "Hello <0 />" with components=[<strong>Jane</strong>]
78-
*/
79-
80-
Object.keys(values).forEach((key) => {
81-
const value = values[key]
82-
const valueIsReactEl =
83-
React.isValidElement(value) ||
84-
(Array.isArray(value) && value.every(React.isValidElement))
85-
if (!valueIsReactEl) return
86-
87-
const index = Object.keys(components).length
88-
89-
components[index] = value
90-
values[key] = `<${index}/>`
91-
})
92-
}
93-
94-
const _translation: string =
95-
i18n && typeof i18n._ === "function"
96-
? i18n._(id, values, { message, formats })
97-
: id // i18n provider isn't loaded at all
98-
99-
const translation = _translation
100-
? formatElements(_translation, components)
101-
: null
102-
103-
if (render === null || component === null) {
104-
// Although `string` is a valid react element, types only allow `Element`
105-
// Upstream issue: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/20544
106-
return translation as unknown as React.ReactElement<any, any>
107-
}
108-
109-
const FallbackComponent: React.ComponentType<TransRenderProps> =
110-
defaultComponent || RenderFragment
111-
112-
const i18nProps: TransRenderProps = {
113-
id,
114-
message,
115-
translation,
116-
isTranslated: id !== translation && message !== translation,
117-
children: translation, // for type-compatibility with `component` prop
118-
}
119-
120-
// Validation of `render` and `component` props
121-
if (render && component) {
122-
console.error(
123-
"You can't use both `component` and `render` prop at the same time. `component` is ignored."
124-
)
125-
} else if (render && typeof render !== "function") {
126-
console.error(
127-
`Invalid value supplied to prop \`render\`. It must be a function, provided ${render}`
128-
)
129-
} else if (component && typeof component !== "function") {
130-
// Apparently, both function components and class components are functions
131-
// See https://stackoverflow.com/a/41658173/1535540
132-
console.error(
133-
`Invalid value supplied to prop \`component\`. It must be a React component, provided ${component}`
134-
)
135-
return React.createElement(FallbackComponent, i18nProps, translation)
136-
}
137-
138-
// Rendering using a render prop
139-
if (typeof render === "function") {
140-
// Component: render={(props) => <a title={props.translation}>x</a>}
141-
return render(i18nProps)
142-
}
143-
144-
// `component` prop has a higher precedence over `defaultComponent`
145-
const Component: React.ComponentType<TransRenderProps> =
146-
component || FallbackComponent
147-
148-
return React.createElement(Component, i18nProps, translation)
149-
}
150-
151-
const RenderFragment = ({ children }: TransRenderProps) => {
152-
// cannot use React.Fragment directly because we're passing in props that it doesn't support
153-
return <React.Fragment>{children}</React.Fragment>
154-
}
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import React, { ComponentType } from "react"
2+
3+
import { formatElements } from "./format"
4+
import type { MessageOptions } from "@lingui/core"
5+
import { I18n } from "@lingui/core"
6+
7+
export type TransRenderProps = {
8+
id: string
9+
translation: React.ReactNode
10+
children: React.ReactNode
11+
message?: string | null
12+
isTranslated: boolean
13+
}
14+
15+
export type TransRenderCallbackOrComponent =
16+
| {
17+
component?: undefined
18+
render?:
19+
| ((props: TransRenderProps) => React.ReactElement<any, any>)
20+
| null
21+
}
22+
| {
23+
component?: React.ComponentType<TransRenderProps> | null
24+
render?: undefined
25+
}
26+
27+
export type TransProps = {
28+
id: string
29+
message?: string
30+
values?: Record<string, unknown>
31+
components?: { [key: string]: React.ElementType | any }
32+
formats?: MessageOptions["formats"]
33+
comment?: string
34+
children?: React.ReactNode
35+
} & TransRenderCallbackOrComponent
36+
37+
/**
38+
* Version of `<Trans>` component without using a Provider/Context React feature.
39+
* Primarily made for support React Server Components (RSC)
40+
*
41+
* @experimental the api of this component is not stabilized yet.
42+
*/
43+
export function TransNoContext(
44+
props: TransProps & {
45+
lingui: { i18n: I18n; defaultComponent?: ComponentType<TransRenderProps> }
46+
}
47+
): React.ReactElement<any, any> | null {
48+
const {
49+
render,
50+
component,
51+
id,
52+
message,
53+
formats,
54+
lingui: { i18n, defaultComponent },
55+
} = props
56+
57+
const values = { ...props.values }
58+
const components = { ...props.components }
59+
60+
if (values) {
61+
/*
62+
Related discussion: https://github.com/lingui/js-lingui/issues/183
63+
64+
Values *might* contain React elements with static content.
65+
They're replaced with <INDEX /> placeholders and added to `components`.
66+
67+
Example:
68+
Translation: Hello {name}
69+
Values: { name: <strong>Jane</strong> }
70+
71+
It'll become "Hello <0 />" with components=[<strong>Jane</strong>]
72+
*/
73+
74+
Object.keys(values).forEach((key) => {
75+
const value = values[key]
76+
const valueIsReactEl =
77+
React.isValidElement(value) ||
78+
(Array.isArray(value) && value.every(React.isValidElement))
79+
if (!valueIsReactEl) return
80+
81+
const index = Object.keys(components).length
82+
83+
components[index] = value
84+
values[key] = `<${index}/>`
85+
})
86+
}
87+
88+
const _translation: string =
89+
i18n && typeof i18n._ === "function"
90+
? i18n._(id, values, { message, formats })
91+
: id // i18n provider isn't loaded at all
92+
93+
const translation = _translation
94+
? formatElements(_translation, components)
95+
: null
96+
97+
if (render === null || component === null) {
98+
// Although `string` is a valid react element, types only allow `Element`
99+
// Upstream issue: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/20544
100+
return translation as unknown as React.ReactElement<any, any>
101+
}
102+
103+
const FallbackComponent: React.ComponentType<TransRenderProps> =
104+
defaultComponent || RenderFragment
105+
106+
const i18nProps: TransRenderProps = {
107+
id,
108+
message,
109+
translation,
110+
isTranslated: id !== translation && message !== translation,
111+
children: translation, // for type-compatibility with `component` prop
112+
}
113+
114+
// Validation of `render` and `component` props
115+
if (render && component) {
116+
console.error(
117+
"You can't use both `component` and `render` prop at the same time. `component` is ignored."
118+
)
119+
} else if (render && typeof render !== "function") {
120+
console.error(
121+
`Invalid value supplied to prop \`render\`. It must be a function, provided ${render}`
122+
)
123+
} else if (component && typeof component !== "function") {
124+
// Apparently, both function components and class components are functions
125+
// See https://stackoverflow.com/a/41658173/1535540
126+
console.error(
127+
`Invalid value supplied to prop \`component\`. It must be a React component, provided ${component}`
128+
)
129+
return React.createElement(FallbackComponent, i18nProps, translation)
130+
}
131+
132+
// Rendering using a render prop
133+
if (typeof render === "function") {
134+
// Component: render={(props) => <a title={props.translation}>x</a>}
135+
return render(i18nProps)
136+
}
137+
138+
// `component` prop has a higher precedence over `defaultComponent`
139+
const Component: React.ComponentType<TransRenderProps> =
140+
component || FallbackComponent
141+
142+
return React.createElement(Component, i18nProps, translation)
143+
}
144+
145+
const RenderFragment = ({ children }: TransRenderProps) => {
146+
// cannot use React.Fragment directly because we're passing in props that it doesn't support
147+
return <React.Fragment>{children}</React.Fragment>
148+
}

packages/react/src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ export { I18nProvider, useLingui, LinguiContext } from "./I18nProvider"
22

33
export type { I18nProviderProps, I18nContext } from "./I18nProvider"
44

5-
export { Trans, TransNoContext } from "./Trans"
5+
export { Trans } from "./Trans"
66

77
export type {
88
TransProps,
99
TransRenderProps,
1010
TransRenderCallbackOrComponent,
11-
} from "./Trans"
11+
} from "./TransNoContext"

packages/react/src/server.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/**
2+
* This is an entry point for React Server Components (RSC)
3+
*
4+
* The RSC uses a static analysis to find any non-valid function calls in the import graph.
5+
* That means this entry point and it's children should not have any Provider/Context calls.
6+
*/
7+
export { TransNoContext } from "./TransNoContext"
8+
9+
export type {
10+
TransProps,
11+
TransRenderProps,
12+
TransRenderCallbackOrComponent,
13+
} from "./TransNoContext"

0 commit comments

Comments
 (0)