Skip to content

Commit 99d9901

Browse files
fix: avoid msg id fallbacks in .po files (#675)
1 parent 1933755 commit 99d9901

5 files changed

Lines changed: 142 additions & 18 deletions

File tree

.changeset/curvy-timers-heal.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+
avoid msg id fallbacks in .po files

packages/cli/src/cli/loaders/_types.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
export interface ILoaderDefinition<I, O, C> {
22
init?(): Promise<C>;
3-
pull(locale: string, input: I, initCtx?: C): Promise<O>;
3+
pull(
4+
locale: string,
5+
input: I,
6+
initCtx?: C,
7+
originalLocale?: string,
8+
): Promise<O>;
49
push(
510
locale: string,
611
data: O,

packages/cli/src/cli/loaders/_utils.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { ILoader, ILoaderDefinition } from "./_types";
22

3-
export function composeLoaders(...loaders: ILoader<any, any, any>[]): ILoader<any, any> {
3+
export function composeLoaders(
4+
...loaders: ILoader<any, any, any>[]
5+
): ILoader<any, any> {
46
return {
57
init: async () => {
68
for (const loader of loaders) {
@@ -30,7 +32,9 @@ export function composeLoaders(...loaders: ILoader<any, any, any>[]): ILoader<an
3032
};
3133
}
3234

33-
export function createLoader<I, O, C>(lDefinition: ILoaderDefinition<I, O, C>): ILoader<I, O, C> {
35+
export function createLoader<I, O, C>(
36+
lDefinition: ILoaderDefinition<I, O, C>,
37+
): ILoader<I, O, C> {
3438
const state = {
3539
defaultLocale: undefined as string | undefined,
3640
originalInput: undefined as I | undefined | null,
@@ -65,7 +69,12 @@ export function createLoader<I, O, C>(lDefinition: ILoaderDefinition<I, O, C>):
6569
}
6670

6771
state.pullInput = input;
68-
const result = await lDefinition.pull(locale, input, state.initCtx);
72+
const result = await lDefinition.pull(
73+
locale,
74+
input,
75+
state.initCtx,
76+
state.defaultLocale,
77+
);
6978
state.pullOutput = result;
7079

7180
return result;

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

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,86 @@ msgstr "[upd] Role"
234234
const result = await loader.push("en-upd", payload);
235235
expect(result).toEqual(updatedInput);
236236
});
237+
238+
it("fallbacks to msgid when single msgstr value is empty", async () => {
239+
const loader = createLoader();
240+
const input = `
241+
#: hello.py:1
242+
msgid "File"
243+
msgstr ""
244+
`.trim();
245+
246+
const data = await loader.pull("en", input);
247+
expect(data).toEqual({
248+
File: {
249+
singular: "File",
250+
plural: null,
251+
},
252+
});
253+
});
254+
255+
it("fallbacks to msgid when msgstr values are empty", async () => {
256+
const loader = createLoader();
257+
const input = `
258+
#: hello.py:1
259+
msgid "File"
260+
msgstr[0] ""
261+
msgstr[1] ""
262+
`.trim();
263+
264+
const data = await loader.pull("en", input);
265+
expect(data).toEqual({
266+
File: {
267+
singular: "File",
268+
plural: "File",
269+
},
270+
});
271+
});
272+
273+
it("does not fallback to msgid for non-source locale when single msgstr value is empty", async () => {
274+
const loader = createLoader();
275+
const input = `
276+
#: hello.py:1
277+
msgid "File"
278+
msgstr ""
279+
`.trim();
280+
281+
// First, pull default locale to satisfy loader invariants
282+
await loader.pull("en", input);
283+
284+
// Pull a different locale with the same content
285+
const data = await loader.pull("fr", input);
286+
287+
expect(data).toEqual({
288+
File: {
289+
singular: null,
290+
plural: null,
291+
},
292+
});
293+
});
294+
295+
it("does not fallback to msgid for non-source locale when msgstr values are empty", async () => {
296+
const loader = createLoader();
297+
const input = `
298+
#: hello.py:1
299+
msgid "File"
300+
msgstr[0] ""
301+
msgstr[1] ""
302+
`.trim();
303+
304+
// Pull default locale first
305+
await loader.pull("en", input);
306+
307+
// Pull a different locale
308+
const data = await loader.pull("fr", input);
309+
310+
expect(data).toEqual({
311+
File: {
312+
singular: null,
313+
plural: null,
314+
},
315+
});
316+
});
237317
});
238318

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

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

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ export default function createPoLoader(
1717
return composeLoaders(createPoDataLoader(params), createPoContentLoader());
1818
}
1919

20-
export function createPoDataLoader(params: PoLoaderParams): ILoader<string, PoTranslationEntry> {
20+
export function createPoDataLoader(
21+
params: PoLoaderParams,
22+
): ILoader<string, PoTranslationEntry> {
2123
return createLoader({
2224
async pull(locale, input) {
2325
const parsedPo = gettextParser.po.parse(input);
@@ -43,7 +45,8 @@ export function createPoDataLoader(params: PoLoaderParams): ILoader<string, PoTr
4345
async push(locale, data, originalInput, originalLocale, pullInput) {
4446
// Parse each section to maintain structure
4547
const currentSections = pullInput?.split("\n\n").filter(Boolean) || [];
46-
const originalSections = originalInput?.split("\n\n").filter(Boolean) || [];
48+
const originalSections =
49+
originalInput?.split("\n\n").filter(Boolean) || [];
4750
const result = originalSections
4851
.map((section) => {
4952
const sectionPo = gettextParser.po.parse(section);
@@ -56,7 +59,9 @@ export function createPoDataLoader(params: PoLoaderParams): ILoader<string, PoTr
5659
const csPo = gettextParser.po.parse(cs);
5760
const csContextKey = _.keys(csPo.translations)[0];
5861
const csEntries = csPo.translations[csContextKey];
59-
const csMsgid = Object.keys(csEntries).find((key) => csEntries[key].msgid);
62+
const csMsgid = Object.keys(csEntries).find(
63+
(key) => csEntries[key].msgid,
64+
);
6065
return csMsgid === msgid;
6166
});
6267

@@ -78,7 +83,10 @@ export function createPoDataLoader(params: PoLoaderParams): ILoader<string, PoTr
7883
return gettextParser.po
7984
.compile(updatedPo, { foldLength: params.multiline ? 76 : false })
8085
.toString()
81-
.replace([`msgid ""`, `msgstr "Content-Type: text/plain\\n"`].join("\n"), "")
86+
.replace(
87+
[`msgid ""`, `msgstr "Content-Type: text/plain\\n"`].join("\n"),
88+
"",
89+
)
8290
.trim();
8391
}
8492
return section.trim();
@@ -89,19 +97,33 @@ export function createPoDataLoader(params: PoLoaderParams): ILoader<string, PoTr
8997
});
9098
}
9199

92-
export function createPoContentLoader(): ILoader<PoTranslationEntry, Record<string, PoTranslationEntry>> {
100+
export function createPoContentLoader(): ILoader<
101+
PoTranslationEntry,
102+
Record<string, PoTranslationEntry>
103+
> {
93104
return createLoader({
94-
async pull(locale, input) {
105+
async pull(locale, input, initCtx, originalLocale) {
95106
const result = _.chain(input)
96107
.entries()
97108
.filter(([, entry]) => !!entry.msgid)
98-
.map(([, entry]) => [
99-
entry.msgid,
100-
{
101-
singular: entry.msgstr[0] || entry.msgid,
102-
plural: (entry.msgstr[1] || entry.msgid_plural || null) as string | null,
103-
},
104-
])
109+
.map(([, entry]) => {
110+
const singularFallback =
111+
locale === originalLocale ? entry.msgid : null;
112+
const pluralFallback =
113+
locale === originalLocale
114+
? entry.msgid_plural || entry.msgid
115+
: null;
116+
const hasPlural = entry.msgstr.length > 1;
117+
return [
118+
entry.msgid,
119+
{
120+
singular: entry.msgstr[0] || singularFallback,
121+
plural: hasPlural
122+
? ((entry.msgstr[1] || pluralFallback) as string | null)
123+
: null,
124+
},
125+
];
126+
})
105127
.fromPairs()
106128
.value();
107129
return result;
@@ -113,7 +135,10 @@ export function createPoContentLoader(): ILoader<PoTranslationEntry, Record<stri
113135
entry.msgid,
114136
{
115137
...entry,
116-
msgstr: [data[entry.msgid]?.singular, data[entry.msgid]?.plural || null].filter(Boolean),
138+
msgstr: [
139+
data[entry.msgid]?.singular,
140+
data[entry.msgid]?.plural || null,
141+
].filter(Boolean),
117142
},
118143
])
119144
.fromPairs()

0 commit comments

Comments
 (0)