Skip to content

Commit 0816a98

Browse files
committed
Fix Lingui nested-element rendering and exclude email dist HTML from inspectcode
1 parent b4ab1f6 commit 0816a98

2 files changed

Lines changed: 38 additions & 28 deletions

File tree

application/shared-webapp/emails/build/lingui-macro-runtime.tsx

Lines changed: 33 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -53,39 +53,45 @@ function serializeChildren(children: ReactNode): {
5353
values: Record<string, unknown>;
5454
components: Record<string, ReactElement>;
5555
} {
56-
const messageParts: string[] = [];
5756
const values: Record<string, unknown> = {};
5857
const components: Record<string, ReactElement> = {};
59-
let placeholderIndex = 0;
58+
// Single counter shared across recursion depths. Lingui's babel macro assigns placeholder indices
59+
// globally across the whole <Trans> tree (so a <strong><Value/></strong> tree extracts as
60+
// `<0><1/></0>`, not `<0><0/></0>`). Using a fresh counter per recursion would diverge from the
61+
// macro's id-generation algorithm — `generateMessageId` would hash a different string, the catalog
62+
// lookup would silently miss for every nested-element template, and components stored at colliding
63+
// keys would render swapped content (e.g. <Link> children replaced by the <Value> render).
64+
const counter = { next: 0 };
6065

61-
Children.forEach(children, (child) => {
62-
if (typeof child === "string" || typeof child === "number") {
63-
messageParts.push(String(child));
64-
return;
65-
}
66+
function visit(nodes: ReactNode): string {
67+
const parts: string[] = [];
68+
Children.forEach(nodes, (child) => {
69+
if (typeof child === "string" || typeof child === "number") {
70+
parts.push(String(child));
71+
return;
72+
}
6673

67-
if (isValidElement(child)) {
68-
const componentIndex = String(placeholderIndex++);
69-
const elementChildren = (child.props as { children?: ReactNode }).children;
70-
if (elementChildren === undefined || elementChildren === null) {
71-
messageParts.push(`<${componentIndex}/>`);
72-
} else {
73-
const inner = serializeChildren(elementChildren);
74-
messageParts.push(`<${componentIndex}>${inner.message}</${componentIndex}>`);
75-
Object.assign(values, inner.values);
76-
Object.assign(components, inner.components);
74+
if (isValidElement(child)) {
75+
const componentIndex = String(counter.next++);
76+
components[componentIndex] = child;
77+
const elementChildren = (child.props as { children?: ReactNode }).children;
78+
if (elementChildren === undefined || elementChildren === null) {
79+
parts.push(`<${componentIndex}/>`);
80+
} else {
81+
parts.push(`<${componentIndex}>${visit(elementChildren)}</${componentIndex}>`);
82+
}
83+
return;
7784
}
78-
components[componentIndex] = child;
79-
return;
80-
}
8185

82-
if (child === null || child === undefined || typeof child === "boolean") return;
86+
if (child === null || child === undefined || typeof child === "boolean") return;
8387

84-
// Treat any remaining renderable (objects, arrays) as opaque values via {0}, {1}, ... placeholders.
85-
const valueKey = String(placeholderIndex++);
86-
values[valueKey] = child;
87-
messageParts.push(`{${valueKey}}`);
88-
});
88+
// Treat any remaining renderable (objects, arrays) as opaque values via {0}, {1}, ... placeholders.
89+
const valueKey = String(counter.next++);
90+
values[valueKey] = child;
91+
parts.push(`{${valueKey}}`);
92+
});
93+
return parts.join("");
94+
}
8995

90-
return { message: messageParts.join(""), values, components };
96+
return { message: visit(children), values, components };
9197
}

developer-cli/Commands/LintCommand.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,12 @@ private static bool RunBackendLinting(string? selfContainedSystem, bool noBuild,
135135
File.Delete(resultJsonPath);
136136
}
137137

138+
// Exclude rendered email artifacts from inspection. React Email emits Outlook-required table
139+
// attributes (align, border, cellPadding, cellSpacing) that JetBrains flags as obsolete HTML5,
140+
// but those attributes are mandatory for cross-client email rendering and cannot be removed.
141+
// The dist folder is gitignored build output, not source.
138142
ProcessHelper.Run(
139-
$"dotnet jb inspectcode {solutionFile.Name} --no-build --no-restore --output=result.json --severity=SUGGESTION",
143+
$"dotnet jb inspectcode {solutionFile.Name} --no-build --no-restore --output=result.json --severity=SUGGESTION --exclude=**/emails/dist/**",
140144
solutionFile.Directory!.FullName,
141145
"Linting",
142146
quiet

0 commit comments

Comments
 (0)