-
-
Notifications
You must be signed in to change notification settings - Fork 49
Expand file tree
/
Copy pathtranslate.normalize.mjs
More file actions
210 lines (186 loc) Β· 6.64 KB
/
Copy pathtranslate.normalize.mjs
File metadata and controls
210 lines (186 loc) Β· 6.64 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
/** Keep fenced examples opaque while repairing prose across the whole document. */
const outsideCodeFences = (content, normalize) => {
const protectedBlocks = [];
const lines = [];
let fence;
let block = [];
let prefix = '<!-- logto-protected-code';
while (content.includes(prefix)) prefix += '-';
const protectBlock = () => {
const placeholder = `${prefix}-${protectedBlocks.length} -->`;
protectedBlocks.push({ placeholder, content: block.join('\n') });
lines.push(placeholder);
block = [];
};
for (const line of content.split('\n')) {
const marker =
/^[\t ]*(?:>[\t ]*)*(?:(?:[-+*]|\d+[.)])[\t ]+)?(?<marker>`{3,}|~{3,})(?<info>.*)$/.exec(
line
)?.groups;
if (fence) {
block.push(line);
if (
marker &&
marker.marker[0] === fence[0] &&
marker.marker.length >= fence.length &&
!marker.info.trim()
) {
protectBlock();
fence = undefined;
}
} else if (marker && !(marker.marker[0] === '`' && marker.info.includes('`'))) {
block.push(line);
fence = marker.marker;
} else {
lines.push(line);
}
}
if (block.length > 0) protectBlock();
return protectedBlocks.reduce(
(result, block) => result.replace(block.placeholder, () => block.content),
normalize(lines.join('\n'))
);
};
/**
* Collapse a simple multiline custom component used in a paragraph or Markdown list item.
*
* MDX treats the opening tag as part of the list-item paragraph, so translated output like:
*
* - <CloudLink>
* translated text
* </CloudLink> trailing text
*
* cannot be parsed. Keep the component and its plain-text child in the same paragraph.
*
* @param {string} content
* @returns {string}
*/
const normalizeMultilineCustomComponents = (content) =>
content.replaceAll(
/^(?<indent>[\t ]*)(?<listPrefix>(?:(?:[*+-]|\d+\.)\s+)?)(?<openingTag><(?<tagName>[A-Z][\dA-Za-z]*)\b[^\n>]*>)\n[\t ]+(?<childText>[^\n<]+)\n[\t ]*<\/\k<tagName>>(?<trailingText>[\t ]+\S[^\n]*)$/gm,
(...arguments_) => {
const { indent, listPrefix, openingTag, tagName, childText, trailingText } =
arguments_.at(-1);
return [
`${indent}{/* prettier-ignore */}`,
`${indent}${listPrefix}${openingTag}${childText.trim()}</${tagName}>${trailingText}`,
].join('\n');
}
);
/**
* Repair an unambiguous translated HTML tag mismatch in details blocks.
*
* Once </summary> has closed the summary and <details> is the current open tag, another
* </summary> can only be the translated form of the expected </details>.
*
* @param {string} content
* @returns {string}
*/
const normalizeDetailsClosingTags = (content) =>
content
.split('\n')
.reduce(
(state, line) => {
const match = /^(?<indent>\s*)<(?<closing>\/?)(?<tagName>details|summary)>$/.exec(line);
if (!match?.groups) {
return { ...state, lines: [...state.lines, line] };
}
const { indent, closing, tagName } = match.groups;
if (!closing) {
return {
...state,
openTags: [...state.openTags, tagName],
lines: [...state.lines, line],
};
}
const currentOpenTag = state.openTags.at(-1);
if (currentOpenTag === tagName) {
return {
...state,
openTags: state.openTags.slice(0, -1),
lines: [...state.lines, line],
};
}
if (tagName === 'summary' && currentOpenTag === 'details') {
return {
...state,
openTags: state.openTags.slice(0, -1),
lines: [...state.lines, `${indent}</details>`],
};
}
return { ...state, lines: [...state.lines, line] };
},
{ lines: [], openTags: [] }
)
.lines.join('\n');
/**
* Repair translated inline code followed by a Markdown link when the model swaps their targets.
*
* Source like:
*
* `Grant.LimitExceeded` [webhook event](/developers/webhooks/webhooks-events)
*
* can become:
*
* [webhook](Grant.LimitExceeded) (/developers/webhooks/webhooks-events)
*
* A dotted PascalCase identifier is not a valid docs destination, while the following
* root-relative path is. Restore the identifier as inline code and attach the real path
* to the translated link label.
*
* @param {string} content
* @returns {string}
*/
const normalizeMisplacedInlineCodeLinks = (content) =>
content.replaceAll(
/\[(?<label>[^\n\]]+)]\((?<identifier>[A-Z][\dA-Za-z]*(?:\.[A-Z][\dA-Za-z]*)+)\)[\t ]+\((?<destination>\/[^\s)]+)\)/g,
'`$<identifier>` [$<label>]($<destination>)'
);
const normalizeStructure = (content) =>
normalizeMisplacedInlineCodeLinks(
normalizeDetailsClosingTags(normalizeMultilineCustomComponents(content))
);
export const normalizeTranslatedMdxAfterAutofix = (content) =>
outsideCodeFences(content, normalizeStructure);
/**
* Split trailing text after closing custom component tags into the next line.
*
* This prevents invalid MDX like:
* </CloudLink> some text
*
* which may trigger `end-tag-mismatch` lint errors after translation.
*
* @param {string} content
* @returns {string}
*/
const normalizeProse = (content) =>
normalizeStructure(
content
// Normalize irregular whitespace that may slip in from model output, e.g. French `U+202F`.
.replaceAll(/[\u00A0\u2002-\u200A\u202F]/g, ' ')
.replaceAll(/[\u2060\uFEFF]/g, '')
)
.split('\n')
.flatMap((line) => {
// Fix headings like `### Title{slug} \{#slug}` where the model accidentally injects
// a bare `{slug}` before the real escaped anchor. The replacement keeps only `\{#slug}`.
const normalizedHeadingLine = line.replace(
/^(#{1,6}\s+.*?)(?<!\\){([\w-]+)}\s+(\\{#\2})$/,
'$1 $3'
);
// Fix translated MDX where a custom component closing tag is followed by trailing text on
// the same line, e.g. `</CloudLink> more text`, by moving that text onto the next line.
// This also supports list items like `- </CloudLink> more text` or `1. </CloudLink> more text`.
const match =
/^(?<indent>\s*)(?<listPrefix>(?:(?:[*+-]|\d+\.)\s+)?)?(?<closingTag><\/[A-Z][\dA-Za-z]*>)\s+(?<trailingText>\S.*)$/.exec(
normalizedHeadingLine
);
if (!match?.groups) {
return [normalizedHeadingLine];
}
const { indent, listPrefix = '', closingTag, trailingText } = match.groups;
const trailingIndent = `${indent}${' '.repeat(listPrefix.length)}`;
return [`${indent}${listPrefix}${closingTag}`, `${trailingIndent}${trailingText}`];
})
.join('\n');
export const normalizeTranslatedMdx = (content) => outsideCodeFences(content, normalizeProse);