Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/eleven-otters-smoke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"lingo.dev": patch
---

split images into sections
5 changes: 5 additions & 0 deletions .changeset/violet-bulldogs-train.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"lingo.dev": patch
---

dates in mdx
3 changes: 0 additions & 3 deletions .prettierrc

This file was deleted.

17 changes: 0 additions & 17 deletions packages/cli/.prettierrc

This file was deleted.

Empty file added packages/cli/demo/mdx/de.mdx
Empty file.
9 changes: 2 additions & 7 deletions packages/cli/i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,8 @@
"version": 1.6,
"locale": {
"source": "en",
"targets": ["es"]
},
"buckets": {
"mdx": {
"lockedKeys": ["meta/slug", "meta/category"],
"include": ["demo/mdx/[locale].mdx"]
}
"targets": ["de"]
},
"main": {},
"$schema": "https://lingo.dev/schema/i18n.json"
}
32 changes: 0 additions & 32 deletions packages/cli/i18n.lock
Original file line number Diff line number Diff line change
@@ -1,32 +0,0 @@
version: 1
checksums:
77624f597c2092ea61483d71313398c3:
meta/title: 0a2cc2035f59644733615dd8675b3618
meta/summary: dd9c3e74401afb579e4668b11f11f2ab
meta/author: 5cc899158b9a4b8e2a6abaf26c498786
content/0: 4dfc7a0ee6a9dc089d8a76ad27d38754
content/1: 133929451ee05c91d68b75a15bfe6596
content/2: c89b46f8b2f582e06111a6c96b82e8bf
content/3: 417995a6cdfb7ab127e5431b5b5ef720
content/4: aa9c76dbf759c838e8bd62ae85825e52
content/5: 5704f51f430cbaa8d6452b38c91ea485
content/6: 8db8789e2a220825c9df6c3706bb8389
content/7: 6278ebdd79f1ea718aabeb8e40b5f999
content/8: 37ad0f04abb0e32b88e3bd6b79bb7110
content/9: f3103fdf8e9bd164be82af6c84e17b81
content/10: f030df31e8f24b9890bc9b7d7d387a69
content/11: da348e7e947e0d6054f2b177abc6060b
content/12: 83e15bc1599d243ca5efee3bb6da3152
content/13: 3c176fdcdcc855b44cb42da632312e73
content/14: cecd264ec97755c0100c8bc7d5b0bd70
content/15: 426e9e0abc16c001e04cfb03c9611a6b
content/16: a42f584c463c26a44c5c6d0f6cb8ace0
content/17: e54125a6e2a6fea17309a2c8949e2490
content/18: 79ef1a72bb4e09c347e3a0afae68c18d
content/19: 3feea98e670a946eefdd2fc48d7b99b5
content/20: 51adf33450cab2ef392e93147386647c
content/21: 66ba09daf8e0ae94effa4b98f19ded22
content/22: fed9e643c4a407e5fa118690285a85ed
content/23: 5975348444539ca25e20d05e53b87105
content/24: 7f019348c6dd5d97e746f32741241c55
content/25: 5678250e0f9cff6a5b71f42ff222bd2d
43 changes: 20 additions & 23 deletions packages/cli/src/cli/loaders/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ describe("bucket loaders", () => {
});

it("should save html data", async () => {
const input = `
const input = dedent`
<html>
<head>
<title>My Page</title>
Expand All @@ -320,26 +320,16 @@ describe("bucket loaders", () => {
"body/2/3/0": "texto en negrita y ",
"body/2/3/1/0": "texto en cursiva",
};
const expectedOutput = `
<html lang="es">
<head>
<title>Mi Página</title>
<meta name="description" content="Descripción de la página" />
</head>
<body>
texto simple sin etiqueta html
<h1>¡Hola, mundo!</h1>
<p>
Este es un párrafo con un
<a href="https://example.com">enlace</a>
y
<b>
texto en negrita y
<i>texto en cursiva</i>
</b>
</p>
</body>
</html>
const expectedOutput = dedent`
<html lang="es"><head>
<title>Mi Página</title>
<meta name="description" content="Descripción de la página">
</head>
<body>texto simple sin etiqueta html<h1>¡Hola, mundo!</h1>
<p>Este es un párrafo con un <a href="https://example.com">enlace</a> y <b>texto en negrita y <i>texto en cursiva</i></b>
</p>

</body></html>
`.trim();

mockFileOperations(input);
Expand Down Expand Up @@ -437,7 +427,14 @@ describe("bucket loaders", () => {

const input = { messages: ["foo", "bar"] };
const payload = { "messages/0": "foo", "messages/1": "bar" };
const expectedOutput = `{\n "messages\": [\"foo\", \"bar\"]\n}`;
const expectedOutput = dedent`
{
"messages": [
"foo",
"bar"
]
}
`.trim();

mockFileOperations(JSON.stringify(input));

Expand Down Expand Up @@ -907,7 +904,7 @@ Another paragraph with **bold** and *italic* text.`;
"md-section-0": "# Heading 1",
"md-section-1": "This is a paragraph.",
"md-section-2": "## Heading 2",
"md-section-3": "Another paragraph with **bold** and _italic_ text.",
"md-section-3": "Another paragraph with **bold** and *italic* text.",
};

mockFileOperations(input);
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/cli/loaders/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Z from "zod";
import jsdom from "jsdom";
import { bucketTypeSchema } from "@lingo.dev/_spec";
import { composeLoaders } from "./_utils";
import createJsonLoader from "./json";
Expand Down
125 changes: 125 additions & 0 deletions packages/cli/src/cli/loaders/mdx2/code-placeholder.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,50 @@ describe("MDX Code Placeholder Loader", () => {
expect(pushed).toBe(md);
});

it("round-trips an image block with surrounding blank lines unchanged", async () => {
const md = dedent`
Text above.

![](https://example.com/img.png)

Text below.
`;

const pulled = await loader.pull("en", md);
const pushed = await loader.push("es", pulled);
expect(pushed).toBe(md);
});

it("round-trips and adds blank lines around an image block when missing", async () => {
const md = dedent`
Text above.
![](https://example.com/img.png)
Text below.
`;

const expected = dedent`
Text above.

![](https://example.com/img.png)

Text below.
`;

const pulled = await loader.pull("en", md);
const pushed = await loader.push("es", pulled);
expect(pushed).toBe(expected);
});

it("keeps image inside blockquote as-is", async () => {
const md = dedent`
> ![](https://example.com/img.png)
`;

const pulled = await loader.pull("en", md);
const pushed = await loader.push("es", pulled);
expect(pushed).toBe(md);
});

it("leaves incomplete fences untouched", async () => {
const md = "```js\nno close";
const pulled = await loader.pull("en", md);
Expand All @@ -275,6 +319,87 @@ describe("MDX Code Placeholder Loader", () => {
const pushed = await loader.push("es", pulled);
expect(pushed).toBe(md);
});

// Edge cases for image spacing

it("adds blank line after image when only before exists", async () => {
const md = dedent`
Before.

![alt](https://example.com/i.png)
After.
`;

const expected = dedent`
Before.

![alt](https://example.com/i.png)

After.
`;

const pulled = await loader.pull("en", md);
const pushed = await loader.push("es", pulled);
expect(pushed).toBe(expected);
});

it("adds blank line before image when only after exists", async () => {
const md = dedent`
Before.
![alt](https://example.com/i.png)

After.
`;

const expected = dedent`
Before.

![alt](https://example.com/i.png)

After.
`;

const pulled = await loader.pull("en", md);
const pushed = await loader.push("es", pulled);
expect(pushed).toBe(expected);
});

it("inserts spacing between consecutive images", async () => {
const md = dedent`
![](a.png)
![](b.png)
`;

const expected = dedent`
![](a.png)

![](b.png)
`;

const pulled = await loader.pull("en", md);
const pushed = await loader.push("es", pulled);
expect(pushed).toBe(expected);
});

it("handles image inside JSX component - adds blank lines", async () => {
const md = dedent`
<Wrapper>
![](pic.png)
</Wrapper>
`;

const expected = dedent`
<Wrapper>

![](pic.png)

</Wrapper>
`;

const pulled = await loader.pull("en", md);
const pushed = await loader.push("es", pulled);
expect(pushed).toBe(expected);
});
});

describe("inline code placeholder", () => {
Expand Down
45 changes: 45 additions & 0 deletions packages/cli/src/cli/loaders/mdx2/code-placeholder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,50 @@ import _ from "lodash";
const fenceRegex = /([ \t]*)(^>\s*)?```([\s\S]*?)```/gm;
const inlineCodeRegex = /(?<!`)`([^`\r\n]+?)`(?!`)/g;

// Matches markdown image tags, with optional alt text & parenthesis URL, possibly inside blockquotes
// Captures patterns like ![](url) or ![alt](url), with optional leading '> ' for blockquotes
const imageRegex = /([ \t]*)(^>\s*)?!\[[^\]]*?\]\([^\n\r]*?\)/gm;

/**
* Ensures that markdown image tags are surrounded by blank lines (\n\n) so that they are properly
* treated as separate blocks during subsequent processing and serialization.
*
* Behaviour mirrors `ensureTrailingFenceNewline` logic for code fences:
* • If an image tag is already inside a blockquote (starts with `>` after trimming) we leave it untouched.
* • Otherwise we add two newlines before and after the image tag, then later collapse multiple
* consecutive blank lines back to exactly one separation using lodash chain logic.
*/
function ensureSurroundingImageNewlines(_content: string) {
let found = false;
let content = _content;
let workingContent = content;

do {
found = false;
const matches = workingContent.match(imageRegex);
if (matches) {
const match = matches[0];

const replacement = match.trim().startsWith(">")
? match
: `\n\n${match}\n\n`;

content = content.replaceAll(match, replacement);
workingContent = workingContent.replaceAll(match, "");
found = true;
}
} while (found);

content = _.chain(content)
.split("\n\n")
.map((section) => _.trim(section, "\n"))
.filter(Boolean)
.join("\n\n")
.value();

return content;
}

function ensureTrailingFenceNewline(_content: string) {
let found = false;
let content = _content;
Expand Down Expand Up @@ -46,6 +90,7 @@ function extractCodePlaceholders(content: string): {
} {
let finalContent = content;
finalContent = ensureTrailingFenceNewline(finalContent);
finalContent = ensureSurroundingImageNewlines(finalContent);

const codePlaceholders: Record<string, string> = {};

Expand Down