-
Notifications
You must be signed in to change notification settings - Fork 3
Add content translation dictionary commands #48
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| import { z } from "zod"; | ||
|
|
||
| import { pipeToStdout } from "../../output/stream"; | ||
| import { connectionFlags, profileFlag } from "../flags"; | ||
| import { defineMetabaseCommand } from "../runtime"; | ||
|
|
||
| const ContentTranslationCsv = z.string().describe("The raw content translation dictionary CSV."); | ||
|
|
||
| export default defineMetabaseCommand({ | ||
| meta: { | ||
| name: "download", | ||
| description: "Stream the complete content translation dictionary as CSV", | ||
| }, | ||
| details: | ||
| "Streams the active dictionary to stdout with the server's CSV formatting. Redirect stdout to preserve it as a file that can be reviewed, versioned, or uploaded later.", | ||
| capabilities: { minVersion: 58, tokenFeature: "content_translation" }, | ||
| args: { | ||
| ...profileFlag, | ||
| ...connectionFlags, | ||
| }, | ||
| outputSchema: ContentTranslationCsv, | ||
| examples: [ | ||
| "mb content-translation download > metabase-content-translations.csv", | ||
| "mb content-translation download --profile prod > translations.csv", | ||
| ], | ||
| async run({ getClient }) { | ||
| const mb = await getClient(); | ||
| const stream = await mb.contentTranslation.download(); | ||
| await pipeToStdout(stream); | ||
| }, | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| import { defineCommandGroup } from "../group"; | ||
|
|
||
| export default defineCommandGroup({ | ||
| name: "content-translation", | ||
| description: "Download or replace the content translation dictionary", | ||
| subCommands: { | ||
| download: () => import("./download").then((mod) => mod.default), | ||
| upload: () => import("./upload").then((mod) => mod.default), | ||
| }, | ||
| }); |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,39 @@ | ||||||
| import { ContentTranslationUploadResult } from "@metabase/client/domain/content-translation"; | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win Use the public client export at both CLI sites. Both files import a client domain schema through a private path.
As per coding guidelines, CLI code may type-import only published resource subpaths. Proposed fix-import { ContentTranslationUploadResult } from "`@metabase/client/domain/content-translation`";
+import { ContentTranslationUploadResult } from "`@metabase/client`";📝 Committable suggestion
Suggested change
📍 Affects 2 files
🤖 Prompt for AI AgentsSource: Coding guidelines |
||||||
|
|
||||||
| import { renderSummary } from "../../output/render"; | ||||||
| import { contentTranslationUploadView } from "../../output/views/content-translation"; | ||||||
| import { readCsvFile, requireUploadFilePath } from "../../runtime/upload"; | ||||||
| import { connectionFlags, outputFlags, profileFlag } from "../flags"; | ||||||
| import { defineMetabaseCommand } from "../runtime"; | ||||||
|
|
||||||
| export default defineMetabaseCommand({ | ||||||
| meta: { | ||||||
| name: "upload", | ||||||
| description: "Replace the complete content translation dictionary from CSV", | ||||||
| }, | ||||||
| details: | ||||||
| "Uploads one complete dictionary and replaces every active content translation on the server. Keep the canonical full CSV in version control and download the current dictionary before replacing it. Metabase accepts dictionaries up to 1.5 MiB.", | ||||||
| capabilities: { minVersion: 58, tokenFeature: "content_translation" }, | ||||||
| args: { | ||||||
| ...outputFlags, | ||||||
| ...profileFlag, | ||||||
| ...connectionFlags, | ||||||
| file: { type: "string", description: "Path to the complete translation dictionary CSV" }, | ||||||
| }, | ||||||
| outputSchema: ContentTranslationUploadResult, | ||||||
| examples: [ | ||||||
| "mb content-translation upload --file translations.csv", | ||||||
| "mb content-translation upload --file translations.csv --profile prod --json", | ||||||
| ], | ||||||
| async run({ args, ctx, getClient }) { | ||||||
| const file = await readCsvFile(requireUploadFilePath(args.file)); | ||||||
| const mb = await getClient(); | ||||||
| const result = await mb.contentTranslation.upload(file); | ||||||
| renderSummary( | ||||||
| result, | ||||||
| contentTranslationUploadView, | ||||||
| `Replaced the content translation dictionary with "${file.filename}".`, | ||||||
| ctx, | ||||||
| ); | ||||||
| }, | ||||||
| }); | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| import { ContentTranslationUploadResult } from "@metabase/client/domain/content-translation"; | ||
|
|
||
| import type { ResourceView } from "../view"; | ||
|
|
||
| export const contentTranslationUploadView: ResourceView<ContentTranslationUploadResult> = { | ||
| compactPick: ContentTranslationUploadResult, | ||
| tableColumns: [{ key: "success", label: "Success" }], | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| import { z } from "zod"; | ||
|
|
||
| export const ContentTranslationUploadResult = z.object({ | ||
| success: z.literal(true), | ||
| }); | ||
| export type ContentTranslationUploadResult = z.infer<typeof ContentTranslationUploadResult>; | ||
|
Comment on lines
+3
to
+6
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
rg -n --glob '*.ts' \
'ContentTranslationUploadResultCompact|z\.looseObject|ContentTranslationUploadResult\.pick' \
packages/client/src/domain packages/cli/srcRepository: metabase/metabase-cli Length of output: 242 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo '--- target file ---'
cat -n packages/client/src/domain/content-translation.ts
echo '--- target symbol usages ---'
rg -n --glob '*.{ts,tsx}' \
'ContentTranslationUploadResult|content-translation|ContentTranslation' \
packages/client/src packages/cli/src
echo '--- domain schema declarations ---'
rg -n --glob '*.ts' \
'export const .*Compact|z\.looseObject|\.pick\(.*\)\.strip\(\)' \
packages/client/src/domainRepository: metabase/metabase-cli Length of output: 8069 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo '--- content translation view ---'
cat -n packages/cli/src/output/views/content-translation.ts
echo '--- content translation resource ---'
cat -n packages/client/src/resources/content-translation.ts
echo '--- upload command ---'
cat -n packages/cli/src/commands/content-translation/upload.ts
echo '--- ResourceView definitions and compactPick usages ---'
rg -n --glob '*.{ts,tsx}' \
'type ResourceView|interface ResourceView|compactPick:|compactPick\b' \
packages/cli/src packages/client/src | head -160
echo '--- comparable result schemas and views ---'
cat -n packages/client/src/domain/eid-translation.ts
rg -n --glob '*.{ts,tsx}' 'EidTranslateResultCompact|EidTranslateResult|TranslateResultCompact' \
packages/cli/src packages/client/srcRepository: metabase/metabase-cli Length of output: 13826 Add the compact upload-result schema. Define 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| import { assert, describe, expect, it } from "vitest"; | ||
|
|
||
| import { createClient } from "../client"; | ||
| import { ResponseShapeError } from "../errors"; | ||
| import type { ClientCredentials } from "../http/transport"; | ||
| import { captureFetch, jsonResponse, TEST_USER_AGENT } from "../testing/fetch-capture"; | ||
|
|
||
| import type { CsvFile } from "./csv-upload"; | ||
|
|
||
| const CREDENTIALS: ClientCredentials = { | ||
| url: "https://mb.example.com/metabase", | ||
| credential: { kind: "apiKey", apiKey: "mb_wire_test_key" }, | ||
| }; | ||
|
|
||
| const CSV_TEXT = "Language,String,Translation\nsv,Title,Rubrik\nar,Cat,قطة\n"; | ||
|
|
||
| const CSV_FILE: CsvFile = { | ||
| filename: "translations.csv", | ||
| bytes: new TextEncoder().encode(CSV_TEXT), | ||
| }; | ||
|
|
||
| const BINARY_READ_HEADERS = { | ||
| accept: "*/*", | ||
| "user-agent": TEST_USER_AGENT, | ||
| "x-api-key": "mb_wire_test_key", | ||
| }; | ||
|
|
||
| const JSON_READ_HEADERS = { | ||
| accept: "application/json", | ||
| "user-agent": TEST_USER_AGENT, | ||
| "x-api-key": "mb_wire_test_key", | ||
| }; | ||
|
|
||
| function clientOver(responses: Array<Response>) { | ||
| const capture = captureFetch(responses); | ||
| const mb = createClient(CREDENTIALS, { | ||
| userAgent: TEST_USER_AGENT, | ||
| fetchImpl: capture.fetch, | ||
| }); | ||
| return { mb, capture }; | ||
| } | ||
|
|
||
| async function thrownBy(run: () => Promise<unknown>): Promise<unknown> { | ||
| try { | ||
| await run(); | ||
| } catch (error: unknown) { | ||
| return error; | ||
| } | ||
| throw new Error("expected the call to reject"); | ||
| } | ||
|
|
||
| describe("content-translation resource wire requests", () => { | ||
| it("downloads the dictionary as an unparsed byte stream", async () => { | ||
| const { mb, capture } = clientOver([ | ||
| new Response(CSV_TEXT, { headers: { "content-type": "text/csv; charset=utf-8" } }), | ||
| ]); | ||
|
|
||
| const stream = await mb.contentTranslation.download(); | ||
|
|
||
| expect(await new Response(stream).text()).toBe(CSV_TEXT); | ||
| expect(capture.calls).toEqual([ | ||
| { | ||
| url: "https://mb.example.com/metabase/api/ee/content-translation/csv", | ||
| method: "GET", | ||
| headers: BINARY_READ_HEADERS, | ||
| body: null, | ||
| }, | ||
| ]); | ||
| }); | ||
|
|
||
| it("uploads the complete CSV as multipart form data", async () => { | ||
| const { mb, capture } = clientOver([jsonResponse({ success: true })]); | ||
|
|
||
| await mb.contentTranslation.upload(CSV_FILE); | ||
|
|
||
| expect(capture.calls).toEqual([ | ||
| { | ||
| url: "https://mb.example.com/metabase/api/ee/content-translation/upload-dictionary", | ||
| method: "POST", | ||
| headers: JSON_READ_HEADERS, | ||
| body: { | ||
| parts: [ | ||
| { | ||
| name: "file", | ||
| value: CSV_TEXT, | ||
| filename: "translations.csv", | ||
| contentType: "text/csv", | ||
| }, | ||
| ], | ||
| }, | ||
| }, | ||
| ]); | ||
| }); | ||
|
|
||
| it("returns the parsed upload confirmation", async () => { | ||
| const { mb } = clientOver([jsonResponse({ success: true })]); | ||
|
|
||
| await expect(mb.contentTranslation.upload(CSV_FILE)).resolves.toEqual({ success: true }); | ||
| }); | ||
|
|
||
| it("rejects an upload response that does not confirm success", async () => { | ||
| const { mb } = clientOver([jsonResponse({ success: false })]); | ||
|
|
||
| const error = await thrownBy(() => mb.contentTranslation.upload(CSV_FILE)); | ||
|
|
||
| assert(error instanceof ResponseShapeError, "expected a ResponseShapeError"); | ||
| expect(error.message).toContain("success: Invalid input: expected true"); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Pass an interruption signal through the streaming path.
The CSV download and stdout pipe can block until the transfer completes. Line 26 does not accept
interruptSignal, and Line 28 does not pass one to the client request. ThreadinterruptSignalthrough the command runtime,contentTranslation.download, and output piping so cancellation can abort the transfer.As per coding guidelines, “Operations that can block must accept
interruptSignalexplicitly, including ... long-running fetches.”🤖 Prompt for AI Agents
Source: Coding guidelines