improve translation with hardcoded terminology dictionary and British English
All checks were successful
CI / update (push) Successful in 9s

Add ingredient terminology dictionaries to override DeepL translations for consistent cooking terminology:
- German cooking terms (EL→tbsp, TL→tsp, Ei→egg, etc.)
- US to British English conversions (zucchini→courgette, eggplant→aubergine, etc.)

Change DeepL target language from EN to EN-GB to force British English translations.

Apply post-processing to all translated text to ensure terminology consistency.
This commit is contained in:
2025-12-27 13:43:24 +01:00
parent d4564ca973
commit 0d01e595ea

View File

@@ -14,6 +14,74 @@ const CATEGORY_TRANSLATIONS: Record<string, string> = {
"Snack": "Snack" "Snack": "Snack"
}; };
// Ingredient terminology dictionary - German cooking terms to British English
// These override DeepL translations for consistent terminology
const INGREDIENT_TERMINOLOGY: Record<string, string> = {
// Measurement abbreviations
"EL": "tbsp",
"TL": "tsp",
"Msp": "pinch",
"Prise": "pinch",
"Zweig": "twig",
"Zweige": "twigs",
"Bund": "bunch",
// Common ingredients
"Ei": "egg",
"Öl": "Oil",
"Backen": "Baking",
};
// US English to British English food terminology
// Applied after DeepL translation to ensure British English
const US_TO_BRITISH_ENGLISH: Record<string, string> = {
"zucchini": "courgette",
"zucchinis": "courgettes",
"eggplant": "aubergine",
"eggplants": "aubergines",
"cilantro": "coriander",
"arugula": "rocket",
"rutabaga": "swede",
"rutabagas": "swedes",
"bell pepper": "pepper",
"bell peppers": "peppers",
"scallion": "spring onion",
"scallions": "spring onions",
"green onion": "spring onion",
"green onions": "spring onions",
};
/**
* Apply ingredient terminology replacements to translated text
* Handles both German terms that may have slipped through DeepL
* and US English to British English conversions
* @param text - The translated text to process
* @returns Text with terminology replacements applied
*/
function applyIngredientTerminology(text: string): string {
if (!text) return text;
let result = text;
// First pass: Replace any remaining German terms with British English
// Using word boundaries to avoid partial matches
Object.entries(INGREDIENT_TERMINOLOGY).forEach(([german, english]) => {
// Case-insensitive replacement with word boundaries
const regex = new RegExp(`\\b${german}\\b`, 'gi');
result = result.replace(regex, english);
});
// Second pass: Replace US English terms with British English
// More careful here to handle both whole words and phrases
Object.entries(US_TO_BRITISH_ENGLISH).forEach(([us, british]) => {
// Case-insensitive replacement with word boundaries
const regex = new RegExp(`\\b${us}\\b`, 'gi');
result = result.replace(regex, british);
});
return result;
}
interface DeepLResponse { interface DeepLResponse {
translations: Array<{ translations: Array<{
detected_source_language: string; detected_source_language: string;
@@ -46,13 +114,13 @@ class DeepLTranslationService {
/** /**
* Translate a single text string * Translate a single text string
* @param text - The text to translate * @param text - The text to translate
* @param targetLang - Target language code (default: 'EN') * @param targetLang - Target language code (default: 'EN-GB' for British English)
* @param preserveFormatting - Whether to preserve HTML/formatting * @param preserveFormatting - Whether to preserve HTML/formatting
* @returns Translated text * @returns Translated text
*/ */
async translateText( async translateText(
text: string | null | undefined, text: string | null | undefined,
targetLang: string = 'EN', targetLang: string = 'EN-GB',
preserveFormatting: boolean = false preserveFormatting: boolean = false
): Promise<string> { ): Promise<string> {
// Return empty string for null, undefined, or empty strings // Return empty string for null, undefined, or empty strings
@@ -86,7 +154,10 @@ class DeepLTranslationService {
} }
const data: DeepLResponse = await response.json(); const data: DeepLResponse = await response.json();
return data.translations[0]?.text || ''; const translatedText = data.translations[0]?.text || '';
// Apply ingredient terminology replacements for British English
return applyIngredientTerminology(translatedText);
} catch (error) { } catch (error) {
console.error('Translation error:', error); console.error('Translation error:', error);
throw error; throw error;
@@ -97,12 +168,12 @@ class DeepLTranslationService {
* Translate multiple texts in a single batch request * Translate multiple texts in a single batch request
* More efficient than individual calls * More efficient than individual calls
* @param texts - Array of texts to translate * @param texts - Array of texts to translate
* @param targetLang - Target language code * @param targetLang - Target language code (default: 'EN-GB' for British English)
* @returns Array of translated texts (preserves empty strings in original positions) * @returns Array of translated texts (preserves empty strings in original positions)
*/ */
async translateBatch( async translateBatch(
texts: string[], texts: string[],
targetLang: string = 'EN' targetLang: string = 'EN-GB'
): Promise<string[]> { ): Promise<string[]> {
if (!texts.length) { if (!texts.length) {
return []; return [];
@@ -155,13 +226,16 @@ class DeepLTranslationService {
const data: DeepLResponse = await response.json(); const data: DeepLResponse = await response.json();
const translatedTexts = data.translations.map(t => t.text); const translatedTexts = data.translations.map(t => t.text);
// Apply ingredient terminology replacements for British English
const processedTexts = translatedTexts.map(text => applyIngredientTerminology(text));
// Map translated texts back to original positions, preserving empty strings // Map translated texts back to original positions, preserving empty strings
const result: string[] = []; const result: string[] = [];
let translatedIndex = 0; let translatedIndex = 0;
for (let i = 0; i < texts.length; i++) { for (let i = 0; i < texts.length; i++) {
if (nonEmptyIndices.includes(i)) { if (nonEmptyIndices.includes(i)) {
result.push(translatedTexts[translatedIndex]); result.push(processedTexts[translatedIndex]);
translatedIndex++; translatedIndex++;
} else { } else {
result.push(''); // Keep empty string result.push(''); // Keep empty string
@@ -340,10 +414,10 @@ class DeepLTranslationService {
result.description = await this.translateText(recipe.description); result.description = await this.translateText(recipe.description);
break; break;
case 'preamble': case 'preamble':
result.preamble = await this.translateText(recipe.preamble || '', 'EN', true); result.preamble = await this.translateText(recipe.preamble || '', 'EN-GB', true);
break; break;
case 'addendum': case 'addendum':
result.addendum = await this.translateText(recipe.addendum || '', 'EN', true); result.addendum = await this.translateText(recipe.addendum || '', 'EN-GB', true);
break; break;
case 'note': case 'note':
result.note = await this.translateText(recipe.note || ''); result.note = await this.translateText(recipe.note || '');