Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
pontusab committed Jan 19, 2025
1 parent 3c23f65 commit ccf83f1
Show file tree
Hide file tree
Showing 35 changed files with 927 additions and 70 deletions.
Binary file modified bun.lockb
Binary file not shown.
1 change: 1 addition & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"fast-xml-parser": "^4.5.1",
"gettext-parser": "^8.0.0",
"jiti": "^2.4.2",
"jsdom": "^26.0.0",
"jsonrepair": "^3.11.2",
"marked": "^15.0.6",
"node-html-parser": "^7.0.1",
Expand Down
23 changes: 22 additions & 1 deletion packages/cli/src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,36 @@ type Format =
| "properties"
| "android"
| "ios-strings"
| "ios-stringsdict"
| "md"
| "html"
| "txt"
| "ts";
| "ts"
| "po"
| "xliff"
| "csv"
| "resx"
| "arb";

const SUPPORTED_FORMATS = [
{ value: "json" as const, label: "JSON (.json)" },
{ value: "yaml" as const, label: "YAML (.yml, .yaml)" },
{ value: "properties" as const, label: "Java Properties (.properties)" },
{ value: "android" as const, label: "Android XML (.xml)" },
{ value: "ios-strings" as const, label: "iOS Strings (.strings)" },
{
value: "ios-stringsdict" as const,
label: "iOS Stringsdict (.stringsdict)",
},
{ value: "md" as const, label: "Markdown (.md)" },
{ value: "html" as const, label: "HTML (.html)" },
{ value: "txt" as const, label: "Text (.txt)" },
{ value: "ts" as const, label: "TypeScript (.ts)" },
{ value: "po" as const, label: "Gettext PO (.po)" },
{ value: "xliff" as const, label: "XLIFF (.xlf, .xliff)" },
{ value: "csv" as const, label: "CSV (.csv)" },
{ value: "resx" as const, label: ".NET RESX (.resx)" },
{ value: "arb" as const, label: "Flutter ARB (.arb)" },
];

const FORMAT_EXAMPLES: Record<Format, string> = {
Expand All @@ -34,10 +49,16 @@ const FORMAT_EXAMPLES: Record<Format, string> = {
properties: "src/locales/messages_[locale].properties",
android: "res/values-[locale]/strings.xml",
"ios-strings": "[locale].lproj/Localizable.strings",
"ios-stringsdict": "[locale].lproj/Localizable.stringsdict",
md: "src/docs/[locale]/*.md",
html: "src/content/[locale]/**/*.html",
txt: "src/content/[locale]/**/*.txt",
ts: "src/locales/[locale].ts",
po: "src/locales/[locale].po",
xliff: "src/locales/[locale].xlf",
csv: "src/locales/[locale].csv",
resx: "src/locales/[locale].resx",
arb: "lib/l10n/app_[locale].arb",
};

export async function commands() {
Expand Down
7 changes: 6 additions & 1 deletion packages/cli/src/commands/translate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,12 @@ export async function translateCommand(args: string[] = []) {
...translatedContent,
};

const serialized = await parser.serialize(mergedContent);
const serialized = await parser.serialize(
targetLocale,
mergedContent,
existingContent,
);

await writeFile(targetPath, serialized, "utf-8");

if (translationInput.length > 0) {
Expand Down
6 changes: 5 additions & 1 deletion packages/cli/src/parsers/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,9 @@ export const parserOptionsSchema = z.object({

export interface Parser {
parse(input: string): Promise<Record<string, string>>;
serialize(data: Record<string, string>): Promise<string>;
serialize(
locale: string,
data: Record<string, string>,
originalData?: Record<string, string>,
): Promise<string>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,14 @@ describe("Android XML parser", () => {
message: "World",
};

const result = await parser.serialize(input);
const result = await parser.serialize("en", input);
expect(result).toContain('<?xml version="1.0" encoding="utf-8"?>');
expect(result).toContain('<string name="greeting">Hello</string>');
expect(result).toContain('<string name="message">World</string>');
});

it("should handle empty object", async () => {
const result = await parser.serialize({});
const result = await parser.serialize("en", {});
expect(result).toContain('<?xml version="1.0" encoding="utf-8"?>');
expect(result).toContain("<resources/>");
});
Expand All @@ -71,7 +71,7 @@ describe("Android XML parser", () => {
message: "Hello & World < > \" '",
};

const result = await parser.serialize(input);
const result = await parser.serialize("en", input);
expect(result).toContain('<?xml version="1.0" encoding="utf-8"?>');
expect(result).toContain(
'<string name="message">Hello &amp; World &lt; &gt; " \'</string>',
Expand Down
67 changes: 67 additions & 0 deletions packages/cli/src/parsers/formats/__tests__/arb.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { describe, expect, it } from "bun:test";
import { createArbParser } from "../../formats/arb.ts";

describe("ARB parser", () => {
const parser = createArbParser();

describe("parse", () => {
it("should parse valid ARB file", async () => {
const input = `{
"@@locale": "en",
"greeting": "Hello",
"@greeting": {
"description": "A greeting message"
},
"message": "World",
"@message": {
"description": "A message"
}
}`;

const result = await parser.parse(input);
expect(result).toEqual({
greeting: "Hello",
message: "World",
});
});

it("should handle empty ARB file", async () => {
const input = "{}";
const result = await parser.parse(input);
expect(result).toEqual({});
});

it("should throw on invalid JSON", async () => {
const input = "invalid json content";
await expect(parser.parse(input)).rejects.toThrow(
"Failed to parse ARB translations",
);
});
});

describe("serialize", () => {
it("should serialize translations to ARB format", async () => {
const input = {
greeting: "Hello",
message: "World",
};

const result = await parser.serialize("en", input);
const parsed = JSON.parse(result);
expect(parsed).toEqual({
"@@locale": "en",
greeting: "Hello",
message: "World",
});
});

it("should handle empty translations", async () => {
const input = {};
const result = await parser.serialize("fr", input);
const parsed = JSON.parse(result);
expect(parsed).toEqual({
"@@locale": "fr",
});
});
});
});
93 changes: 93 additions & 0 deletions packages/cli/src/parsers/formats/__tests__/csv.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { describe, expect, test } from "bun:test";
import { createCsvParser } from "../csv.js";

describe("CSV Parser", () => {
test("parses CSV with id and value columns", async () => {
const parser = createCsvParser();
const input = `id,value
greeting,Hello
farewell,Goodbye`;

const result = await parser.parse(input);

expect(result).toEqual({
greeting: "Hello",
farewell: "Goodbye",
});
});

test("ignores rows without id or value", async () => {
const parser = createCsvParser();
const input = `id,value
greeting,Hello
,Goodbye
invalid_row,
,`;

const result = await parser.parse(input);

expect(result).toEqual({
greeting: "Hello",
});
});

test("throws error on invalid CSV", async () => {
const parser = createCsvParser();
const input = "invalid,csv,format";

await expect(parser.parse(input)).rejects.toThrow(
"Failed to parse CSV translations",
);
});

test("serializes data to CSV format", async () => {
const parser = createCsvParser();
const data = {
greeting: "Hello",
farewell: "Goodbye",
};

const result = await parser.serialize("en", data);

expect(result).toEqual(`id,value
greeting,Hello
farewell,Goodbye
`);
});

test("preserves existing columns when serializing", async () => {
const parser = createCsvParser();
// First parse a CSV with context data to initialize metadata
const initialInput = `id,value,context
greeting,Hello,Welcome message
farewell,Goodbye,Exit message`;
await parser.parse(initialInput);

const data = {
greeting: "Hola",
farewell: "Adios",
newKey: "New Value",
};

const result = await parser.serialize("en", data);

expect(result).toEqual(`id,value,context
greeting,Hola,Welcome message
farewell,Adios,Exit message
newKey,New Value,
`);
});

test("handles empty input when serializing", async () => {
const parser = createCsvParser();
const data = {
greeting: "Hello",
};

const result = await parser.serialize("en", data);

expect(result).toEqual(`id,value
greeting,Hello
`);
});
});
Loading

0 comments on commit ccf83f1

Please sign in to comment.