diff --git a/apps/web/src/api/translation.ts b/apps/web/src/api/translation.ts index a46ac0f590..2181bbf6ef 100644 --- a/apps/web/src/api/translation.ts +++ b/apps/web/src/api/translation.ts @@ -263,16 +263,32 @@ const isSkippableBlock = (block: string): boolean => { ) { return true; } - // Tables: a pipe row plus a |---| separator row. Cell-safe translation is - // out of scope, so the whole block is passed through. - return ( - lines.some((line) => line.includes("|")) && - lines.some( - (line) => line.includes("|") && line.includes("-") && TABLE_SEPARATOR_LINE.test(line) - ) + // Tables: a |---| separator row directly below a pipe row with the SAME cell + // count (the GFM shape). Cell-safe translation is out of scope, so the whole + // block is passed through; requiring adjacency plus matching column counts + // keeps prose that merely contains pipe characters translatable. + return lines.some( + (line, i) => + i > 0 && + line.includes("|") && + line.includes("-") && + TABLE_SEPARATOR_LINE.test(line) && + lines[i - 1].includes("|") && + countTableCells(lines[i - 1]) === countTableCells(line) ); }; +const countTableCells = (line: string): number => { + let trimmed = line.trim(); + if (trimmed.startsWith("|")) { + trimmed = trimmed.slice(1); + } + if (trimmed.endsWith("|")) { + trimmed = trimmed.slice(0, -1); + } + return trimmed.split("|").length; +}; + interface TranslateRequest { text: string; apply: (translated: string) => void; diff --git a/apps/web/src/specs/api/translate-markdown.spec.ts b/apps/web/src/specs/api/translate-markdown.spec.ts index f377f602b6..400cd10252 100644 --- a/apps/web/src/specs/api/translate-markdown.spec.ts +++ b/apps/web/src/specs/api/translate-markdown.spec.ts @@ -277,4 +277,34 @@ it("preserves leading indentation on marker-less continuation lines", async () = ).rejects.toThrow("translate-cancelled"); expect(calls).toBe(1); }); +it("still skips a real GFM table (separator directly under the header row)", async () => { + const table = "| a | b |\n|---|---|\n| 1 | 2 |"; + const result = await translateMarkdown(`Intro.\n\n${table}`, "es", "en"); + + expect(result).toBe(`\u00abIntro.\u00bb\n\n${table}`); + }); + + it("translates prose containing pipes and a stray separator-like line", async () => { + const block = "El valor |x| es absoluto\nnota al margen\n|---|"; + const result = await translateMarkdown(block, "es", "en"); + + expect(result).toContain("\u00ab"); + expect(post).toHaveBeenCalled(); + }); + + it("translates pipe prose directly above a separator when cell counts differ", async () => { + const block = "El valor |x| es absoluto\n|---|"; + const result = await translateMarkdown(block, "es", "en"); + + expect(result).toContain("\u00ab"); + expect(post).toHaveBeenCalled(); + }); + + it("still skips a borderless GFM table (header without outer pipes)", async () => { + const table = "a | b\n--- | ---\n1 | 2"; + const result = await translateMarkdown(table, "es", "en"); + + expect(result).toBe(table); + expect(post).not.toHaveBeenCalled(); + }); });