feat(dashboard): offer AI translation in Import Tasks preview (#2128)

## Summary

Import Tasks can now offer on-demand AI translation when a selected
GitHub or GitLab issue/PR title and body appear to be in a different
language than the active dashboard locale.

- Detect foreign-language content with a conservative client heuristic
(Unicode scripts + Latin stopwords)
- Show an opt-in banner: **Translate**, then **Show original / Show
translation**, plus **Dismiss**
- Call new `POST /api/ai/translate-text` (shared AI-helper rate limit
with refine/draft)
- Translation is **display-only** in the preview; imported task text
stays the original source language

## Why

Operators working in a non-English dashboard (or reading
non-dashboard-language issues) needed a way to understand import
candidates without leaving the preview or changing what gets imported.

## Test plan

- [x] Unit tests for language detection (`detectContentLanguage`)
- [x] Unit tests for translate request validation, response parsing, and
AI agent path
- [x] GitHub import modal: French content shows translate controls;
English content does not
- [x] Dashboard typecheck clean for app + server packages
- [ ] Manual: open Import Tasks with dashboard language English, select
a French/Korean issue, translate and toggle original
- [ ] Manual: confirm Import still creates the task with original
title/body
- [ ] Manual: dismiss banner for a selection and confirm it stays
dismissed for that item

## Notes

- Comments are not translated (title + body only)
- zh-CN / zh-TW share a CJK family so Chinese content does not prompt
translation when the UI is either Chinese locale
- Secondary locale catalogs have empty placeholders for the new
`git.translate*` keys (runtime falls back to English)
This commit is contained in:
gsxdsm
2026-07-15 02:18:43 -07:00
committed by GitHub
parent 78ef3075f6
commit bc2d22df6e
19 changed files with 1547 additions and 52 deletions

View File

@@ -6183,6 +6183,77 @@ export async function draftGoalDescription(title: string, projectId?: string): P
return response.description;
}
/*
FNXC:GitHubImportTranslate 2026-07-14-12:00:
Client for POST /api/ai/translate-text — used by the GitHub/GitLab import preview when issue/PR prose is not the dashboard language.
Structured title+body fields keep markdown import content intact; shares the AI-helper rate-limit budget with refine/draft.
*/
export interface TranslateImportFields {
title?: string;
body?: string;
}
export interface TranslateImportContentResponse {
fields: TranslateImportFields;
}
/**
* Translate import-preview title/body into the dashboard locale via AI.
* @param fields - Original title and/or body
* @param targetLocale - Active dashboard locale
* @param projectId - Optional project scope for settings/MCP
* @param sourceLocale - Optional detection hint for the model
*/
export async function translateImportContent(
fields: TranslateImportFields,
targetLocale: string,
projectId?: string,
sourceLocale?: string,
): Promise<TranslateImportFields> {
const response = await api<TranslateImportContentResponse>(
withProjectId("/ai/translate-text", projectId),
{
method: "POST",
body: JSON.stringify({
fields,
targetLocale,
...(sourceLocale ? { sourceLocale } : {}),
}),
},
);
return response.fields;
}
/** User-facing error copy for translateImportContent failures (toast/banner). */
export const TRANSLATE_ERROR_MESSAGES = {
RATE_LIMIT: "Too many translation requests. Please wait an hour.",
NETWORK: "Failed to translate content. Please try again.",
} as const;
/**
* Map a translateImportContent error to banner-safe copy.
*/
export function getTranslateErrorMessage(error: unknown): string {
if (!(error instanceof Error)) {
return TRANSLATE_ERROR_MESSAGES.NETWORK;
}
const message = error.message.toLowerCase();
if (message.includes("rate limit") || message.includes("429")) {
return TRANSLATE_ERROR_MESSAGES.RATE_LIMIT;
}
if (
message.startsWith("fields") ||
message.startsWith("text to translate") ||
message.startsWith("targetlocale") ||
message.includes("targetlocale must") ||
message.includes("sourceLocale must")
) {
return error.message;
}
return TRANSLATE_ERROR_MESSAGES.NETWORK;
}
export function startSubtaskBreakdown(description: string, projectId?: string): Promise<{ sessionId: string }> {
return api<{ sessionId: string }>(withProjectId("/subtasks/start-streaming", projectId), {
method: "POST",