Skip to content

Commit 3140f2a

Browse files
authored
fix(cli): po loader push performance, multi-entry sections, pseudo mode crash (#2064)
1 parent e490347 commit 3140f2a

File tree

4 files changed

+82
-34
lines changed

4 files changed

+82
-34
lines changed

.changeset/light-shrimps-hear.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"lingo.dev": patch
3+
---
4+
5+
fix PO loader push performance, multi-entry section handling, and pseudo mode crash

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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,35 @@ msgstr ""`;
464464
expect(portugueseResult).not.toContain('"Language: en\\n"');
465465
expect(portugueseResult).toContain('msgstr "Olá"');
466466
});
467+
it("push should translate all entries in multi-entry sections (no blank line between entries)", async () => {
468+
const loader = createLoader();
469+
const input = `
470+
msgid ""
471+
msgstr ""
472+
"Content-Type: text/plain; charset=utf-8\\n"
473+
474+
msgid "Entry A"
475+
msgstr "Entry A"
476+
msgid "Entry B"
477+
msgstr "Entry B"
478+
msgid "Entry C"
479+
msgstr "Entry C"
480+
`.trim();
481+
482+
await loader.pull("en", input);
483+
484+
const updatedData = {
485+
"Entry A": { singular: "Entrada A", plural: null },
486+
"Entry B": { singular: "Entrada B", plural: null },
487+
"Entry C": { singular: "Entrada C", plural: null },
488+
};
489+
490+
const result = await loader.push("en-upd", updatedData);
491+
492+
expect(result).toContain('msgstr "Entrada A"');
493+
expect(result).toContain('msgstr "Entrada B"');
494+
expect(result).toContain('msgstr "Entrada C"');
495+
});
467496
});
468497

469498
function createLoader(params: PoLoaderParams = { multiline: false }) {

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

Lines changed: 45 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ export function createPoDataLoader(
4949

5050
async push(locale, data, originalInput, originalLocale, pullInput) {
5151
// Parse each section to maintain structure
52-
const currentSections = pullInput?.split("\n\n").filter(Boolean) || [];
5352
const originalSections =
5453
originalInput?.split("\n\n").filter(Boolean) || [];
5554
const result = originalSections
@@ -63,45 +62,40 @@ export function createPoDataLoader(
6362
const contextKey = _.keys(sectionPo.translations)[0];
6463
const entries = sectionPo.translations[contextKey];
6564
const msgid = Object.keys(entries).find((key) => entries[key].msgid);
66-
67-
// If the section is empty, try to find it in the current sections
68-
const currentSection = currentSections.find((cs) => {
69-
const csPo = gettextParser.po.parse(cs);
70-
if (Object.keys(csPo.translations).length === 0) {
71-
return false;
72-
}
73-
const csContextKey = _.keys(csPo.translations)[0];
74-
const csEntries = csPo.translations[csContextKey];
75-
if (!csEntries) {
76-
return false;
77-
}
78-
const csMsgid = Object.keys(csEntries).find(
79-
(key) => csEntries[key].msgid,
80-
);
81-
return csMsgid === msgid;
82-
});
8365

8466
if (!msgid) {
85-
if (currentSection) {
86-
return currentSection;
67+
// If the section is empty, try to find it in the current sections
68+
const currentSections =
69+
pullInput?.split("\n\n").filter(Boolean) || [];
70+
const currentSection = currentSections.find((cs) => {
71+
const csPo = gettextParser.po.parse(cs);
72+
if (Object.keys(csPo.translations).length === 0) {
73+
return false;
74+
}
75+
const csContextKey = _.keys(csPo.translations)[0];
76+
const csEntries = csPo.translations[csContextKey];
77+
if (!csEntries) {
78+
return false;
79+
}
80+
const csMsgid = Object.keys(csEntries).find(
81+
(key) => csEntries[key].msgid,
82+
);
83+
return csMsgid === msgid;
84+
});
85+
return currentSection || section;
86+
}
87+
88+
const entriesToMerge: Record<string, { msgstr: string[] }> = {};
89+
for (const [id, entry] of Object.entries(entries)) {
90+
if (entry.msgid && data[id]) {
91+
entriesToMerge[id] = { msgstr: data[id].msgstr };
8792
}
88-
return section;
8993
}
90-
if (data[msgid]) {
91-
// Preserve headers from the target file
92-
const headers = currentSection
93-
? gettextParser.po.parse(currentSection).headers
94-
: sectionPo.headers;
9594

95+
if (Object.keys(entriesToMerge).length > 0) {
9696
const updatedPo = _.merge({}, sectionPo, {
97-
headers,
98-
translations: {
99-
[contextKey]: {
100-
[msgid]: {
101-
msgstr: data[msgid].msgstr,
102-
},
103-
},
104-
},
97+
headers: resolveTargetHeaders(pullInput, sectionPo),
98+
translations: { [contextKey]: entriesToMerge },
10599
});
106100
const updatedSection = gettextParser.po
107101
.compile(updatedPo, { foldLength: params.multiline ? 76 : false })
@@ -174,6 +168,23 @@ export function createPoContentLoader(): ILoader<
174168
});
175169
}
176170

171+
function resolveTargetHeaders(
172+
pullInput: string | null | undefined,
173+
sectionPo: GetTextTranslations,
174+
): Record<string, string> | undefined {
175+
// Only needed for embedded headers (header entry + regular entries in the same section)
176+
if (!sectionPo.translations[""]?.[""] || !pullInput) {
177+
return undefined;
178+
}
179+
const headerSection = pullInput
180+
.split("\n\n")
181+
.find((s) => s.includes('msgid ""'));
182+
if (!headerSection) {
183+
return undefined;
184+
}
185+
return gettextParser.po.parse(headerSection).headers;
186+
}
187+
177188
function preserveCommentOrder(section: string, originalSection: string) {
178189
// Split both sections into lines
179190
const sectionLines = section.split(/\r?\n/);

packages/cli/src/cli/localizer/pseudo.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ export default function createPseudoLocalizer(): ILocalizer {
1414
authenticated: true,
1515
};
1616
},
17+
validateSettings: async () => {
18+
return { valid: true };
19+
},
1720
localize: async (input: LocalizerData, onProgress) => {
1821
// Nothing to translate – return the input as-is.
1922
if (!Object.keys(input.processableData).length) {

0 commit comments

Comments
 (0)