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
- Go to chrome://flags/#language-detection-api.
- Select Enabled
- Go to chrome://flags/#translation-api.
- Select Enabled without language pack limit to try more language pairs.
- Click Relaunch or restart Chrome.
- Open a new tab, go to chrome://components.
- Find Chrome TranslateKit
- Click "Check for update" button to download the language model. The version number should update.
- (Optional) Open a new tab, go to chrome://on-device-translation-internals/
- (Optional) Install language pairs.
Scaffold an Angular Application
ng new translation-api-demo
Install dependencies
npm i -save-exact -save-dev @types/dom-chromium-ai
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" />
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');
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;
},
}
]);
}
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()
]
};
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:
- Browser is Chrome
- Browser version is at least 131
- ai Object is in the window namespace
- Language Detection API’s status is readily
- 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);
}
}
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');
}
)
);
}
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: '' });
}
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;
})
);
}
}
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);
}
}
}
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;
}
@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 });
}
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);
}
}
}
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);
}
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();
}
}
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)});
}
Add a service to wrap the Translation API
const TRANSKIT_LANGUAGES = ['en', 'es', 'ja', 'zh', 'zh-Hant', 'it', 'fr'];
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;
}
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 '';
}
}
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 };
}
}
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.
Top comments (0)