From 72bfd74b2a8032eb82ec1ec4e7984563b54a11d8 Mon Sep 17 00:00:00 2001 From: Samuel Stroschein <35429197+samuelstroschein@users.noreply.github.com> Date: Fri, 6 Sep 2024 13:02:46 -0400 Subject: [PATCH 1/2] fix: NewVariant type --- inlang/source-code/sdk2/src/database/schema.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inlang/source-code/sdk2/src/database/schema.ts b/inlang/source-code/sdk2/src/database/schema.ts index c660d4cfe3..9174ca8895 100644 --- a/inlang/source-code/sdk2/src/database/schema.ts +++ b/inlang/source-code/sdk2/src/database/schema.ts @@ -71,7 +71,7 @@ export type NewMessage = Insertable; export type MessageUpdate = Updateable; export type Variant = Selectable; -export type NewVariant = Selectable; +export type NewVariant = Insertable; export type VariantUpdate = Updateable; export type MessageNested = Message & { From e577a17431791eeac022e581694b45fe8a0c110c Mon Sep 17 00:00:00 2001 From: Samuel Stroschein <35429197+samuelstroschein@users.noreply.github.com> Date: Fri, 6 Sep 2024 16:20:11 -0400 Subject: [PATCH 2/2] refactor: machine translate with support for variants --- inlang/source-code/rpc/package.json | 17 +- inlang/source-code/rpc/src/client.ts | 5 +- inlang/source-code/rpc/src/functions/index.ts | 8 +- .../functions/machineTranslateBundle.test.ts | 329 +++++++++++++++ ...teMessage.ts => machineTranslateBundle.ts} | 131 +++--- .../functions/machineTranslateMessage.test.ts | 373 ------------------ .../rpc/src/functions/subscribeCategory.ts | 2 +- .../rpc/src/functions/subscribeNewsletter.ts | 2 +- inlang/source-code/rpc/src/router.ts | 4 +- .../rpc/src/services/env-variables/.gitignore | 2 + .../services/env-variables/createIndexFile.js | 34 ++ .../rpc/src/services/env-variables/index.d.ts | 13 + inlang/source-code/rpc/src/types.ts | 28 ++ inlang/source-code/rpc/tsconfig.json | 4 +- pnpm-lock.yaml | 43 +- 15 files changed, 507 insertions(+), 488 deletions(-) create mode 100644 inlang/source-code/rpc/src/functions/machineTranslateBundle.test.ts rename inlang/source-code/rpc/src/functions/{machineTranslateMessage.ts => machineTranslateBundle.ts} (68%) delete mode 100644 inlang/source-code/rpc/src/functions/machineTranslateMessage.test.ts create mode 100644 inlang/source-code/rpc/src/services/env-variables/.gitignore create mode 100644 inlang/source-code/rpc/src/services/env-variables/createIndexFile.js create mode 100644 inlang/source-code/rpc/src/services/env-variables/index.d.ts create mode 100644 inlang/source-code/rpc/src/types.ts diff --git a/inlang/source-code/rpc/package.json b/inlang/source-code/rpc/package.json index cd8bca7b4e..51f0f01356 100644 --- a/inlang/source-code/rpc/package.json +++ b/inlang/source-code/rpc/package.json @@ -7,20 +7,15 @@ "./router": "./dist/router.js" }, "scripts": { - "build": "tsc --build", - "dev": "tsc --watch", - "test": "tsc --noEmit && vitest run --passWithNoTests --coverage", + "build": "npm run env-variables && tsc --build", + "dev": "npm run env-variables && tsc --watch", + "test": "npm run env-variables && tsc --noEmit && vitest run --passWithNoTests --coverage", + "env-variables": "node ./src/services/env-variables/createIndexFile.js", "lint": "eslint ./src --fix", "format": "prettier ./src --write", "clean": "rm -rf ./dist ./node_modules" }, "dependencies": { - "@inlang/env-variables": "workspace:*", - "@inlang/language-tag": "workspace:*", - "@inlang/marketplace-registry": "workspace:*", - "@inlang/message": "workspace:*", - "@inlang/result": "workspace:*", - "@inlang/sdk": "workspace:*", "@inlang/sdk2": "workspace:*", "@types/cors": "^2.8.17", "body-parser": "^1.20.2", @@ -36,8 +31,8 @@ "devDependencies": { "@types/body-parser": "1.19.2", "@types/express": "4.17.17", - "@vitest/coverage-v8": "0.34.3", + "@vitest/coverage-v8": "2.0.5", "typescript": "^5.5.2", - "vitest": "0.34.3" + "vitest": "2.0.5" } } diff --git a/inlang/source-code/rpc/src/client.ts b/inlang/source-code/rpc/src/client.ts index 2223810fe1..0091a59e7d 100644 --- a/inlang/source-code/rpc/src/client.ts +++ b/inlang/source-code/rpc/src/client.ts @@ -1,8 +1,7 @@ -import { publicEnv } from "@inlang/env-variables" import { rpcClient } from "typed-rpc" - // ! Only import the type to not leak the implementation to the client import type { AllRpcs } from "./functions/index.js" +import { ENV_VARIABLES } from "./services/env-variables/index.js" // must be identical to path in route.ts export const route = "/_rpc" @@ -15,4 +14,4 @@ export const route = "/_rpc" * @example * const [value, exception] = await rpc.generateConfigFile({ fs, path: "./" }) */ -export const rpc = rpcClient(publicEnv.PUBLIC_SERVER_BASE_URL + route) +export const rpc = rpcClient(ENV_VARIABLES.PUBLIC_SERVER_BASE_URL + route) diff --git a/inlang/source-code/rpc/src/functions/index.ts b/inlang/source-code/rpc/src/functions/index.ts index 90f92a2ea1..782277f9fe 100644 --- a/inlang/source-code/rpc/src/functions/index.ts +++ b/inlang/source-code/rpc/src/functions/index.ts @@ -1,13 +1,13 @@ import { subscribeNewsletter } from "./subscribeNewsletter.js" import { subscribeCategory } from "./subscribeCategory.js" -import { machineTranslateMessage } from "./machineTranslateMessage.js" +import { machineTranslateBundle } from "./machineTranslateBundle.js" export const allRpcs = { - machineTranslateMessage, + machineTranslateBundle, /** - * @deprecated use machineTranslateMessage instead + * @deprecated renamed to `machineTranslateBundle` */ - machineTranslate: () => undefined, + machineTranslateMessage: machineTranslateBundle, subscribeNewsletter, subscribeCategory, } diff --git a/inlang/source-code/rpc/src/functions/machineTranslateBundle.test.ts b/inlang/source-code/rpc/src/functions/machineTranslateBundle.test.ts new file mode 100644 index 0000000000..9f478b720e --- /dev/null +++ b/inlang/source-code/rpc/src/functions/machineTranslateBundle.test.ts @@ -0,0 +1,329 @@ +import { expect, test } from "vitest" +import { machineTranslateBundle } from "./machineTranslateBundle.js" +import type { BundleNested } from "@inlang/sdk2" +import { ENV_VARIABLES } from "../services/env-variables/index.js" + +test.runIf(ENV_VARIABLES.GOOGLE_TRANSLATE_API_KEY)( + "it should machine translate to all provided target locales and variants", + async () => { + const result = await machineTranslateBundle({ + sourceLocale: "en", + targetLocales: ["de", "fr", "en"], + bundle: { + id: "mock-bundle-id", + alias: {}, + messages: [ + { + id: "mock-message-id", + bundleId: "mock-bundle-id", + locale: "en", + declarations: [], + selectors: [], + variants: [ + { + id: "mock-variant-id-name-john", + messageId: "mock-message-id", + match: { + name: "John", + }, + pattern: [{ type: "text", value: "Hello world, John" }], + }, + { + id: "mock-variant-id-*", + messageId: "mock-message-id", + match: { + name: "*", + }, + pattern: [{ type: "text", value: "Hello world" }], + }, + ], + }, + ], + } as BundleNested, + }) + + const bundle = result.data + const messages = result.data?.messages + const variants = messages?.flatMap((m) => m.variants) + + expect(bundle).toBeDefined() + expect(messages).toHaveLength(3) + expect(variants).toHaveLength(6) + + const messageIds = messages?.map((m) => m.id) + const variantIds = variants?.map((v) => v.id) + + // unique ids + expect(messageIds?.length).toEqual(new Set(messageIds).size) + expect(variantIds?.length).toEqual(new Set(variantIds).size) + + // every variant id should be in the message ids + expect(variants?.every((variant) => messageIds?.includes(variant.messageId))).toBe(true) + + // every message should have the same bundle id + expect(messages?.every((message) => message.bundleId === bundle?.id)).toBe(true) + + expect(messages).toStrictEqual( + expect.arrayContaining([ + // the base message should be unmodified + expect.objectContaining({ + id: "mock-message-id", + locale: "en", + }), + // a german message should exist after translation + expect.objectContaining({ + locale: "de", + }), + // a french message should exist after translation + expect.objectContaining({ + locale: "fr", + }), + ]) + ) + + expect(variants).toStrictEqual( + expect.arrayContaining([ + // the english variant should be identical + expect.objectContaining({ + id: "mock-variant-id-name-john", + messageId: "mock-message-id", + match: { + name: "John", + }, + pattern: [{ type: "text", value: "Hello world, John" }], + }), + expect.objectContaining({ + id: "mock-variant-id-*", + messageId: "mock-message-id", + match: { + name: "*", + }, + pattern: [{ type: "text", value: "Hello world" }], + }), + // a german variant should exist + expect.objectContaining({ + match: { + name: "John", + }, + pattern: [{ type: "text", value: "Hallo Welt, John" }], + }), + expect.objectContaining({ + match: { + name: "*", + }, + pattern: [{ type: "text", value: "Hallo Welt" }], + }), + // a french variant should exist + expect.objectContaining({ + match: { + name: "John", + }, + pattern: [{ type: "text", value: "Bonjour tout le monde, John" }], + }), + expect.objectContaining({ + match: { + name: "*", + }, + pattern: [{ type: "text", value: "Bonjour le monde" }], + }), + ]) + ) + } +) + +test.runIf(ENV_VARIABLES.GOOGLE_TRANSLATE_API_KEY)( + "should escape expressions in patterns", + async () => { + const result = await machineTranslateBundle({ + sourceLocale: "en", + targetLocales: ["de"], + bundle: { + id: "mock-bundle-id", + alias: {}, + messages: [ + { + id: "mock-message-id", + bundleId: "mock-bundle-id", + locale: "en", + declarations: [], + selectors: [], + variants: [ + { + id: "mock-variant-id", + messageId: "mock-message-id", + match: {}, + pattern: [ + { type: "text", value: "There are " }, + { type: "expression", arg: { type: "variable", name: "num" } }, + { type: "text", value: " cars on the street." }, + ], + }, + ], + }, + ], + } as BundleNested, + }) + + const messages = result.data?.messages + const variants = messages?.flatMap((m) => m.variants) + + expect(messages).toStrictEqual( + expect.arrayContaining([ + // the base message should be unmodified + expect.objectContaining({ + id: "mock-message-id", + locale: "en", + }), + // a german message should exist after translation + expect.objectContaining({ + locale: "de", + }), + ]) + ) + + expect(variants).toStrictEqual( + expect.arrayContaining([ + // the english variant should be identical + expect.objectContaining({ + pattern: [ + { type: "text", value: "There are " }, + { type: "expression", arg: { type: "variable", name: "num" } }, + { type: "text", value: " cars on the street." }, + ], + }), + // a german variant should exist + expect.objectContaining({ + pattern: [ + { type: "text", value: "Es sind " }, + { type: "expression", arg: { type: "variable", name: "num" } }, + { type: "text", value: " Autos auf der Straße." }, + ], + }), + ]) + ) + } +) + +// test.todo("should not naively compare the variant lengths and instead match variants", async () => { +// const result = await machineTranslateBundle({ +// sourceLocale: "en", +// targetLocales: ["de"], +// bundle: { +// id: "mockBundle", +// alias: {}, +// messages: [ +// { +// id: "mockMessage", +// bundleId: "mockBundle", +// locale: "en", +// declarations: [], +// selectors: [ +// { +// type: "expression", +// arg: { +// type: "variable", +// name: "gender", +// }, +// }, +// ], +// variants: [ +// { +// id: "internal-dummy-id", +// messageId: "dummy-id", +// match: { gender: "male" }, +// pattern: [{ type: "text", value: "Gender male" }], +// }, +// { +// id: "internal-dummy-id", +// messageId: "dummy-id", +// match: { gender: "*" }, +// pattern: [{ type: "text", value: "Veraltete Übersetzung" }], +// }, +// ], +// }, +// ], +// } as BundleNested, +// }) +// expect(result.error).toBeUndefined() +// expect(result.data).toEqual({ +// id: "mockMessage", +// alias: {}, +// selectors: [ +// { +// type: "variable", +// name: "gender", +// }, +// ], +// variants: [ +// { +// id: "internal-dummy-id", +// messageId: "dummy-id", +// match: { gender: "male" }, +// pattern: [{ type: "text", value: "Gender male" }], +// }, +// { +// id: "internal-dummy-id", +// messageId: "dummy-id", +// match: { gender: "*" }, +// pattern: [{ type: "text", value: "Veraltete Übersetzung" }], +// }, +// { +// id: "internal-dummy-id", +// messageId: "dummy-id", +// match: { gender: "male" }, +// pattern: [{ type: "text", value: "Geschlecht männlich" }], +// }, +// ] as Variant[], +// }) +// }) + +test.runIf(ENV_VARIABLES.GOOGLE_TRANSLATE_API_KEY)( + "should keep line breaks in multiline translations", + async () => { + const result = await machineTranslateBundle({ + sourceLocale: "en", + targetLocales: ["de"], + bundle: { + id: "mockBundle", + alias: {}, + messages: [ + { + id: "mockMessage", + bundleId: "mockBundle", + locale: "en", + declarations: [], + selectors: [], + variants: [ + { + id: "internal-dummy-id", + messageId: "dummy-id", + match: {}, + pattern: [ + { + type: "text", + value: "This is a\nmultiline\ntranslation.", + }, + ], + }, + ], + }, + ], + } as BundleNested, + }) + const messages = result.data?.messages + const variants = messages?.flatMap((m) => m.variants) + + expect(variants).toStrictEqual( + expect.arrayContaining([ + // the english variant should be identical + expect.objectContaining({ + pattern: [{ type: "text", value: "This is a\nmultiline\ntranslation." }], + }), + // a german variant should exist + expect.objectContaining({ + pattern: [{ type: "text", value: "Dies ist ein\nmehrzeilig\nÜbersetzung." }], + }), + ]) + ) + } +) diff --git a/inlang/source-code/rpc/src/functions/machineTranslateMessage.ts b/inlang/source-code/rpc/src/functions/machineTranslateBundle.ts similarity index 68% rename from inlang/source-code/rpc/src/functions/machineTranslateMessage.ts rename to inlang/source-code/rpc/src/functions/machineTranslateBundle.ts index 7093162d88..20b9ad0517 100644 --- a/inlang/source-code/rpc/src/functions/machineTranslateMessage.ts +++ b/inlang/source-code/rpc/src/functions/machineTranslateBundle.ts @@ -1,99 +1,92 @@ -import { privateEnv } from "@inlang/env-variables" -import type { Result } from "@inlang/result" import { createVariant, Text, VariableReference, type Variant, type BundleNested, + type NewBundleNested, + uuidv4, } from "@inlang/sdk2" +import type { Result } from "../types.js" +import { ENV_VARIABLES } from "../services/env-variables/index.js" -export async function machineTranslateMessage(args: { +export async function machineTranslateBundle(args: { bundle: BundleNested - baseLocale: string + sourceLocale: string targetLocales: string[] -}): Promise> { +}): Promise> { try { - if (!privateEnv.GOOGLE_TRANSLATE_API_KEY) { - throw new Error("GOOGLE_TRANSLATE_API_KEY is not set") + if (!ENV_VARIABLES.GOOGLE_TRANSLATE_API_KEY) { + return { error: "GOOGLE_TRANSLATE_API_KEY is not set" } } const copy = structuredClone(args.bundle) - // Iterate over each message in the bundle - for (const message of args.bundle.messages) { - // Skip if the message locale does not match the base locale - if (message.locale !== args.baseLocale) { - continue - } - - // Proceed only if the message has exactly one variant - if (message.variants.length !== 1) { - continue - } - - const baseVariant = message.variants[0] + const sourceMessage = copy.messages.find((m) => m.locale === args.sourceLocale) - // Skip if the variant has no pattern - if (!baseVariant) { - continue - } + if (!sourceMessage) { + return { error: "Source locale not found in the bundle" } + } - const q = serializePattern(baseVariant.pattern, {}) + for (const sourceVariant of sourceMessage.variants) { + const sourcePattern = serializePattern(sourceVariant.pattern, {}) - // Translate the variant for each target locale for (const targetLocale of args.targetLocales) { - let translation: string - - if (!process.env.MOCK_TRANSLATE) { - const response = await fetch( - "https://translation.googleapis.com/language/translate/v2?" + - new URLSearchParams({ - q, - target: targetLocale, - source: args.baseLocale, - format: "html", - key: privateEnv.GOOGLE_TRANSLATE_API_KEY, - }), - { method: "POST" } - ) + // if by mistake the source locale is in the target locales, skip it + if (targetLocale === args.sourceLocale) { + continue + } - if (!response.ok) { - const err = `${response.status} ${response.statusText}: translating from ${args.baseLocale} to ${targetLocale}` - return { error: err } - } + const response = await fetch( + "https://translation.googleapis.com/language/translate/v2?" + + new URLSearchParams({ + q: sourcePattern, + target: targetLocale, + source: args.sourceLocale, + format: "html", + key: ENV_VARIABLES.GOOGLE_TRANSLATE_API_KEY, + }), + { method: "POST" } + ) - const json = await response.json() - translation = json.data.translations[0].translatedText - } else { - const mockTranslation = await mockTranslateApi(q, args.baseLocale, targetLocale) - if (mockTranslation.error) return { error: mockTranslation.error } - translation = mockTranslation.translation + if (!response.ok) { + const err = `${response.status} ${response.statusText}: translating from ${args.sourceLocale} to ${targetLocale}` + return { error: err } } - // Deserialize the translated pattern - const translatedPattern = deserializePattern(translation) + const json = await response.json() + const pattern = deserializePattern(json.data.translations[0].translatedText) + const targetMessage = copy.messages.find((m) => m.locale === targetLocale) - // Create a new message with the translated locale and the translated variant - const translatedMessage = { - ...message, - locale: targetLocale, - variants: [ + if (targetMessage) { + targetMessage.variants.push( createVariant({ - pattern: translatedPattern, - messageId: message.id, - }), - ], + id: uuidv4(), + messageId: targetMessage.id, + match: sourceVariant.match, + pattern, + }) as Variant + ) + } else { + const newMessageId = uuidv4() + copy.messages.push({ + ...sourceMessage, + id: newMessageId, + locale: targetLocale, + variants: [ + createVariant({ + id: uuidv4(), + messageId: newMessageId, + match: sourceVariant.match, + pattern, + }) as Variant, + ], + }) } - - // Add the translated message to the bundle - copy.messages.push(translatedMessage) } } - return { data: copy } } catch (error) { - console.error(error) return { error: error?.toString() ?? "unknown error" } } } @@ -217,8 +210,8 @@ function deserializePattern(text: string): Variant["pattern"] { break } - const placeholderAsText = unescapedText.slice(start + escapeStart.length, end) - const placeholder = JSON.parse(placeholderAsText) as VariableReference + const expressionAsText = unescapedText.slice(start + escapeStart.length, end) + const expression = JSON.parse(expressionAsText) as VariableReference // can't get it running, ignoring for now // const lastElement = result[result.length] @@ -235,7 +228,7 @@ function deserializePattern(text: string): Variant["pattern"] { // TODO: handle placeholder with correct type // @ts-expect-error - result.push(placeholder) + result.push(expression) i = end + escapeEnd.length } return result diff --git a/inlang/source-code/rpc/src/functions/machineTranslateMessage.test.ts b/inlang/source-code/rpc/src/functions/machineTranslateMessage.test.ts deleted file mode 100644 index 9ca3a483db..0000000000 --- a/inlang/source-code/rpc/src/functions/machineTranslateMessage.test.ts +++ /dev/null @@ -1,373 +0,0 @@ -import { it, expect } from "vitest" -import { privateEnv } from "@inlang/env-variables" -import { machineTranslateMessage } from "./machineTranslateMessage.js" -import type { Variant, BundleNested } from "@inlang/sdk2" - -it.runIf(privateEnv.GOOGLE_TRANSLATE_API_KEY)( - "should translate multiple target locales", - async () => { - const result = await machineTranslateMessage({ - baseLocale: "en", - targetLocales: ["de", "fr"], - bundle: { - id: "mockBundle", - alias: {}, - messages: [ - { - id: "mockMessage", - bundleId: "mockBundle", - locale: "en", - declarations: [], - selectors: [], - variants: [ - { - id: "internal-dummy-id", - messageId: "dummy-id", - match: {}, - pattern: [{ type: "text", value: "Hello world" }], - }, - ], - }, - ], - } as BundleNested, - }) - expect(result.error).toBeUndefined() - expect(result.data).toEqual({ - id: "mockMessage", - alias: {}, - selectors: [], - variants: [ - { - id: "internal-dummy-id", - messageId: "dummy-id", - match: {}, - pattern: [{ type: "text", value: "Hello world" }], - }, - { - id: "internal-dummy-id", - messageId: "dummy-id", - match: {}, - pattern: [{ type: "text", value: "Hallo Welt" }], - }, - { - id: "internal-dummy-id", - messageId: "dummy-id", - match: {}, - pattern: [{ type: "text", value: "Bonjour le monde" }], - }, - ] as Variant[], - }) - } -) - -it.runIf(privateEnv.GOOGLE_TRANSLATE_API_KEY)( - "should escape pattern elements that are not Text", - async () => { - const result = await machineTranslateMessage({ - baseLocale: "en", - targetLocales: ["de"], - bundle: { - id: "mockBundle", - alias: {}, - messages: [ - { - id: "mockMessage", - bundleId: "mockBundle", - locale: "en", - declarations: [], - selectors: [], - variants: [ - { - id: "internal-dummy-id", - messageId: "dummy-id", - match: {}, - pattern: [ - { type: "text", value: "Good evening" }, - { - type: "expression", - arg: { - type: "variable", - name: "username", - }, - annotation: { - type: "function", - name: "username", - options: [ - { - name: "username", - value: { - type: "variable", - name: "username", - }, - }, - ], - }, - }, - { type: "text", value: ", what a beautiful sunset." }, - ], - }, - ], - }, - ], - } as BundleNested, - }) - expect(result.error).toBeUndefined() - expect(result.data).toEqual({ - id: "mockMessage", - alias: {}, - selectors: [], - variants: [ - { - id: "internal-dummy-id", - messageId: "dummy-id", - match: {}, - pattern: [ - { type: "text", value: "Good evening" }, - { - type: "expression", - arg: { - type: "variable", - name: "username", - }, - annotation: { - type: "function", - name: "username", - options: [ - { - name: "username", - value: { - type: "variable", - name: "username", - }, - }, - ], - }, - }, - { type: "text", value: ", what a beautiful sunset." }, - ], - }, - { - id: "internal-dummy-id", - messageId: "dummy-id", - match: {}, - pattern: [ - { type: "text", value: "Guten Abend" }, - { - type: "expression", - arg: { - type: "variable", - name: "username", - }, - annotation: { - type: "function", - name: "username", - options: [ - { - name: "username", - value: { - type: "variable", - name: "username", - }, - }, - ], - }, - }, - { type: "text", value: ", was für ein schöner Sonnenuntergang." }, - ], - }, - ] as Variant[], - }) - } -) - -it.todo("should not naively compare the variant lengths and instead match variants", async () => { - const result = await machineTranslateMessage({ - baseLocale: "en", - targetLocales: ["de"], - bundle: { - id: "mockBundle", - alias: {}, - messages: [ - { - id: "mockMessage", - bundleId: "mockBundle", - locale: "en", - declarations: [], - selectors: [ - { - type: "expression", - arg: { - type: "variable", - name: "gender", - }, - }, - ], - variants: [ - { - id: "internal-dummy-id", - messageId: "dummy-id", - match: { gender: "male" }, - pattern: [{ type: "text", value: "Gender male" }], - }, - { - id: "internal-dummy-id", - messageId: "dummy-id", - match: { gender: "*" }, - pattern: [{ type: "text", value: "Veraltete Übersetzung" }], - }, - ], - }, - ], - } as BundleNested, - }) - expect(result.error).toBeUndefined() - expect(result.data).toEqual({ - id: "mockMessage", - alias: {}, - selectors: [ - { - type: "variable", - name: "gender", - }, - ], - variants: [ - { - id: "internal-dummy-id", - messageId: "dummy-id", - match: { gender: "male" }, - pattern: [{ type: "text", value: "Gender male" }], - }, - { - id: "internal-dummy-id", - messageId: "dummy-id", - match: { gender: "*" }, - pattern: [{ type: "text", value: "Veraltete Übersetzung" }], - }, - { - id: "internal-dummy-id", - messageId: "dummy-id", - match: { gender: "male" }, - pattern: [{ type: "text", value: "Geschlecht männlich" }], - }, - ] as Variant[], - }) -}) - -it.runIf(privateEnv.GOOGLE_TRANSLATE_API_KEY)( - "should not return escaped quotation marks", - async () => { - const result = await machineTranslateMessage({ - baseLocale: "en", - targetLocales: ["de"], - bundle: { - id: "mockBundle", - alias: {}, - messages: [ - { - id: "mockMessage", - bundleId: "mockBundle", - locale: "en", - declarations: [], - selectors: [], - variants: [ - { - id: "internal-dummy-id", - messageId: "dummy-id", - match: {}, - pattern: [ - { type: "text", value: "'" }, - { type: "variable", name: "id" }, - { type: "text", value: "' added a new todo" }, - ], - }, - ], - }, - ], - } as BundleNested, - }) - expect(result.error).toBeUndefined() - expect(result.data).toEqual({ - id: "mockMessage", - alias: {}, - selectors: [], - variants: [ - { - id: "internal-dummy-id", - messageId: "dummy-id", - match: {}, - pattern: [ - { type: "text", value: "'" }, - { type: "variable", name: "id" }, - { type: "text", value: "' added a new todo" }, - ], - }, - { - id: "internal-dummy-id", - messageId: "dummy-id", - match: {}, - pattern: [ - { type: "text", value: "' " }, - { type: "variable", name: "id" }, - { type: "text", value: " ' hat ein neues To-Do hinzugefügt" }, - ], - }, - ] as Variant[], - }) - } -) - -it.runIf(privateEnv.GOOGLE_TRANSLATE_API_KEY)( - "should not keep line breaks in multiline translations", - async () => { - const result = await machineTranslateMessage({ - baseLocale: "en", - targetLocales: ["de"], - bundle: { - id: "mockBundle", - alias: {}, - messages: [ - { - id: "mockMessage", - bundleId: "mockBundle", - locale: "en", - declarations: [], - selectors: [], - variants: [ - { - id: "internal-dummy-id", - messageId: "dummy-id", - match: {}, - pattern: [ - { - type: "text", - value: "This is a\nmultiline\ntranslation.", - }, - ], - }, - ], - }, - ], - } as BundleNested, - }) - expect(result.error).toBeUndefined() - expect(result.data).toEqual({ - id: "mockMessage", - alias: {}, - selectors: [], - variants: [ - { - id: "internal-dummy-id", - messageId: "dummy-id", - match: {}, - pattern: [{ type: "text", value: "This is a multiline translation." }], - }, - { - id: "internal-dummy-id", - messageId: "dummy-id", - match: {}, - pattern: [{ type: "text", value: "Dies ist eine mehrzeilige Übersetzung." }], - }, - ] as Variant[], - }) - } -) diff --git a/inlang/source-code/rpc/src/functions/subscribeCategory.ts b/inlang/source-code/rpc/src/functions/subscribeCategory.ts index 005e29f933..ef2941c5c4 100644 --- a/inlang/source-code/rpc/src/functions/subscribeCategory.ts +++ b/inlang/source-code/rpc/src/functions/subscribeCategory.ts @@ -1,4 +1,4 @@ -import type { Result } from "@inlang/result" +import type { Result } from "../types.js" export async function subscribeCategory(args: { category: string diff --git a/inlang/source-code/rpc/src/functions/subscribeNewsletter.ts b/inlang/source-code/rpc/src/functions/subscribeNewsletter.ts index 2b1f152e6a..c2b3c4bcd5 100644 --- a/inlang/source-code/rpc/src/functions/subscribeNewsletter.ts +++ b/inlang/source-code/rpc/src/functions/subscribeNewsletter.ts @@ -1,4 +1,4 @@ -import type { Result } from "@inlang/result" +import type { Result } from "../types.js" export async function subscribeNewsletter(args: { email: string }): Promise> { try { diff --git a/inlang/source-code/rpc/src/router.ts b/inlang/source-code/rpc/src/router.ts index 8faad6d969..6c20ebaae3 100644 --- a/inlang/source-code/rpc/src/router.ts +++ b/inlang/source-code/rpc/src/router.ts @@ -3,12 +3,12 @@ import bodyParser from "body-parser" import { rpcHandler } from "typed-rpc/lib/express.js" import { allRpcs } from "./functions/index.js" import { route } from "./client.js" -import { privateEnv } from "@inlang/env-variables" import cors from "cors" +import { ENV_VARIABLES } from "./services/env-variables/index.js" export const router: Router = express.Router() -const allowedOrigins = privateEnv.PUBLIC_ALLOWED_AUTH_URLS.split(",") +const allowedOrigins = ENV_VARIABLES.PUBLIC_ALLOWED_AUTH_URLS?.split(",") // Enable CORS for all allowed origins router.use( diff --git a/inlang/source-code/rpc/src/services/env-variables/.gitignore b/inlang/source-code/rpc/src/services/env-variables/.gitignore new file mode 100644 index 0000000000..c0d68902e4 --- /dev/null +++ b/inlang/source-code/rpc/src/services/env-variables/.gitignore @@ -0,0 +1,2 @@ +index.ts +index.js \ No newline at end of file diff --git a/inlang/source-code/rpc/src/services/env-variables/createIndexFile.js b/inlang/source-code/rpc/src/services/env-variables/createIndexFile.js new file mode 100644 index 0000000000..7cca41ee9f --- /dev/null +++ b/inlang/source-code/rpc/src/services/env-variables/createIndexFile.js @@ -0,0 +1,34 @@ +/* eslint-disable no-restricted-imports */ +/* eslint-disable no-undef */ +/** + * This script writes public environment variables + * to an importable env file. + * + * - The SDK must bundle this file with the rest of the SDK + * - This scripts avoids the need for a bundler + * - Must be ran before building the SDK + */ + +import fs from "node:fs/promises" +import url from "node:url" +import path from "node:path" + +const dirname = path.dirname(url.fileURLToPath(import.meta.url)) + +await fs.writeFile( + dirname + "/index.js", + ` +export const ENV_VARIABLES = { + GOOGLE_TRANSLATE_API_KEY: ${ifDefined(process.env.GOOGLE_TRANSLATE_API_KEY)}, + PUBLIC_SERVER_BASE_URL: ${ifDefined(process.env.PUBLIC_SERVER_BASE_URL)}, + PUBLIC_ALLOWED_AUTH_URLS: ${ifDefined(process.env.PUBLIC_ALLOWED_AUTH_URLS)}, +} +` +) + +// eslint-disable-next-line no-console +// console.log("✅ Created env variable index file."); + +function ifDefined(value) { + return value ? `"${value}"` : undefined +} diff --git a/inlang/source-code/rpc/src/services/env-variables/index.d.ts b/inlang/source-code/rpc/src/services/env-variables/index.d.ts new file mode 100644 index 0000000000..d9c54028af --- /dev/null +++ b/inlang/source-code/rpc/src/services/env-variables/index.d.ts @@ -0,0 +1,13 @@ +/** + * Avoiding TypeScript errors before the `createIndexFile` script + * is invoked by defining the type ahead of time. + */ + +/** + * Env variables that are available at runtime. + */ +export declare const ENV_VARIABLES: { + GOOGLE_TRANSLATE_API_KEY?: string + PUBLIC_SERVER_BASE_URL?: string + PUBLIC_ALLOWED_AUTH_URLS?: string +} diff --git a/inlang/source-code/rpc/src/types.ts b/inlang/source-code/rpc/src/types.ts new file mode 100644 index 0000000000..a1350aa9ef --- /dev/null +++ b/inlang/source-code/rpc/src/types.ts @@ -0,0 +1,28 @@ +export type SuccessResult = { data: Data; error?: never } + +export type ErrorResult = { data?: never; error: Error } + +/** + * + * @deprecated Just use throwing. It's the standard API. + * + * A result represents an either success or an error, but never both. + * + * Do not use Result to represent both success and error at the same time + * to not confuse the consumer of the result. + * + * @example + * function doSomething(): Result { + * return { + * error: "Something went wrong" + * } + * } + * + * const result = doSomething() + * if (result.error) { + * console.log(result.error) + * } + * + */ +// MOVED OUT OF @inlang/result because @inlang/result is going to be removed. +export type Result = SuccessResult | ErrorResult diff --git a/inlang/source-code/rpc/tsconfig.json b/inlang/source-code/rpc/tsconfig.json index b27511e7bb..167da997c6 100644 --- a/inlang/source-code/rpc/tsconfig.json +++ b/inlang/source-code/rpc/tsconfig.json @@ -1,9 +1,11 @@ { "extends": "../tsconfig.base.json", - "include": ["src/**/*.ts"], + "include": ["src/**/*.ts", "src/services/env-variables/index.js"], "compilerOptions": { "lib": ["ESNext", "DOM"], "types": [], + // required to copy js files to dist + "allowJs": true, "outDir": "./dist", "rootDir": "./src" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b8b3384e48..8b9cfb0d82 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2483,24 +2483,6 @@ importers: inlang/source-code/rpc: dependencies: - '@inlang/env-variables': - specifier: workspace:* - version: link:../env-variables - '@inlang/language-tag': - specifier: workspace:* - version: link:../versioned-interfaces/language-tag - '@inlang/marketplace-registry': - specifier: workspace:* - version: link:../marketplace-registry - '@inlang/message': - specifier: workspace:* - version: link:../versioned-interfaces/message - '@inlang/result': - specifier: workspace:* - version: link:../result - '@inlang/sdk': - specifier: workspace:* - version: link:../sdk '@inlang/sdk2': specifier: workspace:* version: link:../sdk2 @@ -2530,14 +2512,14 @@ importers: specifier: 4.17.17 version: 4.17.17 '@vitest/coverage-v8': - specifier: 0.34.3 - version: 0.34.3(vitest@0.34.3(@vitest/browser@1.6.0)(jsdom@22.1.0)(lightningcss@1.26.0)(playwright@1.39.0)(safaridriver@0.1.2)(terser@5.31.6)(webdriverio@8.40.3)) + specifier: 2.0.5 + version: 2.0.5(vitest@2.0.5(@types/node@22.5.2)(jsdom@22.1.0)(lightningcss@1.26.0)(terser@5.31.6)) typescript: specifier: ^5.5.2 version: 5.5.4 vitest: - specifier: 0.34.3 - version: 0.34.3(@vitest/browser@1.6.0)(jsdom@22.1.0)(lightningcss@1.26.0)(playwright@1.39.0)(safaridriver@0.1.2)(terser@5.31.6)(webdriverio@8.40.3) + specifier: 2.0.5 + version: 2.0.5(@types/node@22.5.2)(jsdom@22.1.0)(lightningcss@1.26.0)(terser@5.31.6) inlang/source-code/sdk: dependencies: @@ -20786,6 +20768,21 @@ snapshots: - typescript - verdaccio + '@nrwl/js@18.3.5(@babel/traverse@7.25.3)(@types/node@20.14.14)(nx@18.3.5)(typescript@5.5.4)': + dependencies: + '@nx/js': 18.3.5(@babel/traverse@7.25.3)(@types/node@20.14.14)(nx@18.3.5)(typescript@5.5.4) + transitivePeerDependencies: + - '@babel/traverse' + - '@swc-node/register' + - '@swc/core' + - '@swc/wasm' + - '@types/node' + - debug + - nx + - supports-color + - typescript + - verdaccio + '@nrwl/js@18.3.5(@babel/traverse@7.25.3)(@types/node@22.5.2)(nx@18.3.5)(typescript@5.4.5)': dependencies: '@nx/js': 18.3.5(@babel/traverse@7.25.3)(@types/node@22.5.2)(nx@18.3.5)(typescript@5.4.5) @@ -21074,7 +21071,7 @@ snapshots: '@babel/preset-env': 7.25.3(@babel/core@7.25.2) '@babel/preset-typescript': 7.24.7(@babel/core@7.25.2) '@babel/runtime': 7.25.0 - '@nrwl/js': 18.3.5(@babel/traverse@7.25.3)(@types/node@20.14.14)(nx@18.3.5)(typescript@5.4.5) + '@nrwl/js': 18.3.5(@babel/traverse@7.25.3)(@types/node@20.14.14)(nx@18.3.5)(typescript@5.5.4) '@nx/devkit': 18.3.5(nx@18.3.5) '@nx/workspace': 18.3.5 '@phenomnomnominal/tsquery': 5.0.1(typescript@5.5.4)