Skip to content

Commit 35ba0af

Browse files
committed
feat(cli): support JSON messages in <i18n> block of .vue files
1 parent 385565d commit 35ba0af

File tree

7 files changed

+214
-61
lines changed

7 files changed

+214
-61
lines changed

.changeset/brave-beans-mix.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@lingo.dev/_spec": patch
3+
"lingo.dev": patch
4+
---
5+
6+
support JSON messages in <i18n> block of .vue files

packages/cli/src/cli/cmd/init.ts

Lines changed: 44 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -119,43 +119,56 @@ export default new InteractiveCommand()
119119
};
120120
} else {
121121
let selectedPatterns: string[] = [];
122-
const { patterns, defaultPatterns } = findLocaleFiles(options.bucket);
122+
const localeFiles = findLocaleFiles(options.bucket);
123+
124+
if (!localeFiles) {
125+
spinner.warn(
126+
`Bucket type "${options.bucket}" does not supported automatic initialization. Add paths to "i18n.json" manually.`,
127+
);
128+
newConfig.buckets = {
129+
[options.bucket]: {
130+
include: options.paths || [],
131+
},
132+
};
133+
} else {
134+
const { patterns, defaultPatterns } = localeFiles;
123135

124-
if (patterns.length > 0) {
125-
spinner.succeed("Found existing locale files:");
136+
if (patterns.length > 0) {
137+
spinner.succeed("Found existing locale files:");
126138

127-
selectedPatterns = await checkbox({
128-
message: "Select the paths to use",
129-
choices: patterns.map((value) => ({
130-
value,
131-
})),
132-
});
133-
} else {
134-
spinner.succeed("No existing locale files found.");
135-
}
139+
selectedPatterns = await checkbox({
140+
message: "Select the paths to use",
141+
choices: patterns.map((value) => ({
142+
value,
143+
})),
144+
});
145+
} else {
146+
spinner.succeed("No existing locale files found.");
147+
}
136148

137-
if (selectedPatterns.length === 0) {
138-
const useDefault = await confirm({
139-
message: `Use (and create) default path ${defaultPatterns.join(", ")}?`,
140-
});
141-
if (useDefault) {
142-
ensurePatterns(defaultPatterns, options.source);
143-
selectedPatterns = defaultPatterns;
149+
if (selectedPatterns.length === 0) {
150+
const useDefault = await confirm({
151+
message: `Use (and create) default path ${defaultPatterns.join(", ")}?`,
152+
});
153+
if (useDefault) {
154+
ensurePatterns(defaultPatterns, options.source);
155+
selectedPatterns = defaultPatterns;
156+
}
144157
}
145-
}
146158

147-
if (selectedPatterns.length === 0) {
148-
const customPaths = await input({
149-
message: "Enter paths to use",
150-
});
151-
selectedPatterns = customPaths.includes(",") ? customPaths.split(",") : customPaths.split(" ");
152-
}
159+
if (selectedPatterns.length === 0) {
160+
const customPaths = await input({
161+
message: "Enter paths to use",
162+
});
163+
selectedPatterns = customPaths.includes(",") ? customPaths.split(",") : customPaths.split(" ");
164+
}
153165

154-
newConfig.buckets = {
155-
[options.bucket]: {
156-
include: selectedPatterns || [],
157-
},
158-
};
166+
newConfig.buckets = {
167+
[options.bucket]: {
168+
include: selectedPatterns || [],
169+
},
170+
};
171+
}
159172
}
160173

161174
await saveConfig(newConfig);

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

Lines changed: 123 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1590,9 +1590,9 @@ Mundo!`;
15901590

15911591
mockFileOperations(input);
15921592

1593-
const jsonLoader = createBucketLoader("php", "i18n/[locale].php");
1594-
jsonLoader.setDefaultLocale("en");
1595-
const data = await jsonLoader.pull("en");
1593+
const phpLoader = createBucketLoader("php", "i18n/[locale].php");
1594+
phpLoader.setDefaultLocale("en");
1595+
const data = await phpLoader.pull("en");
15961596

15971597
expect(data).toEqual(expectedOutput);
15981598
});
@@ -1624,53 +1624,147 @@ return array(
16241624

16251625
mockFileOperations(input);
16261626

1627-
const jsonLoader = createBucketLoader("php", "i18n/[locale].php");
1628-
jsonLoader.setDefaultLocale("en");
1629-
await jsonLoader.pull("en");
1627+
const phpLoader = createBucketLoader("php", "i18n/[locale].php");
1628+
phpLoader.setDefaultLocale("en");
1629+
await phpLoader.pull("en");
16301630

1631-
await jsonLoader.push("es", {
1631+
await phpLoader.push("es", {
16321632
"button.title": "Enviar",
16331633
"button.description/0": "Hola",
16341634
"button.description/1": "Adiós",
16351635
});
16361636

16371637
expect(fs.writeFile).toHaveBeenCalledWith("i18n/es.php", expectedOutput, { encoding: "utf-8", flag: "w" });
16381638
});
1639+
});
16391640

1640-
describe("po bucket loader", () => {
1641-
it("should load po file", async () => {
1642-
setupFileMocks();
1641+
describe("po bucket loader", () => {
1642+
it("should load po file", async () => {
1643+
setupFileMocks();
16431644

1644-
const input = `msgid "Hello"\nmsgstr "Hello"`;
1645-
const expectedOutput = { "Hello/singular": "Hello" };
1645+
const input = `msgid "Hello"\nmsgstr "Hello"`;
1646+
const expectedOutput = { "Hello/singular": "Hello" };
16461647

1647-
mockFileOperations(input);
1648+
mockFileOperations(input);
1649+
1650+
const poLoader = createBucketLoader("po", "i18n/[locale].po");
1651+
poLoader.setDefaultLocale("en");
1652+
const data = await poLoader.pull("en");
1653+
1654+
expect(data).toEqual(expectedOutput);
1655+
});
16481656

1649-
const jsonLoader = createBucketLoader("po", "i18n/[locale].po");
1650-
jsonLoader.setDefaultLocale("en");
1651-
const data = await jsonLoader.pull("en");
1657+
it("should save po file", async () => {
1658+
setupFileMocks();
16521659

1653-
expect(data).toEqual(expectedOutput);
1660+
const input = `msgid "Hello"\nmsgstr "Hello"`;
1661+
const expectedOutput = `msgid "Hello"\nmsgstr "Hola"`;
1662+
1663+
mockFileOperations(input);
1664+
1665+
const poLoader = createBucketLoader("po", "i18n/[locale].po");
1666+
poLoader.setDefaultLocale("en");
1667+
await poLoader.pull("en");
1668+
1669+
await poLoader.push("es", {
1670+
"Hello/singular": "Hola",
16541671
});
16551672

1656-
it("should save po file", async () => {
1657-
setupFileMocks();
1673+
expect(fs.writeFile).toHaveBeenCalledWith("i18n/es.po", expectedOutput, { encoding: "utf-8", flag: "w" });
1674+
});
1675+
});
16581676

1659-
const input = `msgid "Hello"\nmsgstr "Hello"`;
1660-
const expectedOutput = `msgid "Hello"\nmsgstr "Hola"`;
1677+
describe("vue-json bucket loader", () => {
1678+
const template = `<template>
1679+
<div id="app">
1680+
<label for="locale">locale</label>
1681+
<select v-model="locale">
1682+
<option>en</option>
1683+
<option>ja</option>
1684+
</select>
1685+
<p>message: {{ $t('hello') }}</p>
1686+
</div>
1687+
</template>`;
1688+
const script = `<script>
1689+
export default {
1690+
name: 'app',
1691+
data () {
1692+
this.$i18n.locale = 'en';
1693+
return { locale: 'en' }
1694+
},
1695+
watch: {
1696+
locale (val) {
1697+
this.$i18n.locale = val
1698+
}
1699+
}
1700+
}
1701+
</script>`;
16611702

1662-
mockFileOperations(input);
1703+
it("should load vue-json file", async () => {
1704+
setupFileMocks();
16631705

1664-
const jsonLoader = createBucketLoader("po", "i18n/[locale].po");
1665-
jsonLoader.setDefaultLocale("en");
1666-
await jsonLoader.pull("en");
1706+
const input = `${template}
16671707
1668-
await jsonLoader.push("es", {
1669-
"Hello/singular": "Hola",
1670-
});
1708+
<i18n>
1709+
{
1710+
"en": {
1711+
"hello": "hello world!"
1712+
}
1713+
}
1714+
</i18n>
16711715
1672-
expect(fs.writeFile).toHaveBeenCalledWith("i18n/es.po", expectedOutput, { encoding: "utf-8", flag: "w" });
1716+
${script}`;
1717+
const expectedOutput = { hello: "hello world!" };
1718+
1719+
mockFileOperations(input);
1720+
1721+
const vueLoader = createBucketLoader("vue-json", "i18n/[locale].vue");
1722+
vueLoader.setDefaultLocale("en");
1723+
const data = await vueLoader.pull("en");
1724+
1725+
expect(data).toEqual(expectedOutput);
1726+
});
1727+
1728+
it("should save vue-json file", async () => {
1729+
setupFileMocks();
1730+
1731+
const input = `${template}
1732+
1733+
<i18n>
1734+
{
1735+
"en": {
1736+
"hello": "hello world!"
1737+
}
1738+
}
1739+
</i18n>
1740+
1741+
${script}`;
1742+
const expectedOutput = `${template}
1743+
1744+
<i18n>
1745+
{
1746+
"en": {
1747+
"hello": "hello world!"
1748+
},
1749+
"es": {
1750+
"hello": "hola mundo!"
1751+
}
1752+
}
1753+
</i18n>
1754+
1755+
${script}`;
1756+
1757+
mockFileOperations(input);
1758+
1759+
const vueLoader = createBucketLoader("vue-json", "i18n/App.vue");
1760+
vueLoader.setDefaultLocale("en");
1761+
await vueLoader.pull("en");
1762+
1763+
await vueLoader.push("es", {
1764+
hello: "hola mundo!",
16731765
});
1766+
1767+
expect(fs.writeFile).toHaveBeenCalledWith("i18n/App.vue", expectedOutput, { encoding: "utf-8", flag: "w" });
16741768
});
16751769
});
16761770
});

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import createVariableLoader from "./variable";
2828
import createSyncLoader from "./sync";
2929
import createPlutilJsonTextLoader from "./plutil-json-loader";
3030
import createPhpLoader from "./php";
31+
import createVueJsonLoader from "./vue-json";
3132

3233
type BucketLoaderOptions = {
3334
isCacheRestore?: boolean;
@@ -198,5 +199,13 @@ export default function createBucketLoader(
198199
createFlatLoader(),
199200
createUnlocalizableLoader(),
200201
);
202+
case "vue-json":
203+
return composeLoaders(
204+
createTextFileLoader(bucketPathPattern),
205+
createVueJsonLoader(),
206+
createSyncLoader(),
207+
createFlatLoader(),
208+
createUnlocalizableLoader(),
209+
);
201210
}
202211
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { jsonrepair } from "jsonrepair";
2+
import { ILoader } from "./_types";
3+
import { createLoader } from "./_utils";
4+
5+
export default function createVueJsonLoader(): ILoader<string, Record<string, any>> {
6+
return createLoader({
7+
pull: async (locale, input, ctx) => {
8+
const { i18n } = parseVueFile(input);
9+
return i18n[locale] ?? {};
10+
},
11+
push: async (locale, data, originalInput) => {
12+
const { before, i18n, after } = parseVueFile(originalInput ?? "");
13+
i18n[locale] = data;
14+
return `${before}<i18n>\n${JSON.stringify(i18n, null, 2)}\n</i18n>${after}`;
15+
},
16+
});
17+
}
18+
19+
function parseVueFile(input: string) {
20+
const [, before, jsonString = "{}", after] = input.match(/^([\s\S]*)<i18n>([\s\S]*)<\/i18n>([\s\S]*)$/) || [];
21+
22+
let i18n: Record<string, any>;
23+
try {
24+
i18n = JSON.parse(jsonString);
25+
} catch (error) {
26+
i18n = JSON.parse(jsonrepair(jsonString));
27+
}
28+
29+
return { before, after, i18n };
30+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export default function findLocaleFiles(bucket: string) {
2727
case "xcode-stringsdict":
2828
return findLocaleFilesForFilename("Localizable.stringsdict");
2929
default:
30-
throw new Error(`Unsupported bucket type: ${bucket}`);
30+
return null;
3131
}
3232
}
3333

packages/spec/src/formats.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export const bucketTypes = [
2222
"vtt",
2323
"php",
2424
"po",
25+
"vue-json",
2526
] as const;
2627

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

0 commit comments

Comments
 (0)