import { test, expect } from "vitest"; import type { BundleImport, InlangPlugin, MessageImport, VariantImport, } from "../plugin/schema.js"; import type { Message } from "../database/schema.js"; import type { Text } from "../json-schema/pattern.js"; import { loadProjectInMemory } from "../project/loadProjectInMemory.js"; import { newProject } from "../project/newProject.js"; import { importFiles } from "./importFiles.js"; import { exportFiles } from "./exportFiles.js"; test("the file should be identical after a roundtrip if no modifications occured", async () => { const project = await loadProjectInMemory({ blob: await newProject(), }); const enResource = JSON.stringify({ hello_world: "value1", }); await importFiles({ files: [{ content: new TextEncoder().encode(enResource), locale: "en" }], pluginKey: "mock", plugins: [mockPluginSimple], settings: await project.settings.get(), db: project.db, }); const importedBundles = await project.db .selectFrom("bundle") .selectAll() .execute(); const importedMessages = await project.db .selectFrom("message") .selectAll() .execute(); const importedVariants = await project.db .selectFrom("variant") .selectAll() .execute(); expect(importedBundles.length).toBe(1); expect(importedMessages.length).toBe(1); expect(importedVariants.length).toBe(1); expect(importedBundles[0]?.id).toBe("hello_world"); expect(importedMessages[0]?.bundleId).toBe("hello_world"); expect(importedVariants[0]?.messageId).toBe(importedMessages[0]?.id); const exportedFiles = await exportFiles({ pluginKey: "mock", plugins: [mockPluginSimple], settings: await project.settings.get(), db: project.db, }); expect(exportedFiles.length).toBe(1); expect(exportedFiles[0]?.name).toBe("en.json"); expect(new TextDecoder().decode(exportedFiles[0]?.content)).toBe(enResource); }); test("a variant with an existing match should update the existing variant and not create a new one to enable roundtrips", async () => { const project = await loadProjectInMemory({ blob: await newProject(), }); const existing = { bundles: [ { id: "mock-bundle-id", declarations: [], }, ], messages: [ { id: "mock-message-id", locale: "en", selectors: [], bundleId: "mock-bundle-id", }, ], variants: [ { id: "mock-variant-id", messageId: "mock-message-id", matches: [ { type: "literal-match", key: "color", value: "blue", }, ], pattern: [ { type: "text", value: "You have blue eyes.", }, ], }, ], }; const updated = structuredClone(existing); // @ts-expect-error - we know this is a text pattern updated.variants[0].pattern[0].value = "You have beautiful blue eyes."; const enResource = JSON.stringify(updated); await importFiles({ files: [{ content: new TextEncoder().encode(enResource), locale: "en" }], pluginKey: "mock", plugins: [mockPluginAst], settings: await project.settings.get(), db: project.db, }); const bundles = await project.db.selectFrom("bundle").selectAll().execute(); const messages = await project.db.selectFrom("message").selectAll().execute(); const variants = await project.db.selectFrom("variant").selectAll().execute(); expect(bundles.length).toBe(1); expect(messages.length).toBe(1); expect(variants.length).toBe(1); // @ts-expect-error - we know this is a text pattern expect(variants[0]?.pattern[0].value).toBe("You have beautiful blue eyes."); }); test("if a message for the bundle id and locale already exists, update it. don't create a new message", async () => { const project = await loadProjectInMemory({ blob: await newProject(), }); const existing: any = { bundles: [{ id: "mock-bundle-id", declarations: [] }], messages: [ { id: "mock-message-id", locale: "en", selectors: [], bundleId: "mock-bundle-id", }, ], variants: [], }; const updated = structuredClone(existing); updated.messages[0]!.selectors = [ { type: "variable-reference", name: "variable2", }, ] satisfies Message["selectors"]; const enResource = new TextEncoder().encode(JSON.stringify(updated)); await importFiles({ files: [{ content: enResource, locale: "en" }], pluginKey: "mock", plugins: [mockPluginAst], settings: await project.settings.get(), db: project.db, }); const bundles = await project.db.selectFrom("bundle").selectAll().execute(); const messages = await project.db.selectFrom("message").selectAll().execute(); expect(bundles.length).toBe(1); expect(messages.length).toBe(1); expect(messages[0]?.selectors).toStrictEqual([ { type: "variable-reference", name: "variable2", }, ]); }); test("keys should be ordered alphabetically for .json to minimize git diffs", async () => { const project = await loadProjectInMemory({ blob: await newProject(), }); const enResource = JSON.stringify({ a: "value1", b: "value2", d: "value4", }); await importFiles({ files: [{ content: new TextEncoder().encode(enResource), locale: "en" }], pluginKey: "mock", plugins: [mockPluginSimple], settings: await project.settings.get(), db: project.db, }); await project.db.insertInto("bundle").values({ id: "c" }).execute(); await project.db .insertInto("message") .values({ id: "c-en", bundleId: "c", locale: "en" }) .execute(); await project.db .insertInto("variant") .values({ messageId: "c-en", pattern: [{ type: "text", value: "value3" }] }) .execute(); const exportedFiles = await exportFiles({ pluginKey: "mock", plugins: [mockPluginSimple], settings: await project.settings.get(), db: project.db, }); expect( JSON.parse(new TextDecoder().decode(exportedFiles[0]?.content)) ).toStrictEqual({ a: "value1", b: "value2", c: "value3", d: "value4", }); }); const mockPluginAst: InlangPlugin = { key: "mock", exportFiles: async ({ bundles, messages, variants }) => { return [ { locale: "every", name: "x.json", content: new TextEncoder().encode( JSON.stringify({ bundles, messages, variants }) ), }, ]; }, importFiles: async ({ files }) => { let bundles: any[] = []; let messages: any[] = []; let variants: any[] = []; for (const file of files) { const parsed = JSON.parse(new TextDecoder().decode(file.content)); bundles = [...bundles, ...parsed.bundles]; messages = [...messages, ...parsed.messages]; variants = [...variants, ...parsed.variants]; } return { bundles, messages, variants }; }, }; // a simple mock plugin to test roundtrips // // purposefully not imported a real plugin // to keep the interdepenedencies low and // increase maintainability const mockPluginSimple: InlangPlugin = { key: "mock", exportFiles: async ({ messages, variants }) => { const jsons: any = {}; for (const message of messages) { const key = message.bundleId; const value = ( variants.find((v) => v.messageId === message.id)?.pattern[0] as Text ).value; if (!jsons[message.locale]) { jsons[message.locale] = {}; } jsons[message.locale][key] = value; } return Object.entries(jsons).map(([locale, json]) => ({ locale, name: locale + ".json", content: new TextEncoder().encode(JSON.stringify(json)), })); }, importFiles: async ({ files }) => { const bundles: BundleImport[] = []; const messages: MessageImport[] = []; const variants: VariantImport[] = []; for (const file of files) { const parsed = JSON.parse(new TextDecoder().decode(file.content)); for (const key in parsed) { bundles.push({ id: key, declarations: [], }); messages.push({ bundleId: key, locale: file.locale, selectors: [], }); variants.push({ messageBundleId: key, messageLocale: file.locale, matches: [], pattern: [ { type: "text", value: parsed[key], }, ], }); } } return { bundles, messages, variants, }; }, };