Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

machine translate multi variants #3097

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 6 additions & 11 deletions inlang/source-code/rpc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"
}
}
5 changes: 2 additions & 3 deletions inlang/source-code/rpc/src/client.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -15,4 +14,4 @@ export const route = "/_rpc"
* @example
* const [value, exception] = await rpc.generateConfigFile({ fs, path: "./" })
*/
export const rpc = rpcClient<AllRpcs>(publicEnv.PUBLIC_SERVER_BASE_URL + route)
export const rpc = rpcClient<AllRpcs>(ENV_VARIABLES.PUBLIC_SERVER_BASE_URL + route)
8 changes: 4 additions & 4 deletions inlang/source-code/rpc/src/functions/index.ts
Original file line number Diff line number Diff line change
@@ -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,
}
Expand Down
329 changes: 329 additions & 0 deletions inlang/source-code/rpc/src/functions/machineTranslateBundle.test.ts
Original file line number Diff line number Diff line change
@@ -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." }],
}),
])
)
}
)
Loading
Loading