Skip to content

Commit f644123

Browse files
feat: add support for plain TXT files (#981)
* feat: add support for plain TXT files - Add 'txt' to bucketTypes array in formats.ts - Add 'txt' case to loader factory switch statement - Use simple loader composition: textFileLoader + syncLoader + unlocalizableLoader - Enables translation of fastlane App Store metadata and other plain text files Fixes #980 Co-Authored-By: [email protected] <[email protected]> * chore: add changeset for TXT file support feature Co-Authored-By: [email protected] <[email protected]> * test: add comprehensive tests for TXT file loader - Add tests for loading TXT files as single translatable content - Add tests for saving translated TXT content - Add test for handling empty TXT files - Follow established test patterns with setupFileMocks and mockFileOperations - Tests verify TXT files use 'content' key for entire file content - All tests pass successfully Co-Authored-By: [email protected] <[email protected]> * fix: apply Prettier formatting to TXT loader files - Fix code style issues in txt.ts and index.spec.ts - Resolve CI formatting check failure Co-Authored-By: [email protected] <[email protected]> * feat: implement line-by-line splitting for TXT loader - Split TXT file content by lines into numeric keys (1, 2, 3, etc.) - Use single space placeholder for empty lines to avoid unlocalizable filter - Add comprehensive tests for empty line preservation and file reconstruction - Enables better chunking for large TXT files since each line becomes separate translatable unit - Follows established patterns from SRT, CSV, and PO loaders Co-Authored-By: [email protected] <[email protected]> * refactor: remove space placeholder workaround from TXT loader - Remove space placeholder logic for empty lines in pull/push methods - Let unlocalizable loader handle empty strings automatically via filtering/restoration - Update tests to expect empty strings to be filtered out during pull - Simplifies implementation while maintaining correct empty line behavior - User feedback: unlocalizable loader already handles this properly Co-Authored-By: [email protected] <[email protected]> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: [email protected] <[email protected]>
1 parent 0843509 commit f644123

File tree

5 files changed

+196
-0
lines changed

5 files changed

+196
-0
lines changed

.changeset/curly-bats-tell.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@lingo.dev/_spec": minor
3+
"lingo.dev": minor
4+
---
5+
6+
Add support for plain TXT files to enable translation of fastlane App Store metadata and other plain text content

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

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2621,6 +2621,158 @@ ${script}`;
26212621
);
26222622
});
26232623
});
2624+
2625+
describe("txt bucket loader", () => {
2626+
it("should load txt", async () => {
2627+
setupFileMocks();
2628+
2629+
const input = `Welcome to our application!
2630+
This is a sample text file for fastlane metadata.
2631+
It contains app description that needs to be translated.`;
2632+
2633+
const expectedOutput = {
2634+
"1": "Welcome to our application!",
2635+
"2": "This is a sample text file for fastlane metadata.",
2636+
"3": "It contains app description that needs to be translated.",
2637+
};
2638+
2639+
mockFileOperations(input);
2640+
2641+
const txtLoader = createBucketLoader(
2642+
"txt",
2643+
"fastlane/metadata/[locale]/description.txt",
2644+
{
2645+
defaultLocale: "en",
2646+
},
2647+
);
2648+
txtLoader.setDefaultLocale("en");
2649+
const data = await txtLoader.pull("en");
2650+
2651+
expect(data).toEqual(expectedOutput);
2652+
});
2653+
2654+
it("should save txt", async () => {
2655+
setupFileMocks();
2656+
2657+
const input = `Welcome to our application!
2658+
This is a sample text file for fastlane metadata.
2659+
It contains app description that needs to be translated.`;
2660+
2661+
const payload = {
2662+
"1": "¡Bienvenido a nuestra aplicación!",
2663+
"2": "Este es un archivo de texto de muestra para metadatos de fastlane.",
2664+
"3": "Contiene la descripción de la aplicación que necesita ser traducida.",
2665+
};
2666+
2667+
const expectedOutput = `¡Bienvenido a nuestra aplicación!
2668+
Este es un archivo de texto de muestra para metadatos de fastlane.
2669+
Contiene la descripción de la aplicación que necesita ser traducida.`;
2670+
2671+
mockFileOperations(input);
2672+
2673+
const txtLoader = createBucketLoader(
2674+
"txt",
2675+
"fastlane/metadata/[locale]/description.txt",
2676+
{
2677+
defaultLocale: "en",
2678+
},
2679+
);
2680+
txtLoader.setDefaultLocale("en");
2681+
await txtLoader.pull("en");
2682+
2683+
await txtLoader.push("es", payload);
2684+
2685+
expect(fs.writeFile).toHaveBeenCalledWith(
2686+
"fastlane/metadata/es/description.txt",
2687+
expectedOutput,
2688+
{ encoding: "utf-8", flag: "w" },
2689+
);
2690+
});
2691+
2692+
it("should handle empty txt files", async () => {
2693+
setupFileMocks();
2694+
2695+
const input = "";
2696+
const expectedOutput = {};
2697+
2698+
mockFileOperations(input);
2699+
2700+
const txtLoader = createBucketLoader(
2701+
"txt",
2702+
"fastlane/metadata/[locale]/description.txt",
2703+
{
2704+
defaultLocale: "en",
2705+
},
2706+
);
2707+
txtLoader.setDefaultLocale("en");
2708+
const data = await txtLoader.pull("en");
2709+
2710+
expect(data).toEqual(expectedOutput);
2711+
});
2712+
2713+
it("should filter out empty lines during pull", async () => {
2714+
setupFileMocks();
2715+
2716+
const input = `Line 1
2717+
2718+
Line 3`;
2719+
const expectedOutput = {
2720+
"1": "Line 1",
2721+
"3": "Line 3",
2722+
};
2723+
2724+
mockFileOperations(input);
2725+
2726+
const txtLoader = createBucketLoader(
2727+
"txt",
2728+
"fastlane/metadata/[locale]/description.txt",
2729+
{
2730+
defaultLocale: "en",
2731+
},
2732+
);
2733+
txtLoader.setDefaultLocale("en");
2734+
const data = await txtLoader.pull("en");
2735+
2736+
expect(data).toEqual(expectedOutput);
2737+
});
2738+
2739+
it("should reconstruct file with empty lines restored", async () => {
2740+
setupFileMocks();
2741+
2742+
const input = `Line 1
2743+
2744+
Line 3`;
2745+
2746+
const payload = {
2747+
"1": "Línea 1",
2748+
"3": "Línea 3",
2749+
};
2750+
2751+
const expectedOutput = `Línea 1
2752+
2753+
Línea 3`;
2754+
2755+
mockFileOperations(input);
2756+
2757+
const txtLoader = createBucketLoader(
2758+
"txt",
2759+
"fastlane/metadata/[locale]/description.txt",
2760+
{
2761+
defaultLocale: "en",
2762+
},
2763+
);
2764+
txtLoader.setDefaultLocale("en");
2765+
await txtLoader.pull("en");
2766+
2767+
await txtLoader.push("es", payload);
2768+
2769+
expect(fs.writeFile).toHaveBeenCalledWith(
2770+
"fastlane/metadata/es/description.txt",
2771+
expectedOutput,
2772+
{ encoding: "utf-8", flag: "w" },
2773+
);
2774+
});
2775+
});
26242776
});
26252777

26262778
function setupFileMocks() {

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import createMdxLockedPatternsLoader from "./mdx2/locked-patterns";
4141
import createIgnoredKeysLoader from "./ignored-keys";
4242
import createEjsLoader from "./ejs";
4343
import createEnsureKeyOrderLoader from "./ensure-key-order";
44+
import createTxtLoader from "./txt";
4445

4546
type BucketLoaderOptions = {
4647
returnUnlocalizedKeys?: boolean;
@@ -278,5 +279,12 @@ export default function createBucketLoader(
278279
createIgnoredKeysLoader(ignoredKeys || []),
279280
createUnlocalizableLoader(options.returnUnlocalizedKeys),
280281
);
282+
case "txt":
283+
return composeLoaders(
284+
createTextFileLoader(bucketPathPattern),
285+
createTxtLoader(),
286+
createSyncLoader(),
287+
createUnlocalizableLoader(options.returnUnlocalizedKeys),
288+
);
281289
}
282290
}

packages/cli/src/cli/loaders/txt.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { ILoader } from "./_types";
2+
import { createLoader } from "./_utils";
3+
4+
export default function createTxtLoader(): ILoader<
5+
string,
6+
Record<string, string>
7+
> {
8+
return createLoader({
9+
async pull(locale, input) {
10+
const result: Record<string, string> = {};
11+
12+
if (input !== undefined && input !== null && input !== "") {
13+
const lines = input.split("\n");
14+
lines.forEach((line, index) => {
15+
result[String(index + 1)] = line;
16+
});
17+
}
18+
19+
return result;
20+
},
21+
22+
async push(locale, payload) {
23+
const sortedEntries = Object.entries(payload).sort(
24+
([a], [b]) => parseInt(a) - parseInt(b),
25+
);
26+
return sortedEntries.map(([_, value]) => value).join("\n");
27+
},
28+
});
29+
}

packages/spec/src/formats.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export const bucketTypes = [
2626
"po",
2727
"vue-json",
2828
"typescript",
29+
"txt",
2930
] as const;
3031

3132
export const bucketTypeSchema = Z.enum(bucketTypes);

0 commit comments

Comments
 (0)