Skip to content

Commit 53ce3dc

Browse files
authored
Merge branch 'main' into fix/1733-batch-size
2 parents 7d2b6ef + 336eeae commit 53ce3dc

File tree

4 files changed

+183
-16
lines changed

4 files changed

+183
-16
lines changed

packages/cli/CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
# lingo.dev
22

3+
## 0.133.7
4+
5+
### Patch Changes
6+
7+
- [#2059](https://github.com/lingodotdev/lingo.dev/pull/2059) [`4344399`](https://github.com/lingodotdev/lingo.dev/commit/43443996d4e6b6e4d4979bbe2aa7d5039b672d41) Thanks [@AndreyHirsa](https://github.com/AndreyHirsa)! - Skip undefined cues during VTT incremental batch push
8+
9+
## 0.133.6
10+
11+
### Patch Changes
12+
13+
- [#2057](https://github.com/lingodotdev/lingo.dev/pull/2057) [`abe3d2f`](https://github.com/lingodotdev/lingo.dev/commit/abe3d2fb8b0f3356dc63f8f9d4b861de66be0da6) Thanks [@AndreyHirsa](https://github.com/AndreyHirsa)! - Fix VTT parser crash on files with STYLE/REGION blocks
14+
315
## 0.133.5
416

517
### Patch Changes

packages/cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "lingo.dev",
3-
"version": "0.133.5",
3+
"version": "0.133.7",
44
"description": "Lingo.dev CLI",
55
"private": false,
66
"repository": {

packages/cli/src/cli/loaders/index.spec.ts

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2645,6 +2645,128 @@ Another cue
26452645
expect(Object.values(data)).toContain("Another cue");
26462646
expect(Object.values(data)).not.toContain("Hello world!");
26472647
});
2648+
2649+
it("should handle vtt files with STYLE blocks", async () => {
2650+
setupFileMocks();
2651+
2652+
const input = `
2653+
WEBVTT
2654+
2655+
STYLE
2656+
::cue(.heading) {
2657+
font-size: 150%;
2658+
font-weight: bold;
2659+
}
2660+
2661+
00:00:00.000 --> 00:00:01.000
2662+
Hello world!
2663+
2664+
00:00:30.000 --> 00:00:31.000
2665+
Another cue
2666+
`.trim();
2667+
2668+
const expectedOutput = {
2669+
"0#0-1#": "Hello world!",
2670+
"1#30-31#": "Another cue",
2671+
};
2672+
2673+
mockFileOperations(input);
2674+
2675+
const vttLoader = createBucketLoader("vtt", "i18n/[locale].vtt", {
2676+
defaultLocale: "en",
2677+
});
2678+
vttLoader.setDefaultLocale("en");
2679+
const data = await vttLoader.pull("en");
2680+
2681+
expect(data).toEqual(expectedOutput);
2682+
});
2683+
2684+
it("should preserve STYLE blocks in push output", async () => {
2685+
setupFileMocks();
2686+
2687+
const input = `
2688+
WEBVTT
2689+
2690+
STYLE
2691+
::cue(.heading) {
2692+
font-size: 150%;
2693+
font-weight: bold;
2694+
}
2695+
2696+
00:00:00.000 --> 00:00:01.000
2697+
Hello world!
2698+
`.trim();
2699+
2700+
mockFileOperations(input);
2701+
2702+
const vttLoader = createBucketLoader("vtt", "i18n/[locale].vtt", {
2703+
defaultLocale: "en",
2704+
});
2705+
vttLoader.setDefaultLocale("en");
2706+
await vttLoader.pull("en");
2707+
await vttLoader.push("es", { "0#0-1#": "¡Hola mundo!" });
2708+
2709+
const written = (fs.writeFile as any).mock.calls[0][1] as string;
2710+
expect(written).toContain("STYLE");
2711+
expect(written).toContain("::cue(.heading)");
2712+
expect(written).toContain("¡Hola mundo!");
2713+
});
2714+
2715+
it("should handle vtt files with REGION blocks", async () => {
2716+
setupFileMocks();
2717+
2718+
const input = `
2719+
WEBVTT
2720+
2721+
REGION
2722+
id:sidebar
2723+
width:30%
2724+
lines:3
2725+
2726+
00:00:00.000 --> 00:00:01.000
2727+
Hello world!
2728+
`.trim();
2729+
2730+
mockFileOperations(input);
2731+
2732+
const vttLoader = createBucketLoader("vtt", "i18n/[locale].vtt", {
2733+
defaultLocale: "en",
2734+
});
2735+
vttLoader.setDefaultLocale("en");
2736+
const data = await vttLoader.pull("en");
2737+
2738+
expect(data).toEqual({ "0#0-1#": "Hello world!" });
2739+
});
2740+
2741+
it("should preserve REGION blocks in push output", async () => {
2742+
setupFileMocks();
2743+
2744+
const input = `
2745+
WEBVTT
2746+
2747+
REGION
2748+
id:sidebar
2749+
width:30%
2750+
lines:3
2751+
2752+
00:00:00.000 --> 00:00:01.000
2753+
Hello world!
2754+
`.trim();
2755+
2756+
mockFileOperations(input);
2757+
2758+
const vttLoader = createBucketLoader("vtt", "i18n/[locale].vtt", {
2759+
defaultLocale: "en",
2760+
});
2761+
vttLoader.setDefaultLocale("en");
2762+
await vttLoader.pull("en");
2763+
await vttLoader.push("es", { "0#0-1#": "¡Hola mundo!" });
2764+
2765+
const written = (fs.writeFile as any).mock.calls[0][1] as string;
2766+
expect(written).toContain("REGION");
2767+
expect(written).toContain("id:sidebar");
2768+
expect(written).toContain("¡Hola mundo!");
2769+
});
26482770
});
26492771

26502772
describe("XML bucket loader", () => {

packages/cli/src/cli/loaders/vtt.ts

Lines changed: 48 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,26 @@ import webvtt from "node-webvtt";
22
import { ILoader } from "./_types";
33
import { createLoader } from "./_utils";
44

5+
const UNSUPPORTED_BLOCK_REGEX = /^(?:STYLE|REGION)\s*$/;
6+
7+
function isUnsupportedBlock(block: string): boolean {
8+
const firstLine = block.trimStart().split("\n", 1)[0];
9+
return UNSUPPORTED_BLOCK_REGEX.test(firstLine);
10+
}
11+
12+
// node-webvtt doesn't handle STYLE/REGION blocks — strip them before parsing
13+
function stripUnsupportedBlocks(input: string): string {
14+
return input
15+
.replace(/\r\n/g, "\n")
16+
.split("\n\n")
17+
.filter((part) => !isUnsupportedBlock(part))
18+
.join("\n\n");
19+
}
20+
21+
function getUnsupportedBlocks(input: string): string[] {
22+
return input.replace(/\r\n/g, "\n").split("\n\n").filter(isUnsupportedBlock);
23+
}
24+
525
export default function createVttLoader(): ILoader<
626
string,
727
Record<string, any>
@@ -11,7 +31,7 @@ export default function createVttLoader(): ILoader<
1131
if (!input) {
1232
return ""; // if VTT file does not exist yet we can not parse it - return empty string
1333
}
14-
const vtt = webvtt.parse(input)?.cues;
34+
const vtt = webvtt.parse(stripUnsupportedBlocks(input))?.cues;
1535
if (Object.keys(vtt).length === 0) {
1636
return {};
1737
} else {
@@ -22,27 +42,40 @@ export default function createVttLoader(): ILoader<
2242
}, {});
2343
}
2444
},
25-
async push(locale, payload) {
26-
const output = Object.entries(payload).map(([key, text]) => {
27-
const [id, timeRange, identifier] = key.split("#");
28-
const [startTime, endTime] = timeRange.split("-");
29-
30-
return {
31-
end: Number(endTime),
32-
identifier: identifier,
33-
start: Number(startTime),
34-
styles: "",
35-
text: text,
36-
};
37-
});
45+
async push(locale, payload, originalInput) {
46+
const output = Object.entries(payload).reduce(
47+
(cues: any[], [key, text]) => {
48+
if (!text) return cues;
49+
50+
const [, timeRange, identifier] = key.split("#");
51+
const [startTime, endTime] = timeRange.split("-");
52+
53+
cues.push({
54+
end: Number(endTime),
55+
identifier,
56+
start: Number(startTime),
57+
styles: "",
58+
text,
59+
});
60+
return cues;
61+
},
62+
[],
63+
);
3864

3965
const input = {
4066
valid: true,
4167
strict: true,
4268
cues: output,
4369
};
4470

45-
return webvtt.compile(input);
71+
const compiled = webvtt.compile(input);
72+
73+
// Re-insert STYLE/REGION blocks after the WEBVTT header
74+
const blocks = getUnsupportedBlocks(originalInput ?? "");
75+
if (blocks.length === 0) return compiled;
76+
77+
const [header, ...rest] = compiled.split("\n\n");
78+
return [header, ...blocks, ...rest].join("\n\n");
4679
},
4780
});
4881
}

0 commit comments

Comments
 (0)