Skip to content

Commit c567ca9

Browse files
authored
fix(backend): support Traditional Chinese translation via two-step LLM approach (#409)
Google Cloud Translation LLM does not support zh-TW (Traditional Chinese) directly. This implements a two-step translation approach to maintain LLM quality while supporting Traditional Chinese: 1. For en → zh-TW: translate en → zh-CN using Translation LLM, then convert zh-CN → zh-TW using NMT 2. For zh-CN → zh-TW: direct conversion using NMT 3. For all other language pairs: continue using Translation LLM directly This ensures high-quality LLM-based translation for the semantic translation step (English to Chinese), with only the script conversion (Simplified to Traditional) using the standard NMT model. Also added early return for empty strings to avoid unnecessary API calls. Changes: - Updated translateText() in shared-backend to handle zh-TW specially - Synced changes to api and math-updater services - Added empty string guard at function entry point
1 parent 81b0b33 commit c567ca9

3 files changed

Lines changed: 261 additions & 0 deletions

File tree

services/api/src/shared-backend/translate.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,97 @@ export async function translateText(
5252
projectId: string,
5353
location: string,
5454
): Promise<string> {
55+
// Handle empty string early - no translation needed
56+
if (!text) {
57+
return "";
58+
}
59+
5560
// Convert BCP 47 codes to Google Cloud Translation language codes
5661
const googleSourceCode = convertToGoogleLanguageCode(sourceLanguageCode);
5762
const googleTargetCode = convertToGoogleLanguageCode(targetLanguageCode);
5863

64+
// For Traditional Chinese (zh-TW): use two-step translation for LLM quality
65+
if (googleTargetCode === "zh-TW") {
66+
// Case 1: Source is already Simplified Chinese → just convert to Traditional
67+
if (googleSourceCode === "zh-CN") {
68+
const request = {
69+
parent: `projects/${projectId}/locations/${location}`,
70+
contents: [text],
71+
mimeType: "text/plain" as const,
72+
sourceLanguageCode: "zh-CN",
73+
targetLanguageCode: "zh-TW",
74+
// Omit model parameter to use default NMT for zh-CN → zh-TW conversion
75+
};
76+
77+
const [response] = await client.translateText(request);
78+
79+
if (
80+
!response.translations ||
81+
response.translations.length === 0 ||
82+
!response.translations[0].translatedText
83+
) {
84+
throw new Error(
85+
`Translation failed for zh-CN → zh-TW conversion: "${text}"`,
86+
);
87+
}
88+
89+
return response.translations[0].translatedText;
90+
}
91+
92+
// Case 2: Source is not Simplified Chinese → translate to Simplified first, then convert
93+
// Step 1: Translate source → Simplified Chinese using LLM
94+
const simplifiedRequest = {
95+
parent: `projects/${projectId}/locations/${location}`,
96+
contents: [text],
97+
mimeType: "text/plain" as const,
98+
sourceLanguageCode: googleSourceCode,
99+
targetLanguageCode: "zh-CN",
100+
model: `projects/${projectId}/locations/${location}/models/general/translation-llm`,
101+
};
102+
103+
const [simplifiedResponse] =
104+
await client.translateText(simplifiedRequest);
105+
106+
if (
107+
!simplifiedResponse.translations ||
108+
simplifiedResponse.translations.length === 0 ||
109+
!simplifiedResponse.translations[0].translatedText
110+
) {
111+
throw new Error(
112+
`Translation failed at Simplified Chinese step for "${text}"`,
113+
);
114+
}
115+
116+
const simplifiedText =
117+
simplifiedResponse.translations[0].translatedText;
118+
119+
// Step 2: Convert Simplified Chinese → Traditional Chinese using NMT
120+
const traditionalRequest = {
121+
parent: `projects/${projectId}/locations/${location}`,
122+
contents: [simplifiedText],
123+
mimeType: "text/plain" as const,
124+
sourceLanguageCode: "zh-CN",
125+
targetLanguageCode: "zh-TW",
126+
// Omit model parameter to use default NMT for zh-CN → zh-TW conversion
127+
};
128+
129+
const [traditionalResponse] =
130+
await client.translateText(traditionalRequest);
131+
132+
if (
133+
!traditionalResponse.translations ||
134+
traditionalResponse.translations.length === 0 ||
135+
!traditionalResponse.translations[0].translatedText
136+
) {
137+
throw new Error(
138+
`Translation failed at Traditional Chinese conversion step for "${simplifiedText}"`,
139+
);
140+
}
141+
142+
return traditionalResponse.translations[0].translatedText;
143+
}
144+
145+
// For all other languages: use Translation LLM directly
59146
const request = {
60147
parent: `projects/${projectId}/locations/${location}`,
61148
contents: [text],

services/math-updater/src/shared-backend/translate.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,97 @@ export async function translateText(
5252
projectId: string,
5353
location: string,
5454
): Promise<string> {
55+
// Handle empty string early - no translation needed
56+
if (!text) {
57+
return "";
58+
}
59+
5560
// Convert BCP 47 codes to Google Cloud Translation language codes
5661
const googleSourceCode = convertToGoogleLanguageCode(sourceLanguageCode);
5762
const googleTargetCode = convertToGoogleLanguageCode(targetLanguageCode);
5863

64+
// For Traditional Chinese (zh-TW): use two-step translation for LLM quality
65+
if (googleTargetCode === "zh-TW") {
66+
// Case 1: Source is already Simplified Chinese → just convert to Traditional
67+
if (googleSourceCode === "zh-CN") {
68+
const request = {
69+
parent: `projects/${projectId}/locations/${location}`,
70+
contents: [text],
71+
mimeType: "text/plain" as const,
72+
sourceLanguageCode: "zh-CN",
73+
targetLanguageCode: "zh-TW",
74+
// Omit model parameter to use default NMT for zh-CN → zh-TW conversion
75+
};
76+
77+
const [response] = await client.translateText(request);
78+
79+
if (
80+
!response.translations ||
81+
response.translations.length === 0 ||
82+
!response.translations[0].translatedText
83+
) {
84+
throw new Error(
85+
`Translation failed for zh-CN → zh-TW conversion: "${text}"`,
86+
);
87+
}
88+
89+
return response.translations[0].translatedText;
90+
}
91+
92+
// Case 2: Source is not Simplified Chinese → translate to Simplified first, then convert
93+
// Step 1: Translate source → Simplified Chinese using LLM
94+
const simplifiedRequest = {
95+
parent: `projects/${projectId}/locations/${location}`,
96+
contents: [text],
97+
mimeType: "text/plain" as const,
98+
sourceLanguageCode: googleSourceCode,
99+
targetLanguageCode: "zh-CN",
100+
model: `projects/${projectId}/locations/${location}/models/general/translation-llm`,
101+
};
102+
103+
const [simplifiedResponse] =
104+
await client.translateText(simplifiedRequest);
105+
106+
if (
107+
!simplifiedResponse.translations ||
108+
simplifiedResponse.translations.length === 0 ||
109+
!simplifiedResponse.translations[0].translatedText
110+
) {
111+
throw new Error(
112+
`Translation failed at Simplified Chinese step for "${text}"`,
113+
);
114+
}
115+
116+
const simplifiedText =
117+
simplifiedResponse.translations[0].translatedText;
118+
119+
// Step 2: Convert Simplified Chinese → Traditional Chinese using NMT
120+
const traditionalRequest = {
121+
parent: `projects/${projectId}/locations/${location}`,
122+
contents: [simplifiedText],
123+
mimeType: "text/plain" as const,
124+
sourceLanguageCode: "zh-CN",
125+
targetLanguageCode: "zh-TW",
126+
// Omit model parameter to use default NMT for zh-CN → zh-TW conversion
127+
};
128+
129+
const [traditionalResponse] =
130+
await client.translateText(traditionalRequest);
131+
132+
if (
133+
!traditionalResponse.translations ||
134+
traditionalResponse.translations.length === 0 ||
135+
!traditionalResponse.translations[0].translatedText
136+
) {
137+
throw new Error(
138+
`Translation failed at Traditional Chinese conversion step for "${simplifiedText}"`,
139+
);
140+
}
141+
142+
return traditionalResponse.translations[0].translatedText;
143+
}
144+
145+
// For all other languages: use Translation LLM directly
59146
const request = {
60147
parent: `projects/${projectId}/locations/${location}`,
61148
contents: [text],

services/shared-backend/src/translate.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,97 @@ export async function translateText(
5151
projectId: string,
5252
location: string,
5353
): Promise<string> {
54+
// Handle empty string early - no translation needed
55+
if (!text) {
56+
return "";
57+
}
58+
5459
// Convert BCP 47 codes to Google Cloud Translation language codes
5560
const googleSourceCode = convertToGoogleLanguageCode(sourceLanguageCode);
5661
const googleTargetCode = convertToGoogleLanguageCode(targetLanguageCode);
5762

63+
// For Traditional Chinese (zh-TW): use two-step translation for LLM quality
64+
if (googleTargetCode === "zh-TW") {
65+
// Case 1: Source is already Simplified Chinese → just convert to Traditional
66+
if (googleSourceCode === "zh-CN") {
67+
const request = {
68+
parent: `projects/${projectId}/locations/${location}`,
69+
contents: [text],
70+
mimeType: "text/plain" as const,
71+
sourceLanguageCode: "zh-CN",
72+
targetLanguageCode: "zh-TW",
73+
// Omit model parameter to use default NMT for zh-CN → zh-TW conversion
74+
};
75+
76+
const [response] = await client.translateText(request);
77+
78+
if (
79+
!response.translations ||
80+
response.translations.length === 0 ||
81+
!response.translations[0].translatedText
82+
) {
83+
throw new Error(
84+
`Translation failed for zh-CN → zh-TW conversion: "${text}"`,
85+
);
86+
}
87+
88+
return response.translations[0].translatedText;
89+
}
90+
91+
// Case 2: Source is not Simplified Chinese → translate to Simplified first, then convert
92+
// Step 1: Translate source → Simplified Chinese using LLM
93+
const simplifiedRequest = {
94+
parent: `projects/${projectId}/locations/${location}`,
95+
contents: [text],
96+
mimeType: "text/plain" as const,
97+
sourceLanguageCode: googleSourceCode,
98+
targetLanguageCode: "zh-CN",
99+
model: `projects/${projectId}/locations/${location}/models/general/translation-llm`,
100+
};
101+
102+
const [simplifiedResponse] =
103+
await client.translateText(simplifiedRequest);
104+
105+
if (
106+
!simplifiedResponse.translations ||
107+
simplifiedResponse.translations.length === 0 ||
108+
!simplifiedResponse.translations[0].translatedText
109+
) {
110+
throw new Error(
111+
`Translation failed at Simplified Chinese step for "${text}"`,
112+
);
113+
}
114+
115+
const simplifiedText =
116+
simplifiedResponse.translations[0].translatedText;
117+
118+
// Step 2: Convert Simplified Chinese → Traditional Chinese using NMT
119+
const traditionalRequest = {
120+
parent: `projects/${projectId}/locations/${location}`,
121+
contents: [simplifiedText],
122+
mimeType: "text/plain" as const,
123+
sourceLanguageCode: "zh-CN",
124+
targetLanguageCode: "zh-TW",
125+
// Omit model parameter to use default NMT for zh-CN → zh-TW conversion
126+
};
127+
128+
const [traditionalResponse] =
129+
await client.translateText(traditionalRequest);
130+
131+
if (
132+
!traditionalResponse.translations ||
133+
traditionalResponse.translations.length === 0 ||
134+
!traditionalResponse.translations[0].translatedText
135+
) {
136+
throw new Error(
137+
`Translation failed at Traditional Chinese conversion step for "${simplifiedText}"`,
138+
);
139+
}
140+
141+
return traditionalResponse.translations[0].translatedText;
142+
}
143+
144+
// For all other languages: use Translation LLM directly
58145
const request = {
59146
parent: `projects/${projectId}/locations/${location}`,
60147
contents: [text],

0 commit comments

Comments
 (0)