Skip to content

Commit 8ffff97

Browse files
authored
fix: preserve trailing new line (#457)
Remove new line loader. Move functionality to text file loader. Use target locale file as source of truth to preserve trailing new line. If it does not exist, use source locale file as source of truth.
1 parent 5f2cc49 commit 8ffff97

5 files changed

Lines changed: 140 additions & 59 deletions

File tree

.changeset/strong-mangos-peel.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 trailing new lines

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

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
22
import _ from "lodash";
33
import fs from "fs/promises";
44
import createBucketLoader from "./index";
5+
import createTextFileLoader from "./text-file";
56

67
describe("bucket loaders", () => {
78
beforeEach(() => {
@@ -1345,6 +1346,92 @@ Mundo!`;
13451346
});
13461347
});
13471348
});
1349+
1350+
describe("text-file", () => {
1351+
describe("when there is no target locale file", () => {
1352+
it("should preserve trailing new line based on the source locale", async () => {
1353+
setupFileMocks();
1354+
1355+
const input = "Hello\n";
1356+
const expectedOutput = "Hola\n";
1357+
1358+
mockFileOperationsForPaths({
1359+
"i18n/en.txt": input,
1360+
"i18n/es.txt": "",
1361+
});
1362+
1363+
const textFileLoader = createTextFileLoader("i18n/[locale].txt");
1364+
textFileLoader.setDefaultLocale("en");
1365+
await textFileLoader.pull("en");
1366+
1367+
await textFileLoader.push("es", "Hola");
1368+
1369+
expect(fs.writeFile).toHaveBeenCalledWith("i18n/es.txt", expectedOutput, { encoding: "utf-8", flag: "w" });
1370+
});
1371+
1372+
it("should not add trailing new line based on the source locale", async () => {
1373+
setupFileMocks();
1374+
1375+
const input = "Hello";
1376+
const expectedOutput = "Hola";
1377+
1378+
mockFileOperationsForPaths({
1379+
"i18n/en.txt": input,
1380+
"i18n/es.txt": "",
1381+
});
1382+
1383+
const textFileLoader = createTextFileLoader("i18n/[locale].txt");
1384+
textFileLoader.setDefaultLocale("en");
1385+
await textFileLoader.pull("en");
1386+
1387+
await textFileLoader.push("es", "Hola");
1388+
1389+
expect(fs.writeFile).toHaveBeenCalledWith("i18n/es.txt", expectedOutput, { encoding: "utf-8", flag: "w" });
1390+
});
1391+
});
1392+
1393+
describe("when there is a target locale file", () => {
1394+
it("should preserve trailing new lines based on the target locale", async () => {
1395+
setupFileMocks();
1396+
1397+
const input = "Hello";
1398+
const expectedOutput = "Hola\n";
1399+
1400+
mockFileOperationsForPaths({
1401+
"i18n/en.txt": input,
1402+
"i18n/es.txt": "Foo\n",
1403+
});
1404+
1405+
const textFileLoader = createTextFileLoader("i18n/[locale].txt");
1406+
textFileLoader.setDefaultLocale("en");
1407+
await textFileLoader.pull("en");
1408+
1409+
await textFileLoader.push("es", "Hola");
1410+
1411+
expect(fs.writeFile).toHaveBeenCalledWith("i18n/es.txt", expectedOutput, { encoding: "utf-8", flag: "w" });
1412+
});
1413+
1414+
it("should not add trailing new line based on the target locale", async () => {
1415+
setupFileMocks();
1416+
1417+
const input = "Hello\n";
1418+
const expectedOutput = "Hola";
1419+
1420+
mockFileOperationsForPaths({
1421+
"i18n/en.txt": input,
1422+
"i18n/es.txt": "Foo",
1423+
});
1424+
1425+
const textFileLoader = createTextFileLoader("i18n/[locale].txt");
1426+
textFileLoader.setDefaultLocale("en");
1427+
await textFileLoader.pull("en");
1428+
1429+
await textFileLoader.push("es", "Hola");
1430+
1431+
expect(fs.writeFile).toHaveBeenCalledWith("i18n/es.txt", expectedOutput, { encoding: "utf-8", flag: "w" });
1432+
});
1433+
});
1434+
});
13481435
});
13491436

13501437
// Helper functions
@@ -1373,3 +1460,15 @@ function mockFileOperations(input: string) {
13731460
(fs.readFile as any).mockImplementation(() => Promise.resolve(input));
13741461
(fs.writeFile as any).mockImplementation(() => Promise.resolve());
13751462
}
1463+
1464+
function mockFileOperationsForPaths(input: Record<string, string>) {
1465+
(fs.access as any).mockImplementation((path) =>
1466+
input.hasOwnProperty(path) ? Promise.resolve() : Promise.reject(`fs.access: ${path} not mocked`),
1467+
);
1468+
(fs.readFile as any).mockImplementation((path) =>
1469+
input.hasOwnProperty(path) ? Promise.resolve(input[path]) : Promise.reject(`fs.readFile: ${path} not mocked`),
1470+
);
1471+
(fs.writeFile as any).mockImplementation((path) =>
1472+
input.hasOwnProperty(path) ? Promise.resolve() : Promise.reject(`fs:writeFile: ${path} not mocked`),
1473+
);
1474+
}

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

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ import createVttLoader from "./vtt";
2727
import createVariableLoader from "./variable";
2828
import createSyncLoader from "./sync";
2929
import createPlutilJsonTextLoader from "./plutil-json-loader";
30-
import createNewLineLoader from "./new-line";
3130

3231
export default function createBucketLoader(
3332
bucketType: Z.infer<typeof bucketTypeSchema>,
@@ -39,7 +38,6 @@ export default function createBucketLoader(
3938
case "android":
4039
return composeLoaders(
4140
createTextFileLoader(bucketPathPattern),
42-
createNewLineLoader(),
4341
createAndroidLoader(),
4442
createFlatLoader(),
4543
createSyncLoader(),
@@ -48,7 +46,6 @@ export default function createBucketLoader(
4846
case "csv":
4947
return composeLoaders(
5048
createTextFileLoader(bucketPathPattern),
51-
createNewLineLoader(),
5249
createCsvLoader(),
5350
createFlatLoader(),
5451
createSyncLoader(),
@@ -57,7 +54,6 @@ export default function createBucketLoader(
5754
case "html":
5855
return composeLoaders(
5956
createTextFileLoader(bucketPathPattern),
60-
createNewLineLoader(),
6157
createPrettierLoader({ parser: "html", alwaysFormat: true }),
6258
createHtmlLoader(),
6359
createSyncLoader(),
@@ -66,7 +62,6 @@ export default function createBucketLoader(
6662
case "json":
6763
return composeLoaders(
6864
createTextFileLoader(bucketPathPattern),
69-
createNewLineLoader(),
7065
createPrettierLoader({ parser: "json" }),
7166
createJsonLoader(),
7267
createFlatLoader(),
@@ -76,7 +71,6 @@ export default function createBucketLoader(
7671
case "markdown":
7772
return composeLoaders(
7873
createTextFileLoader(bucketPathPattern),
79-
createNewLineLoader(),
8074
createPrettierLoader({ parser: "markdown" }),
8175
createMarkdownLoader(),
8276
createSyncLoader(),
@@ -85,7 +79,6 @@ export default function createBucketLoader(
8579
case "po":
8680
return composeLoaders(
8781
createTextFileLoader(bucketPathPattern),
88-
createNewLineLoader(),
8982
createPoLoader(),
9083
createFlatLoader(),
9184
createSyncLoader(),
@@ -95,23 +88,20 @@ export default function createBucketLoader(
9588
case "properties":
9689
return composeLoaders(
9790
createTextFileLoader(bucketPathPattern),
98-
createNewLineLoader(),
9991
createPropertiesLoader(),
10092
createSyncLoader(),
10193
createUnlocalizableLoader(),
10294
);
10395
case "xcode-strings":
10496
return composeLoaders(
10597
createTextFileLoader(bucketPathPattern),
106-
createNewLineLoader(),
10798
createXcodeStringsLoader(),
10899
createSyncLoader(),
109100
createUnlocalizableLoader(),
110101
);
111102
case "xcode-stringsdict":
112103
return composeLoaders(
113104
createTextFileLoader(bucketPathPattern),
114-
createNewLineLoader(),
115105
createXcodeStringsdictLoader(),
116106
createFlatLoader(),
117107
createSyncLoader(),
@@ -120,7 +110,6 @@ export default function createBucketLoader(
120110
case "xcode-xcstrings":
121111
return composeLoaders(
122112
createTextFileLoader(bucketPathPattern),
123-
createNewLineLoader(),
124113
createPlutilJsonTextLoader(),
125114
createJsonLoader(),
126115
createXcodeXcstringsLoader(),
@@ -132,7 +121,6 @@ export default function createBucketLoader(
132121
case "yaml":
133122
return composeLoaders(
134123
createTextFileLoader(bucketPathPattern),
135-
createNewLineLoader(),
136124
createPrettierLoader({ parser: "yaml" }),
137125
createYamlLoader(),
138126
createFlatLoader(),
@@ -142,7 +130,6 @@ export default function createBucketLoader(
142130
case "yaml-root-key":
143131
return composeLoaders(
144132
createTextFileLoader(bucketPathPattern),
145-
createNewLineLoader(),
146133
createPrettierLoader({ parser: "yaml" }),
147134
createYamlLoader(),
148135
createRootKeyLoader(true),
@@ -153,7 +140,6 @@ export default function createBucketLoader(
153140
case "flutter":
154141
return composeLoaders(
155142
createTextFileLoader(bucketPathPattern),
156-
createNewLineLoader(),
157143
createPrettierLoader({ parser: "json" }),
158144
createJsonLoader(),
159145
createFlutterLoader(),
@@ -164,7 +150,6 @@ export default function createBucketLoader(
164150
case "xliff":
165151
return composeLoaders(
166152
createTextFileLoader(bucketPathPattern),
167-
createNewLineLoader(),
168153
createXliffLoader(),
169154
createFlatLoader(),
170155
createSyncLoader(),
@@ -173,7 +158,6 @@ export default function createBucketLoader(
173158
case "xml":
174159
return composeLoaders(
175160
createTextFileLoader(bucketPathPattern),
176-
createNewLineLoader(),
177161
createXmlLoader(),
178162
createFlatLoader(),
179163
createSyncLoader(),
@@ -182,7 +166,6 @@ export default function createBucketLoader(
182166
case "srt":
183167
return composeLoaders(
184168
createTextFileLoader(bucketPathPattern),
185-
createNewLineLoader(),
186169
createSrtLoader(),
187170
createSyncLoader(),
188171
createUnlocalizableLoader(),
@@ -197,7 +180,6 @@ export default function createBucketLoader(
197180
case "vtt":
198181
return composeLoaders(
199182
createTextFileLoader(bucketPathPattern),
200-
createNewLineLoader(),
201183
createVttLoader(),
202184
createSyncLoader(),
203185
createUnlocalizableLoader(),

packages/cli/src/cli/loaders/new-line.ts

Lines changed: 0 additions & 24 deletions
This file was deleted.

packages/cli/src/cli/loaders/text-file.ts

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,35 +6,54 @@ import { createLoader } from "./_utils";
66
export default function createTextFileLoader(pathPattern: string): ILoader<void, string> {
77
return createLoader({
88
async pull(locale) {
9-
const draftPath = pathPattern.replace("[locale]", locale);
10-
const finalPath = path.resolve(draftPath);
11-
12-
// Handle non-existent files
13-
const exists = await fs
14-
.access(finalPath)
15-
.then(() => true)
16-
.catch(() => false);
17-
if (!exists) {
18-
return "";
19-
}
20-
21-
const result = await fs.readFile(finalPath, "utf-8");
22-
return result;
9+
const result = await readFileForLocale(pathPattern, locale);
10+
const trimmedResult = result.trim();
11+
return trimmedResult;
2312
},
24-
async push(locale, data) {
13+
async push(locale, data, _, originalLocale) {
2514
const draftPath = pathPattern.replace("[locale]", locale);
2615
const finalPath = path.resolve(draftPath);
2716

2817
// Create parent directories if needed
2918
const dirPath = path.dirname(finalPath);
3019
await fs.mkdir(dirPath, { recursive: true });
3120

32-
// Ensure consistent line endings
33-
const finalPayload = data.trim();
21+
const trimmedPayload = data.trim();
22+
23+
// Add trailing new line if needed
24+
const trailingNewLine = await getTrailingNewLine(pathPattern, locale, originalLocale);
25+
let finalPayload = trimmedPayload + trailingNewLine;
26+
3427
await fs.writeFile(finalPath, finalPayload, {
3528
encoding: "utf-8",
3629
flag: "w",
3730
});
3831
},
3932
});
4033
}
34+
35+
async function readFileForLocale(pathPattern: string, locale: string) {
36+
const draftPath = pathPattern.replace("[locale]", locale);
37+
const finalPath = path.resolve(draftPath);
38+
const exists = await fs
39+
.access(finalPath)
40+
.then(() => true)
41+
.catch(() => false);
42+
if (!exists) {
43+
return "";
44+
}
45+
return fs.readFile(finalPath, "utf-8");
46+
}
47+
48+
async function getTrailingNewLine(pathPattern: string, locale: string, originalLocale: string) {
49+
let templateData = await readFileForLocale(pathPattern, locale);
50+
if (!templateData) {
51+
templateData = await readFileForLocale(pathPattern, originalLocale);
52+
}
53+
54+
if (templateData?.match(/[\r\n]$/)) {
55+
const ending = templateData?.includes("\r\n") ? "\r\n" : templateData?.includes("\r") ? "\r" : "\n";
56+
return ending;
57+
}
58+
return "";
59+
}

0 commit comments

Comments
 (0)