DEV Community

Connie Leung
Connie Leung

Posted on

Build a translation app with Chrome's Built-in Translation API in Angular

In this blog post, I describe how to build a simple translation application locally using Chrome’s Built-In Translation API and Angular. First, the Angular application calls the API to create a language detector that detects the language code of the inputted text. Then, it creates a translator to translate texts between source and target languages.

The benefit of using Chrome's built-in AI is zero cost since the application uses the local models in Chrome Canary.

This is the happy path when users use Chrome Dev or Chrome Canary. A fallback implementation should exist if users use non-Chrome or old Chrome browsers, such as calling Gemma or Gemini on Vertex AI to return the translated text.

Install Gemini Nano on Chrome

Update the Chrome Dev/Canary to the latest version. As of this writing, the newest version of Chrome Canary is 133.

Please refer to this section to sign up for the early preview program of Chrome Built-in AI.
https://developer.chrome.com/docs/ai/built-in#get_an_early_preview

Please refer to this section to enable Gemini Nano on Chrome and download the model. https://developer.chrome.com/docs/ai/get-started#use_apis_on_localhost

Install Translation Detection API on Chrome

  1. Go to chrome://flags/#language-detection-api.
  2. Select Enabled
  3. Go to chrome://flags/#translation-api.
  4. Select Enabled without language pack limit to try more language pairs.
  5. Click Relaunch or restart Chrome.
  6. Open a new tab, go to chrome://components.
  7. Find Chrome TranslateKit
  8. Click "Check for update" button to download the language model. The version number should update.
  9. (Optional) Open a new tab, go to chrome://on-device-translation-internals/
  10. (Optional) Install language pairs.

Scaffold an Angular Application

ng new translation-api-demo
Enter fullscreen mode Exit fullscreen mode

Install dependencies

npm i -save-exact -save-dev @types/dom-chromium-ai
Enter fullscreen mode Exit fullscreen mode

This dependency provides the TypeScript typing of all the Chrome Built-in APIs. Therefore, developers can write elegant codes to build AI applications in TypeScript.

In main.ts, add a reference tag to point to the package's typing definition file.

// main.ts

/// <reference path="../../../node_modules/@types/dom-chromium-ai/index.d.ts" />   
Enter fullscreen mode Exit fullscreen mode

Bootstrap Language Detector and Translator

import { InjectionToken } from '@angular/core';

export const AI_TRANSLATION_API_TOKEN = new InjectionToken<AITranslatorFactory | undefined>('AI_TRANSLATION_API_TOKEN');

export const AI_LANGUAGE_DETECTION_API_TOKEN = new InjectionToken<AILanguageDetectorFactory | undefined>('AI_LANGUAGE_DETECTION_API_TOKEN');
Enter fullscreen mode Exit fullscreen mode
export function provideTranslationApi(): EnvironmentProviders {
   return makeEnvironmentProviders([
       {
           provide: AI_TRANSLATION_API_TOKEN,
           useFactory: () => {
               const platformId = inject(PLATFORM_ID);
               const objWindow = isPlatformBrowser(platformId) ? window : undefined;
               return objWindow?.ai?.translator ? objWindow.ai.translator : undefined;
           },
       },
       {
           provide: AI_LANGUAGE_DETECTION_API_TOKEN,
           useFactory: () => {
               const platformId = inject(PLATFORM_ID);
               const objWindow = isPlatformBrowser(platformId) ? window : undefined;
               return objWindow?.ai?.languageDetector ? objWindow?.ai?.languageDetector : undefined;
           },
       }
   ]);
}
Enter fullscreen mode Exit fullscreen mode

I define environment providers to return the languageDetector and translator in the window.ai namespace. When the codes inject the AI_LANGUAGE_DETECTION_API_TOKEN token, they can access the Language Detection API to call its' methods to detect the language code. When the code injects the AI_TRANSLATION_API_TOKEN token, they can access the Translation API to call its methods to translate the source and target languages.

// app.config.ts

export const appConfig: ApplicationConfig = {
  providers: [
    provideTranslationApi()
  ]
};
Enter fullscreen mode Exit fullscreen mode

In the application config, provideTranslationApi is imported into the providers array.

Validate browser version and API availability

Chrome's built-in AI is experimental, but the translation API is supported in Chrome version 131 and later. Therefore, I implemented validation logic to ensure the API is available before displaying the user interface so users can enter texts.

The validation rules include:

  1. Browser is Chrome
  2. Browser version is at least 131
  3. ai Object is in the window namespace
  4. Language Detection API’s status is readily
  5. Translation API’s status is readily
export async function checkChromeBuiltInAI(): Promise<string> {
  if (!isChromeBrowser()) {
     throw new Error(ERROR_CODES.NO_CHROME);
  }

  if (getChromVersion() < CHROME_VERSION) {
     throw new Error(ERROR_CODES.OLD_CHROME);
  }

  if (!('ai' in globalThis)) {
     throw new Error(ERROR_CODES.NO_TRANSLATION_API);
  }

  const languageDetection = inject(AI_LANGUAGE_DETECTION_API_TOKEN);
  const translation = inject(AI_TRANSLATION_API_TOKEN);
  await validateLanguageDetector(languageDetection);
  await validateLanguageTranslator(translation);

  return '';
}

async function validateLanguageTranslator(translation: AITranslatorFactory | undefined) {
  if (translation && 'capabilities' in translation) {
     const canTranslateStatus = (await translation.capabilities()).available;
     if (!canTranslateStatus) {
        throw new Error(ERROR_CODES.NO_TRANSLATOR);
     } else if (canTranslateStatus == 'after-download') {
        throw new Error(ERROR_CODES.TRANSLATION_AFTER_DOWNLLOAD);
     } else if (canTranslateStatus === 'no') {
        throw new Error(ERROR_CODES.NO_TRANSLATION_API);
     }
  }
}

async function validateLanguageDetector(languageDetector: AILanguageDetectorFactory | undefined) {
  const canDetectStatus = (await languageDetector?.capabilities())?.available;
  if (!canDetectStatus) {
     throw new Error(ERROR_CODES.NO_LANGUAGE_DETECTOR);
  } else if (canDetectStatus === 'after-download') {
     throw new Error(ERROR_CODES.TRANSLATION_AFTER_DOWNLLOAD);
  } else if (canDetectStatus === 'no') {
     throw new Error(ERROR_CODES.NO_TRANSLATION_API);
  }
}
Enter fullscreen mode Exit fullscreen mode

The checkChromeBuiltInAI function ensures the language detection and translation APIs are defined and ready to use. If checking fails, the function throws an error. Otherwise, it returns an empty string.

export function isTranslationApiSupported(): Observable<string> {
  return from(checkChromeBuiltInAI()).pipe(
     catchError(
        (e) => {
           console.error(e);
           return of(e instanceof Error ? e.message : 'unknown');
        }
     )
  );
}
Enter fullscreen mode Exit fullscreen mode

The isTranslationApiSupported function catches the error and returns an Observable of error message.

Display the AI components

@Component({
    selector: 'app-detect-ai',
    imports: [TranslationContainerComponent],
    template: `
    <div>
      @let error = hasCapability();
      @if (!error) {
        <app-translation-container />
      } @else if (error !== 'unknown') {
        {{ error }}
      }
    </div>
  `
})
export class DetectAIComponent {
  hasCapability = toSignal(isTranslationApiSupported(), { initialValue: '' });
}
Enter fullscreen mode Exit fullscreen mode

The DetectAIComponent renders the TranslationContainerComponent where there is no error. Otherwise, it displays the error message in the error signal.

// translation-container.component.ts 

@Component({
   selector: 'app-translation-container',
   imports: [LanguageDetectionComponent, TranslateTextComponent],
   template: `
   <div>
     <app-language-detection (nextStep)="updateCanTranslate($event)" />
     @let inputText = sample().inputText;
     @if (sample().sourceLanguage && inputText) {
       <app-translate-text [languagePairs]="languagePairs()" [inputText]="inputText"
         (downloadSuccess)="downloadNewLanguage($event)" />
     } @else if (inputText) {
       <p>{{ inputText }} cannot be translated.</p>
     }
   </div>
 `,
   changeDetection: ChangeDetectionStrategy.OnPush
})
export class TranslationContainerComponent {
 translationService = inject(TranslationService);
 languagePairs = signal<LanguagePairAvailable[]>([]);
 sample = signal({ sourceLanguage: '', inputText: '' });

 async updateCanTranslate(allowTranslation: AllowTranslation) {
   this.languagePairs.set([]);
   this.sample.set({ sourceLanguage: '', inputText: '' });
   if (allowTranslation?.toTranslate) {
     const { code: sourceLanguage, inputText } = allowTranslation;
     this.languagePairs.set(await this.translationService.createLanguagePairs(sourceLanguage));
     this.sample.set({ sourceLanguage, inputText });
   }
 }

 downloadNewLanguage(language: LanguagePairAvailable) {
   this.languagePairs.update((prev) => prev.map((item) => {
       if (item.sourceLanguage === language.sourceLanguage &&
         item.targetLanguage === language.targetLanguage) {
         return language
       }
       return item;
     })
   );
 }
}
Enter fullscreen mode Exit fullscreen mode

The TranslationContainerComponent has two components. The LanguageDetectionComponent detects the language of the inputted text. The TranslationTextComponent is responsible for translating the text from the detected language to the target language.

Language Detection

// language-detection.component.ts

@Component({
   selector: 'app-language-detection',
   imports: [FormsModule, LanguageDetectionResultComponent],
   template: `
   <div>
     <div>
       <span class="label" for="input">Input text: </span>
       <textarea id="input" name="input" [(ngModel)]="inputText" rows="3"></textarea>
     </div>
     <button (click)="setup()">Create language detector</button>
     <button (click)="detectLanguage()" [disabled]="isDisableDetectLanguage()">Detect Language</button>
     <app-language-detection-result [detectedLanguage]="detectedLanguage()" [minConfidence]="minConfidence" />
   </div>
 `,
   changeDetection: ChangeDetectionStrategy.OnPush
})
export class LanguageDetectionComponent {
 service = inject(LanguageDetectionService);
 inputText = signal('Buenos tarde. Mucho Gusto. Hoy es 23 de Noviembre, 2024 y Mi charla es sobre Chrome Built-in AI.');
 detectedLanguage = signal<LanguageDetectionWithNameResult | undefined>(undefined);

 detector = this.service.detector;
 isDisableDetectLanguage = computed(() => !this.detector() || this.inputText().trim() === '');
 nextStep = output<AllowTranslation>();

 minConfidence = 0.6;

 async setup() {
   await this.service.createDetector();
 }

 async detectLanguage() {
   const inputText = this.inputText().trim();
   const result = await this.service.detect(inputText, this.minConfidence);
   this.detectedLanguage.set(result);

   if (result?.detectedLanguage) {
     this.nextStep.emit({
       code: result.detectedLanguage,
       toTranslate: result.confidence >= this.minConfidence,
       inputText,
     });
   } else {
     this.nextStep.emit(undefined);
   }
 }
}
Enter fullscreen mode Exit fullscreen mode

The LanguageDetectionComponent displays two buttons: the first button allows the users to create a language detector and the second button detects the language of the inputted text.

export type LanguageDetectionWithNameResult = LanguageDetectionResult & {
   name: string;
}
Enter fullscreen mode Exit fullscreen mode
@Component({
   selector: 'app-language-detection-result',
   imports: [ConfidencePipe],
   template: `
   <div>
       <span class="label">Response: </span>
       @let language = detectedLanguage();
       @if (language) {
         <p>
           <span>Confidence: {{ language.confidence | confidence:minConfidence() }}, </span>
           <span>Detected Language: {{ language.detectedLanguage }}, </span>
           <span>Detected Language Name: {{ language.name }}</span>
         </p>
       }
   </div>
 `,
   changeDetection: ChangeDetectionStrategy.OnPush
})
export class LanguageDetectionResultComponent {
   detectedLanguage = input.required<LanguageDetectionWithNameResult | undefined>();
   minConfidence = input.required<number, number>({ transform: (data) => data < 0 || data > 1 ? 0.6 : data });
}
Enter fullscreen mode Exit fullscreen mode

The LanguageDetectionResultComponent is a presentation component that displays the value of confidence, high or low confidence, language code, and language name.

Translation

@Component({
   selector: 'app-translate-text',
   imports: [FormsModule],
   template: `
   <div>
       <div>
           @for(pair of languagePairs(); track $index) {
               <p>canTranslate('{{ pair.sourceLanguage }}', '{{ pair.targetLanguage}}') = {{ pair.available }}</p>
           }
       </div>
       <div>
           <div>
               @for(item of canTranslateButtons(); track $index) {
                   @let pair = { sourceLanguage: item.sourceLanguage, targetLanguage: item.targetLanguage };
                   @if (item.available === 'readily') {
                       <button (click)="translateText(pair)">{{ item.text }}</button>
                   } @else if (item.available === 'after-download') {
                       <button (click)="download(pair)">{{ item.text }}</button>
                   }
               }
           </div>
           <div>
               <p>Translation: {{ translation() }}</p>
           </div>
       </div>
   </div>
 `,
   changeDetection: ChangeDetectionStrategy.OnPush
})
export class TranslateTextComponent {
   service = inject(TranslationService);
   languagePairs = input.required<LanguagePairAvailable[]>();
   inputText = input.required<string>();
   translation = signal('');
   downloadSuccess = output<LanguagePairAvailable>();

   canTranslateButtons = computed(() =>
       this.languagePairs().reduce((acc, pair) => {
           if (pair.available === 'readily') {
               return acc.concat({ ...pair, text: `${pair.sourceLanguage} to ${pair.targetLanguage}` })
           } else if (pair.available === 'after-download') {
               return acc.concat({ ...pair, text: `Download ${pair.targetLanguage}` })
           }
           return acc;
       }, [] as (LanguagePairAvailable & { text: string })[])
   );

   async translateText(languagePair: LanguagePair) {
       this.translation.set('');
       const result = await this.service.translate(languagePair, this.inputText());
       this.translation.set(result);
   }

   async download(languagePair: LanguagePair) {
       try {
        const result = await this.service.downloadLanguagePackage(languagePair);
        if (result?.available === 'readily') {
           this.downloadSuccess.emit(result);
        }
       } catch (e) {
           console.error(e);
       }
   }
}
Enter fullscreen mode Exit fullscreen mode

The TranslationTextComponent can translate texts into English, Japanese, Spanish,Simplified Chinese, Traditional Chinese, Italian, and French.

The component calls the Translation API to determine the status of the source and target languages. When the status is "readily", translation can occur. When the status is "after-download", translation can occur after downloading the On Device Translation model. When the status is "no", translation cannot occur.

When translation is possible, the component passes the source and target languages and the inputted text to the API to obtain the translated text.

When a translation is possible after download, users must perform two steps. First, they click the button to download the model. Then, they click the same button to allow the translation to take place.

Add a service to wrap the Language Detection API

The LanguageDetectionService service encapsulates the logic of the Language Detection API. The createDetector method creates a detector and stores it in a signal.

async createDetector() {
       if (this.detector()) {
           console.log('Language Detector found.');
           return;
       }

       const canCreatStatus = (await this.#languageDetectionAPI?.capabilities())?.available === 'readily';
       if (!canCreatStatus) {
           throw new Error(ERROR_CODES.NO_LANGUAGE_DETECTOR);
       }
       const newDetector = await this.#languageDetectionAPI?.create();
       this.#detector.set(newDetector);
}
Enter fullscreen mode Exit fullscreen mode

To avoid memory leaks, the ngOnDestroy method destroys the detector when the service is destroyed.

ngOnDestroy(): void {
   const detector = this.detector();
   if (detector) {
       detector.destroy();
   }
}
Enter fullscreen mode Exit fullscreen mode

The detect method accepts text and calls the API to return the results ordered by confidence in descending order. This method returns the first language where the confidence is at least 0.6.

async detect(query: string, minConfidence = 0.6) {
    if (!this.#translationAPI) {
       throw new Error(ERROR_CODES.NO_API);
    }

    const detector = this.detector();
    if (!detector) {
        throw new Error(ERROR_CODES.NO_LANGUAGE_DETECTOR);
    }

    const results = await detector.detect(query);
    if (!results.length) {
         return undefined;
    }

    // choose the first language for which its confidence >= minConfidence
    const highConfidenceResult = results.find((result) => result.confidence >= minConfidence);

    const finalLanguage = highConfidenceResult ? highConfidenceResult : results[0];
    return ({ ...finalLanguage, name: this.languageTagToHumanReadable(finalLanguage.detectedLanguage)});
}
Enter fullscreen mode Exit fullscreen mode

Add a service to wrap the Translation API

const TRANSKIT_LANGUAGES = ['en', 'es', 'ja', 'zh', 'zh-Hant', 'it', 'fr'];
Enter fullscreen mode Exit fullscreen mode

The service can translate the source language into English, Spanish, Japanese, Simplified Chinese, Traditional Chinese, Italian, and French.

async createLanguagePairs(sourceLanguage: string): Promise<LanguagePairAvailable[]> {
    if (!this.#translationAPI) {
        throw new Error(ERROR_CODES.NO_API);
    }

    const results: LanguagePairAvailable[] = [];
    for (const targetLanguage of TRANSKIT_LANGUAGES) {
        if (sourceLanguage !== targetLanguage) {
            const capabilities = await this.#translationAPI.capabilities();
            const available = capabilities.languagePairAvailable(sourceLanguage, targetLanguage);
            if (available !== 'no') {
               results.push({ sourceLanguage, targetLanguage, available });
            }
        }
   }
   return results;
}
Enter fullscreen mode Exit fullscreen mode

This method calls the languagePairAvailable method to determine the status of the language pair. When the status is "readily", translation can occur immediately. When the status is "after-download", translation can occur after users manually download the On Device Translation Model. When the status is "no", translation cannot occur, and the users should not see the option.

async translate(languagePair: LanguagePair, inputText: string): Promise<string> {
       try {
           if (!this.#translationAPI) {
               throw new Error(ERROR_CODES.NO_API);
           }

           const translator = await this.#translationAPI.create({ ...languagePair, signal: this.#controller.signal });
           if (!translator) {
               return '';
           }

           const result = await translator.translate(inputText);           
           translator.destroy();

           return result;
       } catch (e) {
           console.error(e);
           return '';
       }
}
Enter fullscreen mode Exit fullscreen mode

This method calls the API's create method to create a translator. Then, it calls the translator's translate method to translate texts between the given language pair. After obtaining the result, the method destroys the translator to release the resources.

async downloadLanguagePackage(languagePair: LanguagePair) {
       try {
           if (!this.#translationAPI) {
               throw new Error(ERROR_CODES.NO_API);
           }

           const translator = await this.#translationAPI.create({ ...languagePair, signal: this.#controller.signal });
           const capabilities = await this.#translationAPI.capabilities();
           const available = capabilities.languagePairAvailable(languagePair.sourceLanguage, languagePair.targetLanguage)

           translator.destroy();

           return { ...languagePair, available };
       } catch (e) {
           console.error(e);
           return { ...languagePair, available: 'no' as AICapabilityAvailability };
       }
}
Enter fullscreen mode Exit fullscreen mode

This method calls the API's create method to create a translator. The action triggers the browser to download the On Device Translation model. Then, it calls the API's languagePairAvailable method to determine the availability of the language pair. After downloading the model, the method destroys the translator to release the resources.

In conclusion, software engineers can create Web AI applications without setting up a backend server or accumulating the costs of LLM on the cloud.

Resources:

Top comments (0)