Skip to content

Commit a096300

Browse files
authored
feat: support php buckets (#485)
1 parent faea102 commit a096300

File tree

8 files changed

+166
-2
lines changed

8 files changed

+166
-2
lines changed

.changeset/curly-pandas-hear.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 php buckets

packages/cli/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,10 @@
4444
"author": "",
4545
"license": "Apache-2.0",
4646
"dependencies": {
47-
"@lingo.dev/_sdk": "workspace:*",
48-
"@lingo.dev/_spec": "workspace:*",
4947
"@datocms/cma-client-node": "^3.4.0",
5048
"@inquirer/prompts": "^7.2.3",
49+
"@lingo.dev/_sdk": "workspace:*",
50+
"@lingo.dev/_spec": "workspace:*",
5151
"@paralleldrive/cuid2": "^2.2.2",
5252
"chalk": "^5.4.1",
5353
"cors": "^2.8.5",
@@ -81,6 +81,7 @@
8181
"open": "^10.1.0",
8282
"ora": "^8.1.1",
8383
"p-limit": "^6.2.0",
84+
"php-array-reader": "^2.1.2",
8485
"plist": "^3.1.0",
8586
"prettier": "^3.4.2",
8687
"properties-parser": "^0.6.0",

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

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1560,6 +1560,63 @@ Mundo!`;
15601560
});
15611561
});
15621562
});
1563+
1564+
describe("php bucket loader", () => {
1565+
it("should load php array", async () => {
1566+
setupFileMocks();
1567+
1568+
const input = `<?php return ['button.title' => 'Submit'];`;
1569+
const expectedOutput = { "button.title": "Submit" };
1570+
1571+
mockFileOperations(input);
1572+
1573+
const jsonLoader = createBucketLoader("php", "i18n/[locale].php");
1574+
jsonLoader.setDefaultLocale("en");
1575+
const data = await jsonLoader.pull("en");
1576+
1577+
expect(data).toEqual(expectedOutput);
1578+
});
1579+
1580+
it("should save php array", async () => {
1581+
setupFileMocks();
1582+
1583+
const input = `<?php
1584+
// this is locale
1585+
1586+
return array(
1587+
'button.title' => 'Submit',
1588+
'button.description' => ['Hello', 'Goodbye'],
1589+
'button.index' => 1,
1590+
'button.class' => null,
1591+
);`;
1592+
const expectedOutput = `<?php
1593+
// this is locale
1594+
1595+
return array(
1596+
'button.title' => 'Enviar',
1597+
'button.description' => array(
1598+
'Hola',
1599+
'Adiós'
1600+
),
1601+
'button.index' => 1,
1602+
'button.class' => null
1603+
);`;
1604+
1605+
mockFileOperations(input);
1606+
1607+
const jsonLoader = createBucketLoader("php", "i18n/[locale].php");
1608+
jsonLoader.setDefaultLocale("en");
1609+
await jsonLoader.pull("en");
1610+
1611+
await jsonLoader.push("es", {
1612+
"button.title": "Enviar",
1613+
"button.description/0": "Hola",
1614+
"button.description/1": "Adiós",
1615+
});
1616+
1617+
expect(fs.writeFile).toHaveBeenCalledWith("i18n/es.php", expectedOutput, { encoding: "utf-8", flag: "w" });
1618+
});
1619+
});
15631620
});
15641621

15651622
// Helper functions

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

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

3132
type BucketLoaderOptions = {
3233
isCacheRestore?: boolean;
@@ -189,5 +190,13 @@ export default function createBucketLoader(
189190
createSyncLoader(),
190191
createUnlocalizableLoader(isCacheRestore),
191192
);
193+
case "php":
194+
return composeLoaders(
195+
createTextFileLoader(bucketPathPattern),
196+
createPhpLoader(),
197+
createSyncLoader(),
198+
createFlatLoader(),
199+
createUnlocalizableLoader(),
200+
);
192201
}
193202
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { ILoader } from "./_types";
2+
import { createLoader } from "./_utils";
3+
import { fromString } from "php-array-reader";
4+
5+
export default function createPhpLoader(): ILoader<string, Record<string, any>> {
6+
return createLoader({
7+
pull: async (locale, input) => {
8+
try {
9+
const output = fromString(input);
10+
return output;
11+
} catch (error) {
12+
throw new Error(`Error parsing PHP file for locale ${locale}`);
13+
}
14+
},
15+
push: async (locale, data, originalInput) => {
16+
const output = toPhpString(data, originalInput);
17+
return output;
18+
},
19+
});
20+
}
21+
22+
function toPhpString(data: Record<string, any>, originalPhpString: string | null) {
23+
const defaultFilePrefix = "<?php\n\n";
24+
if (originalPhpString) {
25+
const [filePrefix = defaultFilePrefix] = originalPhpString.split("return ");
26+
const shortArraySyntax = !originalPhpString.includes("array(");
27+
const output = `${filePrefix}return ${toPhpArray(data, shortArraySyntax)};`;
28+
return output;
29+
}
30+
return `${defaultFilePrefix}return ${toPhpArray(data)};`;
31+
}
32+
33+
function toPhpArray(data: any, shortSyntax = true, indentLevel = 1): string {
34+
if (data === null || data === undefined) {
35+
return "null";
36+
}
37+
if (typeof data === "string") {
38+
return `'${escapePhpString(data)}'`;
39+
}
40+
if (typeof data === "number") {
41+
return data.toString();
42+
}
43+
if (typeof data === "boolean") {
44+
return data ? "true" : "false";
45+
}
46+
47+
const arrayStart = shortSyntax ? "[" : "array(";
48+
const arrayEnd = shortSyntax ? "]" : ")";
49+
50+
if (Array.isArray(data)) {
51+
return `${arrayStart}\n${data
52+
.map((value) => `${indent(indentLevel)}${toPhpArray(value, shortSyntax, indentLevel + 1)}`)
53+
.join(",\n")}\n${indent(indentLevel - 1)}${arrayEnd}`;
54+
}
55+
56+
const output = `${arrayStart}\n${Object.entries(data)
57+
.map(([key, value]) => `${indent(indentLevel)}'${key}' => ${toPhpArray(value, shortSyntax, indentLevel + 1)}`)
58+
.join(",\n")}\n${indent(indentLevel - 1)}${arrayEnd}`;
59+
return output;
60+
}
61+
62+
function indent(level: number) {
63+
return " ".repeat(level);
64+
}
65+
66+
function escapePhpString(str: string) {
67+
return str
68+
.replaceAll("\\", "\\\\")
69+
.replaceAll("'", "\\'")
70+
.replaceAll("\r", "\\r")
71+
.replaceAll("\n", "\\n")
72+
.replaceAll("\t", "\\t");
73+
}

packages/cli/src/cli/utils/find-locale-paths.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ export default function findLocaleFiles(bucket: string) {
1616
return findLocaleFilesWithExtension(".xml");
1717
case "markdown":
1818
return findLocaleFilesWithExtension(".md");
19+
case "php":
20+
return findLocaleFilesWithExtension(".php");
1921
case "xcode-xcstrings":
2022
return findLocaleFilesForFilename("Localizable.xcstrings");
2123
case "xcode-strings":

packages/spec/src/formats.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export const bucketTypes = [
2020
"dato",
2121
"compiler",
2222
"vtt",
23+
"php",
2324
] as const;
2425

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

pnpm-lock.yaml

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)