From f82e0d3faa2448c007d254aab1da030816f83616 Mon Sep 17 00:00:00 2001 From: maxprilutskiy Date: Thu, 1 May 2025 16:09:28 -0700 Subject: [PATCH 1/2] fix: dates in mdx feat: support locked keys in mdx --- .changeset/violet-bulldogs-train.md | 5 + packages/cli/demo/mdx/en.mdx | 0 packages/cli/demo/mdx/es.mdx | 0 packages/cli/i18n.json | 7 +- packages/cli/i18n.lock | 169 +++--------------- packages/cli/src/cli/loaders/flat.spec.ts | 60 ++++++- packages/cli/src/cli/loaders/flat.ts | 37 +++- packages/cli/src/cli/loaders/index.spec.ts | 34 ++++ packages/cli/src/cli/loaders/index.ts | 1 + .../src/cli/loaders/mdx2/frontmatter-split.ts | 35 +++- 10 files changed, 190 insertions(+), 158 deletions(-) create mode 100644 .changeset/violet-bulldogs-train.md create mode 100644 packages/cli/demo/mdx/en.mdx create mode 100644 packages/cli/demo/mdx/es.mdx diff --git a/.changeset/violet-bulldogs-train.md b/.changeset/violet-bulldogs-train.md new file mode 100644 index 000000000..d8a892cd3 --- /dev/null +++ b/.changeset/violet-bulldogs-train.md @@ -0,0 +1,5 @@ +--- +"lingo.dev": patch +--- + +dates in mdx diff --git a/packages/cli/demo/mdx/en.mdx b/packages/cli/demo/mdx/en.mdx new file mode 100644 index 000000000..e69de29bb diff --git a/packages/cli/demo/mdx/es.mdx b/packages/cli/demo/mdx/es.mdx new file mode 100644 index 000000000..e69de29bb diff --git a/packages/cli/i18n.json b/packages/cli/i18n.json index 009f866c9..7c7711cbc 100644 --- a/packages/cli/i18n.json +++ b/packages/cli/i18n.json @@ -4,6 +4,11 @@ "source": "en", "targets": ["es"] }, - "buckets": {}, + "buckets": { + "mdx": { + "lockedKeys": ["meta/slug", "meta/category"], + "include": ["demo/mdx/[locale].mdx"] + } + }, "$schema": "https://lingo.dev/schema/i18n.json" } diff --git a/packages/cli/i18n.lock b/packages/cli/i18n.lock index e94da315a..f7e889a76 100644 --- a/packages/cli/i18n.lock +++ b/packages/cli/i18n.lock @@ -1,143 +1,32 @@ version: 1 checksums: 77624f597c2092ea61483d71313398c3: - meta/title: e2fb2a02229795be33772c83e43d6ec0 - meta/description: 22cb80fdc3ff54436a6e911c897b5563 - content/0: 8669019fdbb2cfc2d94a8fdc76333289 - content/1: 0b3e6591c20e9c2f7995fb8ea8150c0d - content/2: a885b09c29f2d77fbb7240e8ecb82d71 - content/3: 3db42a3ab228c0867d2904c8d9326e78 - content/4: fc67acac41259de6a882fb90a0ba97c7 - content/5: f962f29f56ae53c1e45c7f84972845fe - content/6: 48a70bf3dc2328eaa3d0a0fa0da266b6 - content/7: 51229b58a8a0092674e11f0b779cc669 - content/8: 8d16b73b16f0247208893f7832306e14 - content/9: 8bb8615f257ea78084a436256f6a0383 - content/10: c7d13744dbfb66641d7bfe744274668c - content/11: fbb2ae34b45a0d9d1aae59fdcb7f3697 - content/12: 29b0c41292ab1b8dd84bc7019ab8c110 - content/13: 57a519330e9311aac242f67fb675da40 - content/14: 1983a13000b7186a172b411f752f5a87 - content/15: fb506d8078da4d699b0bb09af7859ba2 - content/16: df3fc2cca7a7dbd073411c12538cc2d8 - content/17: 75434b5cec52c7992e82b16ed153622f - content/18: 263450bda03a8f3e199fd6f6f7f18df3 - content/19: 2612c038df28b262f30bd206c3e2bb92 - content/20: 532c64fa539ecfda6b819d79ffc978a5 - content/21: da09c2469f4b0071c4440e74c889f7eb - content/22: 36e1c42c25ffc7553a7fc9f85fe153f3 - content/23: fbd131eeabc1ba1f12de14e44c60155b - content/24: c2ba183b236c80dc82945312cff3a73a - content/25: 30934cc98ee9bc0531e55076b78cbfb1 - content/26: d2f34271c2fb61407a3d97b73b399c4b - content/27: 532c64fa539ecfda6b819d79ffc978a5 - content/28: 054a37a1e8b899ef4d50fefcd6a8c288 - content/29: 36e1c42c25ffc7553a7fc9f85fe153f3 - content/30: 19b55aecf778111519f6fc34f790b317 - content/31: 47559c448d86e0b8454be7edc8b9b1a5 - content/32: fbd131eeabc1ba1f12de14e44c60155b - content/33: 1c66dcf6b3d552f266f255bf4bcf79a8 - content/34: ea7b33b2f720d0a0edb3898491f4460c - content/35: e9e9cfa3981eb2000daa3a32ed6d67cb - content/36: 42d1cbe8080ec442fac8d36c5c355250 - content/37: d7624486811d1ffac1e0271f9d490e62 - content/38: 22bee2c7876b4a0432388ce3c2c5581a - content/39: a1fa3c9086f58b9634fd2f80a1059f7d - content/40: 532c64fa539ecfda6b819d79ffc978a5 - content/41: e850178aecaca59ab569c4640ddcb82d - content/42: 9c92bb299f2c90cb5e61584ddb965f11 - content/43: c23863a0bf30dca05d9673cd46882e5a - content/44: 93e97b200e02f64d812b6f99a2ab27d7 - content/45: e4458dc85a36ff38ab59085fea95bd29 - content/46: 95c7dfe3ef2d9e8f1791aadb54964029 - content/47: b7cda597f2c572a5399b52381e1d22e5 - content/48: 43763947f32df467cd79fe7317b788e0 - content/49: fbd131eeabc1ba1f12de14e44c60155b - content/50: f2adbf230d0cc2d692e0874d70eb3761 - content/51: 3a7ef3a9d8ac3324a2fbc0295dff316b - content/52: ab22c2877d37961ad7081bac762ff40a - content/53: 94511f66a74c07c008f4926046f118f3 - content/54: 07b70955bc8542fa69f68e5c9ac79fea - content/55: 532c64fa539ecfda6b819d79ffc978a5 - content/56: 8198784dc73b24684412f2f064af7fae - content/57: fbd131eeabc1ba1f12de14e44c60155b - content/58: 8193a8c88b54c40f110fe91ac7991b2a - content/59: e9e2b3c60c56bc817a203c4276e43f97 - content/60: 19c5246e25bd7e7ee7301c2ccb25649c - content/61: b34ef3f974c3e496a4c8428cc7ec34ab - content/62: 198efa2ace291a6e2341212e1fa13173 - content/63: 0c4ce0fc7100db6818cfd3b8f54ef7ab - content/64: 532c64fa539ecfda6b819d79ffc978a5 - content/65: 4baf7c60f192458c2cf580d80bf45886 - content/66: b51c649ded1f5129607321e17cda50ac - content/67: 5de887a8bb95f41420bc8471cd6ba237 - content/68: 6ab7c9811fb962d0cd6af1317cba58a6 - content/69: 587ab6a3de61d60e72157e0364cc450a - content/70: 0312b2ae592aced8f69ecffd07f3a8c4 - content/71: fbd131eeabc1ba1f12de14e44c60155b - content/72: 71cd921ee08deb528dc5ed88e734366d - content/73: 748e148725bb7dce6aeb37583314022d - content/74: cd0012f82026e5259c82a8a01f93548d - content/75: b1044ce7c8c728161623de5149031da7 - content/76: 8afa48b66aafb543572ece965caa92b5 - content/77: 1be302529ca305e6dd9c43e0c4ce062c - content/78: 532c64fa539ecfda6b819d79ffc978a5 - content/79: 8c308e80377a7228021a4c71b75f7e5b - content/80: 8d7c6a1c7e6d58c40e36de640030f4fb - content/81: aa86d22ba1174fcb6c2399a5367b7eb3 - content/82: 0147c1d2371526957c954e6ba3a4a497 - content/83: 0abc3050a3526cd3ef5036f98ab4da2f - content/84: ff7e6a0f3f0d1a8e9188ba8a1ab5c83e - content/85: 4f431510dd44bc50d78302de730b833a - content/86: 46e649764e676132df3b2071c4352246 - content/87: fbd131eeabc1ba1f12de14e44c60155b - content/88: 8c308e80377a7228021a4c71b75f7e5b - content/89: db98db1b77b5701518cc989656be65b1 - content/90: aa86d22ba1174fcb6c2399a5367b7eb3 - content/91: 991ba980de06819b51b0a2afbf0ea4de - content/92: 7a13302a245b587877a3a4adc06bfcdb - content/93: 73142542d0cc3f22fe0bf13d752356bf - content/94: 6adc8368e7524f1084f66c07392561c5 - content/95: 329db40c8de149e992cf7c8c86c85124 - content/96: 532c64fa539ecfda6b819d79ffc978a5 - content/97: 7be33d5f66246431debeb85c5d7977d2 - content/98: fbd131eeabc1ba1f12de14e44c60155b - content/99: 92c44c1a7dd8f934dafd2ffe73f9ac4d - content/100: ce3cdada9749f44b3da1b2e29accaf99 - content/101: c148f41b64a4e43bbce6e801edced8a5 - content/102: 0032e3b4b6f84958868add2c37bfa15b - content/103: 4526009c1563356c22075e7b2f0f49f7 - content/104: 300aeb0033b21a2a014070c85f3cab61 - content/105: c955e9917dcfacf6cabf0877e50a97e7 - content/106: 532c64fa539ecfda6b819d79ffc978a5 - content/107: beb7a1ffdf5ac23ba68b0aa929d6d5b3 - content/108: f9f94ca530fee2d0e35170e3d3e795ee - content/109: be14e07853f36ccf1c3ce513a98b2dd7 - content/110: a1aad7630c6a8d664bde98bd046dedc2 - content/111: 4b11e0bc3924ca1bfb6f6d080794379b - content/112: e9a7066d60dff19e016c77ce51eb3c7d - content/113: fbd131eeabc1ba1f12de14e44c60155b - content/114: beb7a1ffdf5ac23ba68b0aa929d6d5b3 - content/115: 5239dbbeff70511b0f3444370e00ab3b - content/116: be14e07853f36ccf1c3ce513a98b2dd7 - content/117: 4d934fa2f525170597802b91d609805e - content/118: 4b11e0bc3924ca1bfb6f6d080794379b - content/119: 75a6b27d6d1af7580d4f8819b87eceec - content/120: 5764a4faf8072cd1e9c91a9a4a796226 - content/121: 8d60dc75cc12ec0c0e795c869fe9e39c - content/122: 53f538dc288085fc4ca7e88f16061333 - content/123: b5829b740dfcb79f99d0e955f19aa635 - content/124: 33dd3556ad1f9d0cb21c872ea4419f59 - content/125: 5add80131019504c7a5c81a81855880c - content/126: 532c64fa539ecfda6b819d79ffc978a5 - content/127: 2c75df9946cf436f268b1acbf7bb3c17 - content/128: fbd131eeabc1ba1f12de14e44c60155b - content/129: 5690517c65f0f53b8be38e6cc14aa63d - content/130: 251e1c4f7fbd5c0094b4939a0ba8d48e - content/131: 00f3066eee15e5780f2ef0d1a4bc3769 - content/132: ad51f2de9681ef204db0f1c458eb9505 - content/133: 9b49af1601fbf7132fef6ebf400173d9 - content/134: c03859332fc4a6824bb2bc20ddb7c460 - content/135: 01b73b171c9d1420c6c0a37df0545c96 - content/136: 2ff78a7555656bee7bfa6e8f37e19144 - content/137: 89381d2f61834b55cb64bdddc3b88276 + 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 diff --git a/packages/cli/src/cli/loaders/flat.spec.ts b/packages/cli/src/cli/loaders/flat.spec.ts index c88a01622..6803869d4 100644 --- a/packages/cli/src/cli/loaders/flat.spec.ts +++ b/packages/cli/src/cli/loaders/flat.spec.ts @@ -29,6 +29,24 @@ describe("flat loader", () => { years: ["January 13, 2025", "February 14, 2025"], }); }); + + it("handles date objects correctly", async () => { + const loader = createFlatLoader(); + loader.setDefaultLocale("en"); + const date = new Date("2023-01-01T00:00:00Z"); + await loader.pull("en", { + publishedAt: date, + metadata: { createdAt: date }, + }); + const output = await loader.push("en", { + publishedAt: date.toISOString(), + "metadata/createdAt": date.toISOString(), + }); + expect(output).toEqual({ + publishedAt: date.toISOString(), + metadata: { createdAt: date.toISOString() }, + }); + }); }); describe("helper functions", () => { @@ -59,11 +77,21 @@ describe("flat loader", () => { messages: ["a", "b", "c"], }); }); + + it("should preserve date objects", () => { + const date = new Date(); + const input = { createdAt: date }; + const output = denormalizeObjectKeys(input); + expect(output).toEqual({ createdAt: date }); + }); }); describe("buildDenormalizedKeysMap", () => { it("should build normalized keys map", () => { - const denormalized: Record = flatten(denormalizeObjectKeys(inputObj), { delimiter: "/" }); + const denormalized: Record = flatten( + denormalizeObjectKeys(inputObj), + { delimiter: "/" }, + ); const output = buildDenormalizedKeysMap(denormalized); expect(output).toEqual({ "messages/1": `messages/${OBJECT_NUMERIC_KEY_PREFIX}1`, @@ -72,7 +100,10 @@ describe("flat loader", () => { }); it("should build keys map array", () => { - const denormalized: Record = flatten(denormalizeObjectKeys(inputArray), { delimiter: "/" }); + const denormalized: Record = flatten( + denormalizeObjectKeys(inputArray), + { delimiter: "/" }, + ); const output = buildDenormalizedKeysMap(denormalized); expect(output).toEqual({ "messages/0": "messages/0", @@ -92,21 +123,38 @@ describe("flat loader", () => { const output = normalizeObjectKeys(denormalizeObjectKeys(inputArray)); expect(output).toEqual(inputArray); }); + + it("should preserve date objects", () => { + const date = new Date(); + const input = { createdAt: date }; + const output = normalizeObjectKeys(input); + expect(output).toEqual({ createdAt: date }); + }); }); describe("mapDeormalizedKeys", () => { it("should map normalized keys", () => { - const denormalized: Record = flatten(denormalizeObjectKeys(inputObj), { delimiter: "/" }); + const denormalized: Record = flatten( + denormalizeObjectKeys(inputObj), + { delimiter: "/" }, + ); const keyMap = buildDenormalizedKeysMap(denormalized); - const flattened: Record = flatten(inputObj, { delimiter: "/" }); + const flattened: Record = flatten(inputObj, { + delimiter: "/", + }); const mapped = mapDenormalizedKeys(flattened, keyMap); expect(mapped).toEqual(denormalized); }); it("should map array", () => { - const denormalized: Record = flatten(denormalizeObjectKeys(inputArray), { delimiter: "/" }); + const denormalized: Record = flatten( + denormalizeObjectKeys(inputArray), + { delimiter: "/" }, + ); const keyMap = buildDenormalizedKeysMap(denormalized); - const flattened: Record = flatten(inputArray, { delimiter: "/" }); + const flattened: Record = flatten(inputArray, { + delimiter: "/", + }); const mapped = mapDenormalizedKeys(flattened, keyMap); expect(mapped).toEqual(denormalized); }); diff --git a/packages/cli/src/cli/loaders/flat.ts b/packages/cli/src/cli/loaders/flat.ts index 7979c6ede..16e5a0935 100644 --- a/packages/cli/src/cli/loaders/flat.ts +++ b/packages/cli/src/cli/loaders/flat.ts @@ -14,7 +14,10 @@ type DenormalizeResult = { keysMap: Record; }; -function createDenormalizeLoader(): ILoader, DenormalizeResult> { +function createDenormalizeLoader(): ILoader< + Record, + DenormalizeResult +> { return createLoader({ pull: async (locale, input) => { const inputDenormalized = denormalizeObjectKeys(input || {}); @@ -34,7 +37,10 @@ function createDenormalizeLoader(): ILoader, DenormalizeResu }); } -function createNormalizeLoader(): ILoader> { +function createNormalizeLoader(): ILoader< + DenormalizeResult, + Record +> { return createLoader({ pull: async (locale, input) => { const normalized = normalizeObjectKeys(input.denormalized); @@ -69,7 +75,10 @@ export function buildDenormalizedKeysMap(obj: Record) { ); } -export function mapDenormalizedKeys(obj: Record, denormalizedKeysMap: Record) { +export function mapDenormalizedKeys( + obj: Record, + denormalizedKeysMap: Record, +) { return Object.keys(obj).reduce( (acc, key) => { const denormalizedKey = denormalizedKeysMap[key] ?? key; @@ -80,13 +89,20 @@ export function mapDenormalizedKeys(obj: Record, denormalizedKeysMa ); } -export function denormalizeObjectKeys(obj: Record): Record { +export function denormalizeObjectKeys( + obj: Record, +): Record { if (_.isObject(obj) && !_.isArray(obj)) { return _.transform( obj, (result, value, key) => { - const newKey = !isNaN(Number(key)) ? `${OBJECT_NUMERIC_KEY_PREFIX}${key}` : key; - result[newKey] = _.isObject(value) ? denormalizeObjectKeys(value) : value; + const newKey = !isNaN(Number(key)) + ? `${OBJECT_NUMERIC_KEY_PREFIX}${key}` + : key; + result[newKey] = + _.isObject(value) && !_.isDate(value) + ? denormalizeObjectKeys(value) + : value; }, {} as Record, ); @@ -95,13 +111,18 @@ export function denormalizeObjectKeys(obj: Record): Record): Record { +export function normalizeObjectKeys( + obj: Record, +): Record { if (_.isObject(obj) && !_.isArray(obj)) { return _.transform( obj, (result, value, key) => { const newKey = `${key}`.replace(OBJECT_NUMERIC_KEY_PREFIX, ""); - result[newKey] = _.isObject(value) ? normalizeObjectKeys(value) : value; + result[newKey] = + _.isObject(value) && !_.isDate(value) + ? normalizeObjectKeys(value) + : value; }, {} as Record, ); diff --git a/packages/cli/src/cli/loaders/index.spec.ts b/packages/cli/src/cli/loaders/index.spec.ts index e6c940a96..a8b2d9e83 100644 --- a/packages/cli/src/cli/loaders/index.spec.ts +++ b/packages/cli/src/cli/loaders/index.spec.ts @@ -1,4 +1,5 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; +import dedent from "dedent"; import _ from "lodash"; import fs from "fs/promises"; import createBucketLoader from "./index"; @@ -852,6 +853,39 @@ describe("bucket loaders", () => { }); }); + describe("mdx bucket loader", () => { + it("should skip locked keys", async () => { + setupFileMocks(); + + const input = dedent` +--- +title: Test Mdx +category: test +--- + +# Heading 1 +`; + const expectedPayload = { + "meta/title": "Test Mdx", + "content/0": "\n# Heading 1", + }; + + mockFileOperations(input); + + const mdxLoader = createBucketLoader( + "mdx", + "i18n/[locale].mdx", + { isCacheRestore: false, defaultLocale: "en" }, + ["meta/category"], + ); + + mdxLoader.setDefaultLocale("en"); + const data = await mdxLoader.pull("en"); + + expect(data).toEqual(expectedPayload); + }); + }); + describe("markdown bucket loader", () => { it("should load markdown data", async () => { setupFileMocks(); diff --git a/packages/cli/src/cli/loaders/index.ts b/packages/cli/src/cli/loaders/index.ts index 8d584385b..7ba7bfde7 100644 --- a/packages/cli/src/cli/loaders/index.ts +++ b/packages/cli/src/cli/loaders/index.ts @@ -122,6 +122,7 @@ export default function createBucketLoader( createMdxSectionsSplit2Loader(), createLocalizableMdxDocumentLoader(), createFlatLoader(), + createLockedKeysLoader(lockedKeys || [], options.isCacheRestore), createSyncLoader(), createUnlocalizableLoader( options.isCacheRestore, diff --git a/packages/cli/src/cli/loaders/mdx2/frontmatter-split.ts b/packages/cli/src/cli/loaders/mdx2/frontmatter-split.ts index 94f768ff9..5d3371916 100644 --- a/packages/cli/src/cli/loaders/mdx2/frontmatter-split.ts +++ b/packages/cli/src/cli/loaders/mdx2/frontmatter-split.ts @@ -1,13 +1,19 @@ import matter from "gray-matter"; +import YAML from "yaml"; import { ILoader } from "../_types"; import { createLoader } from "../_utils"; import { RawMdx } from "./_types"; -export default function createMdxFrontmatterSplitLoader(): ILoader { +export default function createMdxFrontmatterSplitLoader(): ILoader< + string, + RawMdx +> { + const fmEngine = createFmEngine(); + return createLoader({ async pull(locale, input) { const source = input || ""; - const { data: frontmatter, content } = matter(source); + const { data: frontmatter, content } = fmEngine.parse(source); return { frontmatter: frontmatter as Record, @@ -18,9 +24,32 @@ export default function createMdxFrontmatterSplitLoader(): ILoader YAML.parse(str), + stringify: (obj: any) => + YAML.stringify(obj, { defaultStringType: "PLAIN" }), + }; + + return { + parse: (input: string) => + matter(input, { + engines: { + yaml: yamlEngine, + }, + }), + stringify: (content: string, frontmatter: Record) => + matter.stringify(content, frontmatter, { + engines: { + yaml: yamlEngine, + }, + }), + }; +} From 5ace7ed01a6fc4c7aed89a8c66adc870c6d1fb1d Mon Sep 17 00:00:00 2001 From: maxprilutskiy Date: Thu, 1 May 2025 17:15:52 -0700 Subject: [PATCH 2/2] feat: split images into sections too --- .changeset/eleven-otters-smoke.md | 5 + .prettierrc | 3 - packages/cli/.prettierrc | 17 -- packages/cli/demo/mdx/{es.mdx => de.mdx} | 0 packages/cli/i18n.json | 2 +- packages/cli/i18n.lock | 184 +++++++++++++++--- packages/cli/src/cli/loaders/index.spec.ts | 43 ++-- packages/cli/src/cli/loaders/index.ts | 1 + .../cli/loaders/mdx2/code-placeholder.spec.ts | 125 ++++++++++++ .../src/cli/loaders/mdx2/code-placeholder.ts | 45 +++++ 10 files changed, 353 insertions(+), 72 deletions(-) create mode 100644 .changeset/eleven-otters-smoke.md delete mode 100644 .prettierrc delete mode 100644 packages/cli/.prettierrc rename packages/cli/demo/mdx/{es.mdx => de.mdx} (100%) diff --git a/.changeset/eleven-otters-smoke.md b/.changeset/eleven-otters-smoke.md new file mode 100644 index 000000000..7219c16a1 --- /dev/null +++ b/.changeset/eleven-otters-smoke.md @@ -0,0 +1,5 @@ +--- +"lingo.dev": patch +--- + +split images into sections diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index 963354f23..000000000 --- a/.prettierrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "printWidth": 120 -} diff --git a/packages/cli/.prettierrc b/packages/cli/.prettierrc deleted file mode 100644 index 29f26b486..000000000 --- a/packages/cli/.prettierrc +++ /dev/null @@ -1,17 +0,0 @@ -{ - "tabWidth": 2, - "useTabs": false, - "trailingComma": "all", - "semi": true, - "printWidth": 80, - "endOfLine": "auto", - "proseWrap": "always", - "overrides": [ - { - "files": ["*.mdx"], - "options": { - "trailingComma": "none" - } - } - ] -} diff --git a/packages/cli/demo/mdx/es.mdx b/packages/cli/demo/mdx/de.mdx similarity index 100% rename from packages/cli/demo/mdx/es.mdx rename to packages/cli/demo/mdx/de.mdx diff --git a/packages/cli/i18n.json b/packages/cli/i18n.json index 7c7711cbc..f79856263 100644 --- a/packages/cli/i18n.json +++ b/packages/cli/i18n.json @@ -2,7 +2,7 @@ "version": 1.6, "locale": { "source": "en", - "targets": ["es"] + "targets": ["de"] }, "buckets": { "mdx": { diff --git a/packages/cli/i18n.lock b/packages/cli/i18n.lock index f7e889a76..19fb7fa75 100644 --- a/packages/cli/i18n.lock +++ b/packages/cli/i18n.lock @@ -1,32 +1,160 @@ version: 1 checksums: 77624f597c2092ea61483d71313398c3: - meta/title: 0a2cc2035f59644733615dd8675b3618 - meta/summary: dd9c3e74401afb579e4668b11f11f2ab + meta/title: 3d466e72adbd64aec260a33ab40c66e0 + meta/summary: 5e2437bda4bb745328b74e7536c5e2bc 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 + content/0: c97dcf52430d0b6ceaa5e8c54da6a123 + content/1: 6308d0b5d9ccb3d47263b1073857d7a5 + content/2: 6ed1a07128a8f62e92754b9366c15733 + content/3: ffdfd1c29699de1202d6e9c2fe40ca19 + content/4: cb8590c0da13e373e41a3c45110db7f7 + content/5: d09b811661a3477457978754e08ab73c + content/6: 90d6872f0354507f83e69969e07a2379 + content/7: 708ce864e329423462c8e387613e4211 + content/8: 0c46c9c081224173cbf86307463e61bd + content/9: 43f5f4274c6ea32d5f44a93cdd48038b + content/10: a1b48aedb1d7a5e4b220f4af24935621 + content/11: 9f5e46feb6d19fd4d825bf440e6d90e3 + content/12: 5ab65edb2215ca64c3cd762ea8d4d4a1 + content/13: 78044c30cb25038ad514d515998c025c + content/14: 6d4a783680382cffaa1a6b6f915446fd + content/15: 1ad8bf8b4eceae635a46033cc9be8cc2 + content/16: f5fc6f2de0af848581b7816c5f0eed92 + content/17: 748aeea4e1b6992e1c4312dd468f2d9f + content/18: 1e3b98536114408d0b0e132603292d84 + content/19: f0d0a0772f66fb81999316e968784fff + content/20: 7ab161b2d7f0ad4b7bf9b2bf7e885f7f + content/21: e0990c915cc3685d36c2f6d8a8b604b2 + content/22: 48abaa9e154fd09c65976bc356e10fdf + content/23: d88b1d340487695ed42f06ea3b384d25 + content/24: 68f9b9a8335ea04f629bbabbc1bddc6b + content/25: 9191c0a544b6bff2cc0d88a6ee015562 + content/26: a018089431ab81d8e3eb650d48cb0891 + content/27: 6568cae194e6b02e3473e2f03ce7a99b + content/28: b503050df4b5d39f8793a78428f043a3 + content/29: 57efa401f62a370e0994ffa54f432a51 + content/30: 71f582e9536f97187a11aed7667b6b25 + content/31: bc0f428b5020be69ab2f8ffc9b4fd552 + content/32: 0954da1d11fe28875c6fd43d058c3a0b + content/33: fe7eda6052021e301aa2109a632d79f0 + content/34: b4e2d48485d16402bf1d64dfd0f87e54 + content/35: 1e01a3fd453ec5420291101ec6dca074 + content/36: fe0e531d5aed6c3a5459482ebd2717db + content/37: 627582b6be6c0e24db4e31ea8d66407a + content/38: 4ac36375a5140dac63c4071760272d20 + content/39: f199b53e2ac104fa6c2582677b5e5ffa + content/40: 616c3cc0b38a55dbee9dfd3531ac8375 + content/41: 7bce3295a7b3ce13641b99958372429f + content/42: 08ce8af1dc131aa8d90189f025813b0b + content/43: 5abbb2be9a09a748f49e1eb86875c221 + content/44: 933e7fa179dbae58c56f8ab83cab3a22 + content/45: db45ce7c3065145d73ee8b15d8c070e1 + content/46: 64cd1a60efd61f94df81966676250035 + content/47: 93ada6c3c5920f86d6b8544fdaa2773f + content/48: 85125d16017563d88a91daf4343ac90e + content/49: f428dd5816421f34081250ec13024d32 + content/50: 2e5d72fa29d0b4320731d18f617ce4f0 + content/51: f1090be1fb863412049b30a0deefb7c1 + content/52: 4f6f5aad573cb95f2086b1a6b6caa6a9 + content/53: 1c15a921f24e17488f44c55a2c674c0c + content/54: 1c4c83f498b8e18eb0dc7fdb867e2533 + content/55: c1b24a1a8c10f444f4e8ba7c7dbe1138 + content/56: 1da2e967020de5e8bd08bc8299ecf3ff + content/57: c4b3d116f75fe26a4de95d4d2842a8f8 + content/58: dee7a4239dae9df8954df0f68ff51cdf + content/59: cbb0b336a4814c162f6c61cf9bf48298 + content/60: ad58d0a1da1b87f77fa288fdb399888b + content/61: 7f33d0a2c4214fae06dfead8e281b1b2 + content/62: 8612edd1fecca3ad2152fb95077a54bb + content/63: 4f0f7a90b065946674f538f1319fdeee + content/64: 7105b152e5563abca039fbeaa4f2f24b + content/65: 7513fedac680d637150e05c220426b27 + content/66: e658e51e8e2aac99445e39844a5d8def + content/67: b0d131773db2cdf5bfde6b4a0cb8e5a5 + content/68: 863e6116fc41df8ad14b07b78dea4cdf + content/69: 85d9c13554a58ce963f9930430873e8e + content/70: 9a119ab060475cdb0a597c816da9ab21 + content/71: 4facb91f7007ed2cc224923870555921 + content/72: 51b3713b7f20c841f8c5832cf80eae73 + content/73: 350f2f59aa3d3f3538b1ce499ec639ad + content/74: 6791f6eaf526eec0fd026e7fb6cf9226 + content/75: f0e9f16aeb3a39f59bab8ddf36678599 + content/76: 423189dd9440b14fdd8e6dbcff7e5e17 + content/77: 135276d473d4149f3222afcf4b9a7095 + content/78: 9088863a51e5c47ffa9feb246f1c3622 + content/79: 7e1338de3c32afd39f923ed63f0a3609 + content/80: 34e3d6e1f4b7e6e21f5ae673dde7f5b8 + content/81: 14e319da1a46e50ce1a71f4fe30c9ff3 + content/82: 260c386d971c3275ddc293bca8ecfc8b + content/83: 95594f9a695bf4afb55619eb46287972 + content/84: 3af542992184584c638ba7be3deed588 + content/85: d9a93fad68fdc9b43d841cacd1170507 + content/86: 9694f7a46b8854fbf8f3f8ea199177aa + content/87: 8cfc863780a27ac62a4a2983989e6e84 + content/88: 57113a36d2108692420118fdf28dd654 + content/89: 2bf7d8c57ecf79a0887995b934675620 + content/90: dd61ee8c55ce7c0ed710ffe680b2057f + content/91: 7fc5365070cca1b21bd905073703e5c7 + content/92: 390764d42face8a98a80ecaeda005dea + content/93: 7c00667e903214aad3bcb5172980ef8e + content/94: 7d23f1a511cbfc4ff99c949733e046e7 + content/95: 66c7d69732edbd421f261e581ec1650f + content/96: 8827a80fb9b5d6c8c5bd5e4907725203 + content/97: eb42d819e8a75398c0f838ebf10d0291 + content/98: bdf86138c391b1b8e498c5058114d80d + content/99: 5044cd5784c27c71aff91edd17dc1b23 + content/100: c221901449485f4b2b7ca21554ebd2d5 + content/101: ba1053462702674cfedf77dc76c9b84b + content/102: eef1bf8fee32326562d07cb82dfae122 + content/103: 9ebe8da3ed850d61e66f7dbc179fd55e + content/104: 37deb40fdeff2174ebaea3681719ea16 + content/105: 13b57cd2648b661b5b0059cf1dfd43aa + content/106: a2ed53a63528f4e69092c373c567a260 + content/107: 7cb30b33d7694d69988736850f9d451e + content/108: b2ea789ae947f71339f086b4a921c58e + content/109: dfc2e5f326ed0c78299e2e5ec1ee593d + content/110: 8b9f77a13ace3ca80fbe95afeb6a12ae + content/111: cf1a62cee449c30aedfe8d4740b69895 + content/112: 526ec2e19b9cc88addd6630298d051c0 + content/113: bfa0c042fb9e5424ff663e706f4d6542 + content/114: 9ccf708a26c5cfd147382bb520f296eb + content/115: 99cd568194e162589f1e597b5a72bcf5 + content/116: c3a0ac987aff1cdbc9e71f12ac2c4225 + content/117: 7a9f9276e90f8349e1d54d64dcd86eb9 + content/118: e4683631a12b40d367e212bcb576f314 + content/119: c34368b6b76a0a6f3712dae43727bac5 + content/120: a049b579345624f4b3c88a516daa4b3e + content/121: c489d2f8e02ab79265b859923a60fdd9 + content/122: 736d043337c0c776005ccd201d9759ed + content/123: 13990c2745d99c1ed8c6455fb1566c73 + content/124: 504f7922bb9428e196bcecae5e85799c + content/125: 5bb87ebebeb8fe36fb0edbf3844d5718 + content/126: d5d6129a2da2d070039f05d16ff69f71 + content/127: 9a1e70482f3798576c9b615f7340e4c4 + content/128: 4feaf678badd23444dabbf1b1a26b2dd + content/129: 9d85e8b92d87800b1e86f5fef5af90ab + content/130: e0657a0b0e51b136f813a30d910eaa8d + content/131: e1f19edda44618f049c1bfc4373e9e3a + content/132: e931c2d89bffe55d1c5cecca1fdc33c6 + content/133: 7d85dafaac24d69dd091ea4690212c4d + content/134: dcb505e3ba446a842a52d5e4c1bf81ac + content/135: 2ae8a12ebfc6612cb769c28cb4439f63 + content/136: f01063aa044e7d728fa70422b2546456 + content/137: 584ff19d9665141e83d5082c15633723 + content/138: be2134876ccd470fbdb9a2c6b3c53933 + content/139: afe3ca9ce337e4f75c9042cee8d279f5 + content/140: 140db12ff0e96d95803cee9de99454fd + content/141: b4c31983b0426d43c50b90c205db7dbe + content/142: 591914d79f46478067ec1539a020a1bd + content/143: b928caffe1bc63e03cdd9dad236eec14 + content/144: 4132364d10a1fc1b578ba66d860f920e + content/145: e5be8069f85bffa902b45a20c542b951 + content/146: f81ad0e547ff9e651830dde871767d0b + content/147: a6dab10268b2fc0236fa6ffbeee5bff8 + content/148: 1d26a45f5a4d4df59c93e8f9acbf211e + content/149: 65180d4f728f559053ec9c165a6d7eff + content/150: 71dec662b83c17e35c88ba9eb32d2f5f + content/151: 09136ad0618037d38a7c4c08c5af4621 + content/152: 11cfcee16e724a77a87810d728a7319c + content/153: 58ba3d7ac2f856d6bd1808ce67fedae9 diff --git a/packages/cli/src/cli/loaders/index.spec.ts b/packages/cli/src/cli/loaders/index.spec.ts index a8b2d9e83..7a9f8d093 100644 --- a/packages/cli/src/cli/loaders/index.spec.ts +++ b/packages/cli/src/cli/loaders/index.spec.ts @@ -294,7 +294,7 @@ describe("bucket loaders", () => { }); it("should save html data", async () => { - const input = ` + const input = dedent` My Page @@ -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 = ` - - - Mi Página - - - - texto simple sin etiqueta html -

¡Hola, mundo!

-

- Este es un párrafo con un - enlace - y - - texto en negrita y - texto en cursiva - -

- - + const expectedOutput = dedent` + + Mi Página + + + texto simple sin etiqueta html

¡Hola, mundo!

+

Este es un párrafo con un enlace y texto en negrita y texto en cursiva +

+ + `.trim(); mockFileOperations(input); @@ -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)); @@ -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); diff --git a/packages/cli/src/cli/loaders/index.ts b/packages/cli/src/cli/loaders/index.ts index 7ba7bfde7..9b95ea085 100644 --- a/packages/cli/src/cli/loaders/index.ts +++ b/packages/cli/src/cli/loaders/index.ts @@ -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"; diff --git a/packages/cli/src/cli/loaders/mdx2/code-placeholder.spec.ts b/packages/cli/src/cli/loaders/mdx2/code-placeholder.spec.ts index 211c5b62f..3f3eb71cf 100644 --- a/packages/cli/src/cli/loaders/mdx2/code-placeholder.spec.ts +++ b/packages/cli/src/cli/loaders/mdx2/code-placeholder.spec.ts @@ -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); @@ -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` + + ![](pic.png) + + `; + + const expected = dedent` + + + ![](pic.png) + + + `; + + const pulled = await loader.pull("en", md); + const pushed = await loader.push("es", pulled); + expect(pushed).toBe(expected); + }); }); describe("inline code placeholder", () => { diff --git a/packages/cli/src/cli/loaders/mdx2/code-placeholder.ts b/packages/cli/src/cli/loaders/mdx2/code-placeholder.ts index 7f2092cf6..292ead56b 100644 --- a/packages/cli/src/cli/loaders/mdx2/code-placeholder.ts +++ b/packages/cli/src/cli/loaders/mdx2/code-placeholder.ts @@ -6,6 +6,50 @@ import _ from "lodash"; const fenceRegex = /([ \t]*)(^>\s*)?```([\s\S]*?)```/gm; const inlineCodeRegex = /(? ' 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; @@ -46,6 +90,7 @@ function extractCodePlaceholders(content: string): { } { let finalContent = content; finalContent = ensureTrailingFenceNewline(finalContent); + finalContent = ensureSurroundingImageNewlines(finalContent); const codePlaceholders: Record = {};