Skip to content

Commit

Permalink
refactor(macro): robust whitespace handling according to jsx spec (#1882
Browse files Browse the repository at this point in the history
)
  • Loading branch information
timofei-iatsenko authored Mar 26, 2024
1 parent 30b5940 commit 2071e0f
Show file tree
Hide file tree
Showing 12 changed files with 112 additions and 173 deletions.
5 changes: 2 additions & 3 deletions examples/create-react-app/src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,14 @@ import React from "react"
import { getByText, render, act } from "@testing-library/react"
import { i18n } from "@lingui/core"
import { I18nProvider } from "@lingui/react"

import App from "./App"

i18n.load({
en: {
ZXBDaP: "Language switcher example:",
"zg/nOF": "Language switcher example: ",
},
cs: {
ZXBDaP: "Příklad přepínače jazyků:",
"zg/nOF": "Příklad přepínače jazyků: ",
},
})

Expand Down
26 changes: 13 additions & 13 deletions examples/create-react-app/src/locales/cs.po
Original file line number Diff line number Diff line change
Expand Up @@ -11,38 +11,38 @@ msgstr ""
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"

#: src/App.tsx:31
#: src/App.tsx:43
msgid "{count, plural, zero {There are no books} one {There's one book} other {There are # books}}"
msgstr "{count, plural, zero {Nejsou žádné knihy} one {Je tu jedna kniha} other {Existuje # knih}}"

#: src/App.tsx:37
#: src/App.tsx:50
msgid "Date formatter example:"
msgstr "Příklad formátovače data:"

#: src/App.tsx:28
#: src/App.tsx:40
msgid "Decrement"
msgstr "Úbytek"

#: src/App.tsx:45
#: src/App.tsx:59
msgid "I have a balance of {0}"
msgstr "Mám zůstatek {0}"

#: src/App.tsx:25
#: src/App.tsx:37
msgid "Increment"
msgstr "Přírůstek"

#: src/App.tsx:14
msgid "Language switcher example:"
msgstr "Příklad přepínače jazyků:"
#: src/App.tsx:19
msgid "Language switcher example: "
msgstr "Příklad přepínače jazyků: "

#: src/App.tsx:43
#: src/App.tsx:56
msgid "Number formatter example:"
msgstr "Příklad formátovače čísel:"

#: src/App.tsx:22
msgid "Plurals example:"
msgstr "Příklad množného čísla:"
#: src/App.tsx:33
msgid "Plurals example: "
msgstr "Příklad množného čísla: "

#: src/App.tsx:39
#: src/App.tsx:53
msgid "Today is {0}"
msgstr "Dnes je {0}"
26 changes: 13 additions & 13 deletions examples/create-react-app/src/locales/en.po
Original file line number Diff line number Diff line change
Expand Up @@ -13,38 +13,38 @@ msgstr ""
"Language-Team: \n"
"Plural-Forms: \n"

#: src/App.tsx:31
#: src/App.tsx:43
msgid "{count, plural, zero {There are no books} one {There's one book} other {There are # books}}"
msgstr "{count, plural, zero {There are no books} one {There's one book} other {There are # books}}"

#: src/App.tsx:37
#: src/App.tsx:50
msgid "Date formatter example:"
msgstr "Date formatter example:"

#: src/App.tsx:28
#: src/App.tsx:40
msgid "Decrement"
msgstr "Decrement"

#: src/App.tsx:45
#: src/App.tsx:59
msgid "I have a balance of {0}"
msgstr "I have a balance of {0}"

#: src/App.tsx:25
#: src/App.tsx:37
msgid "Increment"
msgstr "Increment"

#: src/App.tsx:14
msgid "Language switcher example:"
msgstr "Language switcher example:"
#: src/App.tsx:19
msgid "Language switcher example: "
msgstr "Language switcher example: "

#: src/App.tsx:43
#: src/App.tsx:56
msgid "Number formatter example:"
msgstr "Number formatter example:"

#: src/App.tsx:22
msgid "Plurals example:"
msgstr "Plurals example:"
#: src/App.tsx:33
msgid "Plurals example: "
msgstr "Plurals example: "

#: src/App.tsx:39
#: src/App.tsx:53
msgid "Today is {0}"
msgstr "Today is {0}"
4 changes: 2 additions & 2 deletions packages/babel-plugin-lingui-macro/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { PluginObj, Visitor, PluginPass } from "@babel/core"
import type * as babelTypes from "@babel/types"
import MacroJSX from "./macroJsx"
import { MacroJSX } from "./macroJsx"
import { NodePath } from "@babel/traverse"
import MacroJs from "./macroJs"
import { MacroJs } from "./macroJs"
import {
MACRO_CORE_PACKAGE,
MACRO_REACT_PACKAGE,
Expand Down
2 changes: 1 addition & 1 deletion packages/babel-plugin-lingui-macro/src/macroJs.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as types from "@babel/types"
import { type CallExpression, type Expression } from "@babel/types"
import MacroJs from "./macroJs"
import { MacroJs } from "./macroJs"
import type { NodePath } from "@babel/traverse"
import { transformSync } from "@babel/core"
import { JsMacroName } from "./constants"
Expand Down
2 changes: 1 addition & 1 deletion packages/babel-plugin-lingui-macro/src/macroJs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export type MacroJsOpts = {
stripNonEssentialProps: boolean
}

export default class MacroJs {
export class MacroJs {
// Babel Types
types: typeof babelTypes

Expand Down
83 changes: 1 addition & 82 deletions packages/babel-plugin-lingui-macro/src/macroJsx.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { JSXElement } from "@babel/types"
import * as types from "@babel/types"
import MacroJSX, { normalizeWhitespace } from "./macroJsx"
import { MacroJSX } from "./macroJsx"
import { transformSync } from "@babel/core"
import type { NodePath } from "@babel/traverse"
import { JsxMacroName } from "./constants"
Expand Down Expand Up @@ -38,87 +38,6 @@ function createMacro() {
}

describe("jsx macro", () => {
describe("normalizeWhitespace", () => {
it("should remove whitespace before/after expression", () => {
const actual = normalizeWhitespace(
`You have
{count, plural, one {Message} other {Messages}}`
)

expect(actual).toBe(
`You have{count, plural, one {Message} other {Messages}}`
)
})

it("should remove whitespace before/after tag", () => {
const actual = normalizeWhitespace(
` Hello <strong>World!</strong><br />
<p>
My name is <a href="/about">{{" "}}
<em>{{name}}</em></a>
</p>`
)

expect(actual).toBe(
`Hello <strong>World!</strong><br /><p>My name is <a href="/about">{{" "}}<em>{{name}}</em></a></p>`
)
})

it("should remove whitespace before/after tag", () => {
const actual = normalizeWhitespace(
`Property {0},
function {1},
array {2},
constant {3},
object {4},
everything {5}`
)

expect(actual).toBe(
`Property {0}, function {1}, array {2}, constant {3}, object {4}, everything {5}`
)
})

it("should remove trailing whitespaces in icu expressions", () => {
const actual = normalizeWhitespace(
`{count, plural, one {
<0>#</0> slot added
} other {
<1>#</1> slots added
}}
`
)

expect(actual).toBe(
`{count, plural, one {<0>#</0> slot added} other {<1>#</1> slots added}}`
)
})

it("should remove leading whitespaces in icu expressions", () => {
const actual = normalizeWhitespace(
`{count, plural, one {
One hello
} other {
Other hello
}}
`
)

expect(actual).toBe(
`{count, plural, one {One hello} other {Other hello}}`
)
})
})

describe("tokenizeTrans", () => {
it("simple message without arguments", () => {
const macro = createMacro()
Expand Down
44 changes: 5 additions & 39 deletions packages/babel-plugin-lingui-macro/src/macroJsx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,19 +31,14 @@ import {
MACRO_LEGACY_PACKAGE,
} from "./constants"
import { generateMessageId } from "@lingui/message-utils/generateMessageId"
import cleanJSXElementLiteralChild from "./utils/cleanJSXElementLiteralChild"

const pluralRuleRe = /(_[\d\w]+|zero|one|two|few|many|other)/
const jsx2icuExactChoice = (value: string) =>
value.replace(/_(\d+)/, "=$1").replace(/_(\w+)/, "$1")

type JSXChildPath = NodePath<JSXElement["children"][number]>

// replace whitespace before/after newline with single space
const keepSpaceRe = /\s*(?:\r\n|\r|\n)+\s*/g
// remove whitespace before/after tag or expression
const stripAroundTagsRe =
/(?:([>}])(?:\r\n|\r|\n)+\s*|(?:\r\n|\r|\n)+\s*(?=[<{]))/g

function maybeNodeValue(node: Node): string {
if (!node) return null
if (node.type === "StringLiteral") return node.value
Expand All @@ -55,28 +50,12 @@ function maybeNodeValue(node: Node): string {
return null
}

export function normalizeWhitespace(text: string): string {
return (
text
.replace(stripAroundTagsRe, "$1")
.replace(keepSpaceRe, " ")
// keep escaped newlines
.replace(/\\n/g, "\n")
.replace(/\\s/g, " ")
// we remove trailing whitespace inside Plural
.replace(/(\s+})/gm, "}")
// we remove leading whitespace inside Plural
.replace(/({\s+)/gm, "{")
.trim()
)
}

export type MacroJsxOpts = {
stripNonEssentialProps: boolean
transImportName: string
}

export default class MacroJSX {
export class MacroJSX {
types: typeof babelTypes
expressionIndex = makeCounter()
elementIndex = makeCounter()
Expand Down Expand Up @@ -109,13 +88,7 @@ export default class MacroJSX {
}

const messageFormat = new ICUMessageFormat()
const {
message: messageRaw,
values,
jsxElements,
} = messageFormat.fromTokens(tokens)
const message = normalizeWhitespace(messageRaw)

const { message, values, jsxElements } = messageFormat.fromTokens(tokens)
const { attributes, id, comment, context } = this.stripMacroAttributes(
path as NodePath<JSXElement>
)
Expand Down Expand Up @@ -296,8 +269,7 @@ export default class MacroJSX {
const exp = path.get("expression") as NodePath<Expression>

if (exp.isStringLiteral()) {
// Escape forced newlines to keep them in message.
return [this.tokenizeText(exp.node.value.replace(/\n/g, "\\n"))]
return [this.tokenizeText(exp.node.value)]
}
if (exp.isTemplateLiteral()) {
return this.tokenizeTemplateLiteral(exp)
Expand All @@ -315,7 +287,7 @@ export default class MacroJSX {
} else if (path.isJSXSpreadChild()) {
// just do nothing
} else if (path.isJSXText()) {
return [this.tokenizeText(path.node.value)]
return [this.tokenizeText(cleanJSXElementLiteralChild(path.node.value))]
} else {
// impossible path
// return this.tokenizeText(node.value)
Expand Down Expand Up @@ -533,10 +505,4 @@ export default class MacroJSX {
return JsxMacroName.SelectOrdinal
}
}

getJsxTagName = (node: JSXElement): string => {
if (this.types.isJSXIdentifier(node.openingElement.name)) {
return node.openingElement.name.name
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// taken from babel repo -> packages/babel-types/src/utils/react/cleanJSXElementLiteralChild.ts
export default function cleanJSXElementLiteralChild(value: string) {
const lines = value.split(/\r\n|\n|\r/)

let lastNonEmptyLine = 0

for (let i = 0; i < lines.length; i++) {
if (lines[i].match(/[^ \t]/)) {
lastNonEmptyLine = i
}
}

let str = ""

for (let i = 0; i < lines.length; i++) {
const line = lines[i]

const isFirstLine = i === 0
const isLastLine = i === lines.length - 1
const isLastNonEmptyLine = i === lastNonEmptyLine

// replace rendered whitespace tabs with spaces
let trimmedLine = line.replace(/\t/g, " ")

// trim whitespace touching a newline
if (!isFirstLine) {
trimmedLine = trimmedLine.replace(/^[ ]+/, "")
}

// trim whitespace touching an endline
if (!isLastLine) {
trimmedLine = trimmedLine.replace(/[ ]+$/, "")
}

if (trimmedLine) {
if (!isLastNonEmptyLine) {
trimmedLine += " "
}

str += trimmedLine
}
}

return str
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import { Trans as _Trans } from "@lingui/react"
;<_Trans id={"<stripped>"} message={"Keep multiple\nforced\nnewlines!"} />
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { Trans } from "@lingui/react/macro"
;<Trans>
Keep multiple{"\n"}
forced{"\n"}
newlines!
</Trans>
Loading

0 comments on commit 2071e0f

Please sign in to comment.