diff --git a/next.config.js b/next.config.js index 8a7c325..61a3632 100644 --- a/next.config.js +++ b/next.config.js @@ -14,7 +14,7 @@ class LinguiTransRscResolver { ...request, request: request.context.issuerLayer === 'rsc' // RSC Version without Context - ? path.resolve('./src/i18n/Trans.tsx') + ? path.resolve('./src/i18n/rsc-trans.tsx') // Regular version : '@lingui/react', }; diff --git a/package.json b/package.json index 581dea3..838d010 100644 --- a/package.json +++ b/package.json @@ -9,9 +9,9 @@ "lint": "next lint" }, "dependencies": { - "@lingui/core": "^4.3.0", - "@lingui/macro": "^4.3.0", - "@lingui/react": "^4.3.0", + "@lingui/core": "^4.4.2", + "@lingui/macro": "^4.4.2", + "@lingui/react": "^4.4.2", "@lingui/swc-plugin": "^4.0.4", "@types/node": "20.4.5", "@types/react": "18.2.17", @@ -24,6 +24,6 @@ "typescript": "5.1.6" }, "devDependencies": { - "@lingui/loader": "^4.3.0" + "@lingui/loader": "^4.4.2" } } diff --git a/src/app/[locale]/page.tsx b/src/app/[locale]/page.tsx index f6b8828..d9dc328 100644 --- a/src/app/[locale]/page.tsx +++ b/src/app/[locale]/page.tsx @@ -22,8 +22,6 @@ export default async function Home({ params }) { i18n, ); - - return (
@@ -31,7 +29,7 @@ export default async function Home({ params }) { - {/**/} +

diff --git a/src/i18n/Trans.tsx b/src/i18n/Trans.tsx deleted file mode 100644 index 38ddeb7..0000000 --- a/src/i18n/Trans.tsx +++ /dev/null @@ -1,137 +0,0 @@ -import React from "react" - -import { formatElements } from "./format" -import { getI18n } from './locale'; - -export type MessageOptions = { - message?: string - formats?: any - comment?: string -} - -export type TransRenderProps = { - id: string - translation: React.ReactNode - children: React.ReactNode - message?: string | null - isTranslated: boolean -} - -export type TransRenderCallbackOrComponent = - | { - component?: undefined - render?: - | ((props: TransRenderProps) => React.ReactElement) - | null - } - | { - component?: React.ComponentType | null - render?: undefined - } - -export type TransProps = { - id: string - message?: string - values?: Record - components?: { [key: string]: React.ElementType | any } - formats?: MessageOptions["formats"] - comment?: string - children?: React.ReactNode -} & TransRenderCallbackOrComponent - -export function Trans(props: TransProps): React.ReactElement | null { - const i18n = getI18n() - - const { render, component, id, message, formats } = props - - const values = { ...props.values } - const components = { ...props.components } - - if (values) { - /* - Related discussion: https://github.com/lingui/js-lingui/issues/183 - - Values *might* contain React elements with static content. - They're replaced with placeholders and added to `components`. - - Example: - Translation: Hello {name} - Values: { name: Jane } - - It'll become "Hello <0 />" with components=[Jane] - */ - - Object.keys(values).forEach((key) => { - const value = values[key] - const valueIsReactEl = - React.isValidElement(value) || - (Array.isArray(value) && value.every(React.isValidElement)) - if (!valueIsReactEl) return - - const index = Object.keys(components).length - - components[index] = value - values[key] = `<${index}/>` - }) - } - - const _translation: string = - i18n && typeof i18n._ === "function" - ? i18n._(id, values, { message, formats }) - : id // i18n provider isn't loaded at all - - const translation = _translation - ? formatElements(_translation, components) - : null - - if (render === null || component === null) { - // Although `string` is a valid react element, types only allow `Element` - // Upstream issue: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/20544 - return translation as unknown as React.ReactElement - } - - const FallbackComponent: React.ComponentType = RenderFragment - - const i18nProps: TransRenderProps = { - id, - message, - translation, - isTranslated: id !== translation && message !== translation, - children: translation, // for type-compatibility with `component` prop - } - - // Validation of `render` and `component` props - if (render && component) { - console.error( - "You can't use both `component` and `render` prop at the same time. `component` is ignored." - ) - } else if (render && typeof render !== "function") { - console.error( - `Invalid value supplied to prop \`render\`. It must be a function, provided ${render}` - ) - } else if (component && typeof component !== "function") { - // Apparently, both function components and class components are functions - // See https://stackoverflow.com/a/41658173/1535540 - console.error( - `Invalid value supplied to prop \`component\`. It must be a React component, provided ${component}` - ) - return React.createElement(FallbackComponent, i18nProps, translation) - } - - // Rendering using a render prop - if (typeof render === "function") { - // Component: render={(props) => x} - return render(i18nProps) - } - - // `component` prop has a higher precedence over `defaultComponent` - const Component: React.ComponentType = - component || FallbackComponent - - return React.createElement(Component, i18nProps, translation) -} - -const RenderFragment = ({ children }: TransRenderProps) => { - // cannot use React.Fragment directly because we're passing in props that it doesn't support - return {children} -} diff --git a/src/i18n/format.ts b/src/i18n/format.ts deleted file mode 100644 index 68fea2b..0000000 --- a/src/i18n/format.ts +++ /dev/null @@ -1,123 +0,0 @@ -import React from "react" - -// match paired and unpaired tags -const tagRe = /<([a-zA-Z0-9]+)>(.*?)<\/\1>|<([a-zA-Z0-9]+)\/>/ -const nlRe = /(?:\r\n|\r|\n)/g - -// For HTML, certain tags should omit their close tag. We keep a whitelist for -// those special-case tags. -const voidElementTags = { - area: true, - base: true, - br: true, - col: true, - embed: true, - hr: true, - img: true, - input: true, - keygen: true, - link: true, - meta: true, - param: true, - source: true, - track: true, - wbr: true, - menuitem: true, -} - -/** - * `formatElements` - parse string and return tree of react elements - * - * `value` is string to be formatted with Paired or (unpaired) - * placeholders. `elements` is a array of react elements which indexes - * correspond to element indexes in formatted string - */ -function formatElements( - value: string, - elements: { [key: string]: React.ReactElement } = {} -): string | Array { - const uniqueId = makeCounter(0, "$lingui$") - const parts = value.replace(nlRe, "").split(tagRe) - - // no inline elements, return - if (parts.length === 1) return value - - const tree: Array = [] - - const before = parts.shift() - if (before) tree.push(before) - - for (const [index, children, after] of getElements(parts)) { - let element = typeof index !== "undefined" ? elements[index] : undefined - - if ( - !element || - (voidElementTags[element.type as keyof typeof voidElementTags] && - children) - ) { - if (!element) { - console.error( - `Can't use element at index '${index}' as it is not declared in the original translation` - ) - } else { - console.error( - `${element.type} is a void element tag therefore it must have no children` - ) - } - - // ignore problematic element but push its children and elements after it - element = React.createElement(React.Fragment) - } - - if (Array.isArray(element)) { - element = React.createElement(React.Fragment, {}, element) - } - - tree.push( - React.cloneElement( - element, - { key: uniqueId() }, - - // format children for pair tags - // unpaired tags might have children if it's a component passed as a variable - children ? formatElements(children, elements) : element.props.children - ) - ) - - if (after) tree.push(after) - } - - return tree -} - -/* - * `getElements` - return array of element indices and element children - * - * `parts` is array of [pairedIndex, children, unpairedIndex, textAfter, ...] - * where: - * - `pairedIndex` is index of paired element (undef for unpaired) - * - `children` are children of paired element (undef for unpaired) - * - `unpairedIndex` is index of unpaired element (undef for paired) - * - `textAfter` is string after all elements (empty string, if there's nothing) - * - * `parts` length is always a multiple of 4 - * - * Returns: Array<[elementIndex, children, after]> - */ -function getElements( - parts: string[] -): Array { - if (!parts.length) return [] - - const [paired, children, unpaired, after] = parts.slice(0, 4) - - const triple = [paired || unpaired, children || "", after] as const - return [triple].concat(getElements(parts.slice(4, parts.length))) -} - -const makeCounter = - (count = 0, prefix = "") => - () => - `${prefix}_${count++}` - -export { formatElements } diff --git a/src/i18n/rsc-trans.tsx b/src/i18n/rsc-trans.tsx new file mode 100644 index 0000000..caa5b9e --- /dev/null +++ b/src/i18n/rsc-trans.tsx @@ -0,0 +1,14 @@ +import React from "react" + +import { getI18n } from './locale'; +import { TransNoContext, TransProps } from '@lingui/react/server'; + +export function Trans(props: TransProps): React.ReactElement | null { + const i18n = getI18n() + + if (!i18n) { + throw new Error('Lingui for RSC is not initialized. Use `setI18n()` first in root of your RSC tree.'); + } + + return +} diff --git a/yarn.lock b/yarn.lock index 2aad6d9..37ee14e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -417,26 +417,26 @@ "@jridgewell/resolve-uri" "3.1.0" "@jridgewell/sourcemap-codec" "1.4.14" -"@lingui/babel-plugin-extract-messages@4.3.0": - version "4.3.0" - resolved "https://registry.yarnpkg.com/@lingui/babel-plugin-extract-messages/-/babel-plugin-extract-messages-4.3.0.tgz#2cf9486880318804d846e9ef96cef9841a68c14e" - integrity sha512-X23evX574Pv5wRpg9HytCuE0dsRaFi0kkrj8uQC++Dk1Iti0ptRFeuMU4jxINlfa972Ri3GF6ckGQ+Req3drYg== +"@lingui/babel-plugin-extract-messages@^4.4.1": + version "4.4.1" + resolved "http://0.0.0.0:4873/@lingui%2fbabel-plugin-extract-messages/-/babel-plugin-extract-messages-4.4.1.tgz#eef9752727d12fd414bce959e075790c2a983c3c" + integrity sha512-NFDrG8O4lmCageIxlmZo80J633rdb6ZKXWpP3EfyWyiO3ynrCzt0prrzgniMgfVRfcuCq0h7ktvUt8Jf8x0LoQ== -"@lingui/cli@4.3.0": - version "4.3.0" - resolved "https://registry.yarnpkg.com/@lingui/cli/-/cli-4.3.0.tgz#7c0530ef2caf0902c13fafd4db4b8b552d57446e" - integrity sha512-Z44IxdK7kzmL+5TB2Kuu2xB3O6n1WCsBG0nUXOMtwTWHNxBLdr89hFogO3Orf7QNyxpfw6yZ3Go3xRm3JO3rJg== +"@lingui/cli@^4.4.1": + version "4.4.1" + resolved "http://0.0.0.0:4873/@lingui%2fcli/-/cli-4.4.1.tgz#55d9249b3a2c044c938cadc8470fb52ba90aa4b7" + integrity sha512-IRv4Kemaee4zl8JL8CaPcBJG2YR2ZnDIq/60MH2UHtsl2KhTEuIgQjVnwEOWA/58S08m2kgPznOb+9i9L9PHOg== dependencies: "@babel/core" "^7.21.0" "@babel/generator" "^7.21.1" "@babel/parser" "^7.21.2" "@babel/runtime" "^7.21.0" "@babel/types" "^7.21.2" - "@lingui/babel-plugin-extract-messages" "4.3.0" - "@lingui/conf" "4.3.0" - "@lingui/core" "4.3.0" - "@lingui/format-po" "4.3.0" - "@lingui/message-utils" "4.3.0" + "@lingui/babel-plugin-extract-messages" "^4.4.1" + "@lingui/conf" "^4.4.1" + "@lingui/core" "^4.4.1" + "@lingui/format-po" "^4.4.1" + "@lingui/message-utils" "^4.4.1" babel-plugin-macros "^3.0.1" chalk "^4.1.0" chokidar "3.5.1" @@ -457,10 +457,10 @@ ramda "^0.27.1" source-map "^0.8.0-beta.0" -"@lingui/conf@4.3.0": - version "4.3.0" - resolved "https://registry.yarnpkg.com/@lingui/conf/-/conf-4.3.0.tgz#714c6b1b0415cb8af922daf0bc38c7339b40fada" - integrity sha512-ZflR/igJi3+MUr1VeJDNkYMpnNvinRdw9m17G7mxmemxtnYV+RsIiUB+aR3AcgbQbEEmF95XmZb8jlUFYWlvtw== +"@lingui/conf@^4.4.1": + version "4.4.1" + resolved "http://0.0.0.0:4873/@lingui%2fconf/-/conf-4.4.1.tgz#166a6174fccde9b5b32516ee2d50a45103ddd1a4" + integrity sha512-CTttJT47iCV8pfQj5HJx1Oy+IQNkGrYtB5HeqhzKVqp6QCP8+itjgB/eD8cObZlAJRLqSTjiIC67AUSXJBkkMg== dependencies: "@babel/runtime" "^7.20.13" chalk "^4.1.0" @@ -469,59 +469,59 @@ jiti "^1.17.1" lodash.get "^4.4.2" -"@lingui/core@4.3.0", "@lingui/core@^4.3.0": - version "4.3.0" - resolved "https://registry.yarnpkg.com/@lingui/core/-/core-4.3.0.tgz#239e9fed6e858185ecfd59355b09678df1335393" - integrity sha512-nx9UM3ndYaK9VG6xQpF0OD/n0W35o5EPbiIzxgNvdNzRFtv9l1Z3pBipHXurRuZX0oDepGhVyXFD0ykPDeF7jA== +"@lingui/core@^4.4.1": + version "4.4.1" + resolved "http://0.0.0.0:4873/@lingui%2fcore/-/core-4.4.1.tgz#1291b91f08e801a2048d7245689c678e6a71304f" + integrity sha512-yo/+ccyDcaLueMcUlpznQjUkknKbiJMYq4Sj237KLM0dBI178xWS52MT8AYEUknnGU0b1g1+DGr4p+Y5uxKvSg== dependencies: "@babel/runtime" "^7.20.13" - "@lingui/message-utils" "4.3.0" + "@lingui/message-utils" "^4.4.1" unraw "^2.0.1" -"@lingui/format-po@4.3.0": - version "4.3.0" - resolved "https://registry.yarnpkg.com/@lingui/format-po/-/format-po-4.3.0.tgz#ce4281ea047749842c1ebcbe4cb01fefd102571a" - integrity sha512-iwqMve+VMxfRisTCk2evcnhAfn54v5qXTXZUDGZQrSQ60r2A4VGtS8VdCzqHwKxvbIatjq4NfWGa0v0zb092jg== +"@lingui/format-po@^4.4.1": + version "4.4.1" + resolved "http://0.0.0.0:4873/@lingui%2fformat-po/-/format-po-4.4.1.tgz#b63257340a0dd3b2483f749e4186b312fd851aa7" + integrity sha512-/mX8sXUXAPISfWs619+AKiXmbjPWrV1DwKCxPOkTql/PYHNClDYmzW+il90eVJevKWFhhv8hi4nExNIiLBTyIw== dependencies: - "@lingui/conf" "4.3.0" - "@lingui/message-utils" "4.3.0" + "@lingui/conf" "^4.4.1" + "@lingui/message-utils" "^4.4.1" date-fns "^2.29.3" pofile "^1.1.4" -"@lingui/loader@^4.3.0": - version "4.3.0" - resolved "https://registry.yarnpkg.com/@lingui/loader/-/loader-4.3.0.tgz#cd8898036a09d70455102f00f3d98b7f0453270f" - integrity sha512-W22egBu2b4PnVAA4qSsTPm1Ca39jCzIaVQgm5l2MG4Ac9bIljQ7bei/Sda2SOg+r0p/JrRTDhKQePT94Z6r7dA== +"@lingui/loader@^4.4.1": + version "4.4.1" + resolved "http://0.0.0.0:4873/@lingui%2floader/-/loader-4.4.1.tgz#a8b09ceea9bfa7187cadf6524fc170dd2c583df1" + integrity sha512-94wQt6/P02D5d/wzR0HYiU9NIZljgtVCJxjak/+8EcBOgNUQ2wPSovym8jKNRJ/ha0f7L10cP4+ZBXNCurJJDQ== dependencies: "@babel/runtime" "^7.20.13" - "@lingui/cli" "4.3.0" - "@lingui/conf" "4.3.0" + "@lingui/cli" "^4.4.1" + "@lingui/conf" "^4.4.1" -"@lingui/macro@^4.3.0": - version "4.3.0" - resolved "https://registry.yarnpkg.com/@lingui/macro/-/macro-4.3.0.tgz#e2209bdd73a932647ed2402143f4cb55d99fc3b5" - integrity sha512-MkeYKshuEp2j8Nx0IOhppcSiN3wEw3UxMPXsHzMe34U/MPrrriYaxMRY7CSsQrXqftcacQTm9IiDWvJAj4/V9w== +"@lingui/macro@^4.4.1": + version "4.4.1" + resolved "http://0.0.0.0:4873/@lingui%2fmacro/-/macro-4.4.1.tgz#234f5859411fec523604b233ae990721ef6998f8" + integrity sha512-X1oDx7EXwNNOPOzcGNDNNDnSzIeH6JyBG1izMyZjOD26t2iw4KB7AGyYJr0fGE//n6l6iVWI0th7WTpTYlVWaQ== dependencies: "@babel/runtime" "^7.20.13" "@babel/types" "^7.20.7" - "@lingui/conf" "4.3.0" - "@lingui/core" "4.3.0" - "@lingui/message-utils" "4.3.0" + "@lingui/conf" "^4.4.1" + "@lingui/core" "^4.4.1" + "@lingui/message-utils" "^4.4.1" -"@lingui/message-utils@4.3.0": - version "4.3.0" - resolved "https://registry.yarnpkg.com/@lingui/message-utils/-/message-utils-4.3.0.tgz#e5ace0103261e2611c8a86748f7c9711e47e3db7" - integrity sha512-UtgO5pm6LpJKqmZctr2R2hT+qicb8UijFDG3dctKGBuwkkwunFZUswOLJ+AxfhaO652Vov/yf5qsymJ6UFXPBA== +"@lingui/message-utils@^4.4.1": + version "4.4.1" + resolved "http://0.0.0.0:4873/@lingui%2fmessage-utils/-/message-utils-4.4.1.tgz#6943b800c6772a270a96b34970f28c434d23cc69" + integrity sha512-oh8fST7vX4Ojn+zd4q5K90yRYo3ZLE3x38GEo3HOzDljNPi5BNT8i+f1q/MQAIYWDm+zofLfaaCxrsD32CzrTQ== dependencies: "@messageformat/parser" "^5.0.0" -"@lingui/react@^4.3.0": - version "4.3.0" - resolved "https://registry.yarnpkg.com/@lingui/react/-/react-4.3.0.tgz#d08fe2b3a6b97c891adc26bee68c83451ba24198" - integrity sha512-EbkS5J2i26ENuAie70Fb+urOlajOTpa0xF3wZ78Zt4XKJSruZQ1Vg0U7vndpD5QeLsIpjGmJONkLjg8KzyVmBA== +"@lingui/react@^4.4.1": + version "4.4.1" + resolved "http://0.0.0.0:4873/@lingui%2freact/-/react-4.4.1.tgz#6da8542b529a6e118b438f2dc255936b108aa3d4" + integrity sha512-Wggeg+xfEaL2oCs4cVYEdW3aDdMxX2JBbZCjlZygvyzc/8BdUA339YecWnsdA25AkT7zi2/jKfES5xGoESKu1w== dependencies: "@babel/runtime" "^7.20.13" - "@lingui/core" "4.3.0" + "@lingui/core" "^4.4.1" "@lingui/swc-plugin@^4.0.4": version "4.0.4"