Skip to content

Commit 091ee35

Browse files
feat(cli): add support for .po format (#275)
* feat(cli): add support for `.po` format * chore: add `.po` example
1 parent 3925a3a commit 091ee35

File tree

9 files changed

+163
-0
lines changed

9 files changed

+163
-0
lines changed

.changeset/weak-fans-sort.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@replexica/spec": minor
3+
"@replexica/cli": minor
4+
"replexica": minor
5+
---
6+
7+
add support for `.po` format

package/demo/po/en.po

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# This is a comment
2+
msgid "greeting"
3+
msgstr "Hello, {name}!"
4+
5+
msgid "farewell"
6+
msgstr "Goodbye, {name}!"

package/demo/po/es.po

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
msgid "greeting"
2+
msgstr "¡Hola, {name}!"
3+
4+
msgid "farewell"
5+
msgstr "¡Adiós, {name}!"

package/i18n.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@
4040
"demo/markdown/[locale]/ignored.md"
4141
]
4242
},
43+
"po": {
44+
"include": [
45+
"demo/po/[locale].po"
46+
]
47+
},
4348
"properties": {
4449
"include": [
4550
"demo/properties/[locale].properties"

package/i18n.lock

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,3 +178,6 @@ checksums:
178178
home/title%2Fmain: ef1fb56b8dafd605fb267df2b0a8a91f
179179
home/description%2Fdev: b866f4d6b135451633de827fc20f4fdb
180180
home/i-am-a-developer: d8feacce28faea3fd4228a1ebcb11473
181+
cd761e96e923fc52320b39dc7a0089c8:
182+
greeting: a4028aa70eaa7049318f039837199173
183+
farewell: 0361aa0c2874ff3ffca6c7ae87e1c73b

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

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,73 @@ Otro párrafo con texto en **negrita** y en *cursiva*.
406406
});
407407
});
408408

409+
describe('po bucket loader', () => {
410+
it('should load and handle various po data', async () => {
411+
setupFileMocks();
412+
413+
const input = `
414+
# This is a comment
415+
msgid "greeting"
416+
msgstr "Hello, {name}!"
417+
418+
msgid "farewell"
419+
msgstr "Goodbye, {name}!"
420+
421+
# Another comment
422+
msgid "empty"
423+
msgstr ""
424+
`.trim();
425+
const expectedOutput = {
426+
greeting: 'Hello, {name}!',
427+
farewell: 'Goodbye, {name}!',
428+
empty: ''
429+
};
430+
431+
mockFileOperations(input);
432+
433+
const poLoader = createBucketLoader('po', 'i18n/[locale].po');
434+
poLoader.setDefaultLocale('en');
435+
const data = await poLoader.pull('en');
436+
437+
expect(data).toEqual(expectedOutput);
438+
});
439+
440+
it('should save po data with variable patterns and comments', async () => {
441+
setupFileMocks();
442+
443+
const input = `
444+
# This is a comment
445+
msgid "greeting"
446+
msgstr "Hello, {name}!"
447+
`.trim();
448+
const payload = {
449+
greeting: '¡Hola, {name}!',
450+
farewell: '¡Adiós, {name}!'
451+
};
452+
const expectedOutput = `
453+
msgid "greeting"
454+
msgstr "¡Hola, {name}!"
455+
456+
msgid "farewell"
457+
msgstr "¡Adiós, {name}!"
458+
`.trim() + '\n';
459+
460+
mockFileOperations(input);
461+
462+
const poLoader = createBucketLoader('po', 'i18n/[locale].po');
463+
poLoader.setDefaultLocale('es');
464+
await poLoader.pull('es');
465+
466+
await poLoader.push('es', payload);
467+
468+
expect(fs.writeFile).toHaveBeenCalledWith(
469+
'i18n/es.po',
470+
expectedOutput,
471+
{ encoding: 'utf-8', flag: 'w' },
472+
);
473+
});
474+
});
475+
409476
describe('properties bucket loader', () => {
410477
it('should load properties data', async () => {
411478
setupFileMocks();

packages/cli/src/loaders/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import createXcodeStringsdictLoader from './xcode-stringsdict';
1818
import createXcodeXcstringsLoader from './xcode-xcstrings';
1919
import createPrettierLoader from './prettier';
2020
import createUnlocalizableLoader from './unlocalizable';
21+
import createPoLoader from './po';
2122

2223
export default function createBucketLoader(
2324
bucketType: Z.infer<typeof bucketTypeSchema>,
@@ -56,6 +57,11 @@ export default function createBucketLoader(
5657
createMarkdownLoader(),
5758
createUnlocalizableLoader(),
5859
);
60+
case 'po': return composeLoaders(
61+
createTextFileLoader(bucketPathPattern),
62+
createPoLoader(),
63+
createUnlocalizableLoader(),
64+
);
5965
case 'properties': return composeLoaders(
6066
createTextFileLoader(bucketPathPattern),
6167
createPropertiesLoader(),

packages/cli/src/loaders/po.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { ILoader } from "./_types";
2+
import { createLoader } from './_utils';
3+
4+
export default function createPoLoader(): ILoader<string, Record<string, any>> {
5+
return createLoader({
6+
async pull(locale, text) {
7+
const result: Record<string, string> = {};
8+
const lines = text.split('\n');
9+
let currentKey = '';
10+
let currentValue = '';
11+
12+
for (const line of lines) {
13+
const trimmed = line.trim();
14+
15+
// Skip empty lines and comments
16+
if (isSkippableLine(trimmed)) {
17+
continue;
18+
}
19+
20+
if (trimmed.startsWith('msgid')) {
21+
// If we have a current key, save it before moving to the next
22+
if (currentKey) {
23+
result[currentKey] = currentValue;
24+
}
25+
currentKey = parseMsgId(trimmed);
26+
currentValue = ''; // Reset current value
27+
} else if (trimmed.startsWith('msgstr')) {
28+
currentValue = parseMsgStr(trimmed);
29+
}
30+
}
31+
32+
// Save the last key-value pair
33+
if (currentKey) {
34+
result[currentKey] = currentValue;
35+
}
36+
37+
return result;
38+
},
39+
async push(locale, payload) {
40+
const result = Object.entries(payload)
41+
.map(([key, value]) => {
42+
return `msgid "${key}"\nmsgstr "${value}"`;
43+
})
44+
.join('\n\n');
45+
46+
return result;
47+
}
48+
});
49+
}
50+
51+
function isSkippableLine(line: string): boolean {
52+
return !line || line.startsWith('#');
53+
}
54+
55+
function parseMsgId(line: string): string {
56+
const match = line.match(/msgid "(.*)"/);
57+
return match ? match[1] : '';
58+
}
59+
60+
function parseMsgStr(line: string): string {
61+
const match = line.match(/msgstr "(.*)"/);
62+
return match ? match[1] : '';
63+
}

packages/spec/src/formats.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export const bucketTypes = [
1313
'yaml',
1414
'yaml-root-key',
1515
'properties',
16+
'po',
1617

1718
'compiler',
1819
] as const;

0 commit comments

Comments
 (0)