DEV Community

Connie Leung
Connie Leung

Posted on

Build a language detection app with Chrome's Language Detection API in Angular

In this blog post, I describe how to build a simple language detection application locally using Chrome's Built-In Language Detection API and Angular. First, the Angular application calls the API to create a language detector and use it to detect the language code of the inputted text. Then, it invokes a helper function to map the language code into the language name.

The cost is zero since the application does not need any vendor's LLM.

This is the happy path when users use Chrome Dev or Chrome Canary. If users use non-Chrome or old Chrome browsers, a fallback implementation should exist, such as calling Gemma or Gemini on Vertex AI to return the language name of the 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 Language Detection API on Chrome

  1. Go to chrome://flags/#language-detection-api.
  2. Select Enabled.
  3. Click Relaunch or restart Chrome.

Scaffold an Angular Application

ng new language-detector-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

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

export const AI_LANGUAGE_DETECTION_API_TOKEN = new InjectionToken<AILanguageDetectorFactory | undefined>('AI_LANGUAGE_DETECTION_API_TOKEN');
Enter fullscreen mode Exit fullscreen mode
import { AI_LANGUAGE_DETECTION_API_TOKEN } from '../constants/core.constant';

export function provideAILanguageDetectionAPI(): EnvironmentProviders {
    return makeEnvironmentProviders([
        {
            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 an environment provider to return the languageDetector instance 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.

// app.config.ts

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

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

Validate browser version and API availability

Chrome's built-in AI is experimental, but the language detection API is supported in Chrome version 129 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 129
  3. ai Object is in the window namespace
  4. Language Detection API’s status is readily
export async function checkChromeBuiltInAI(): Promise<string> {
  if (!isChromeBrowser()) {
     throw new Error(ERROR_CODES.UNSUPPORTED_BROWSER);
  }

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

  const apiName = 'Language Detection API';
  if (!('ai' in globalThis)) {
     throw new Error(ERROR_CODES.NO_API);
  }

  const languageDetector = inject(AI_LANGUAGE_DETECTION_API_TOKEN);
  const status = (await languageDetector?.capabilities())?.available;
  if (!status) {
     throw new Error(ERROR_CODES.NO_API);
  } else if (status === 'after-download') {
     throw new Error(ERROR_CODES.AFTER_DOWNLOAD);
  } else if (status === 'no') {
     throw new Error(ERROR_CODES.NO_GEMINI_NANO);
  }

  return '';
}
Enter fullscreen mode Exit fullscreen mode

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

export function isLanguageDetectionAPISupported(): 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 isLanguageDetectionAPISupported function catches the error and returns an Observable of error message.

Display the AI components

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

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

@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 capabilities and detector</button>
      <button (click)="teardown()">Destroy capabilities and detector</button>
      <button (click)="detectLanguage()" [disabled]="isDisableDetectLanguage()">Detect Language</button>
      <app-language-detection-result [detectedLanguages]="detectedLanguages()" />
    </div>`
})
export class LanguageDetectionComponent {
  service = inject(LanguageDetectionService);
  inputText = signal('');
  detectedLanguages = signal<LanguageDetectionWithNameResult[]>([]);

  capabilities = this.service.capabilities;
  detector = this.service.detector;

  isDisableDetectLanguage = computed(() => 
this.capabilities()?.available !== 'readily' 
|| !this.detector() || this.inputText().trim() === '');

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

  teardown() {
    this.service.destroyDetector();
  }

  async detectLanguage(topNLanguages = 3) {
    const results = await this.service.detect(this.inputText(), topNLanguages);
    this.detectedLanguages.set(results);
  }
}
Enter fullscreen mode Exit fullscreen mode

The LanguageDetectionComponent has three buttons. The create button calls the language detection service to create a language detector. The destroy button destroys the language detector to release the resources. The detect button calls the Language Detection API to detect the top three language codes.

export type LanguageDetectionWithNameResult = LanguageDetectionResult & {
   name: string;
}
Enter fullscreen mode Exit fullscreen mode
@Component({
 selector: 'app-language-detection-result',
 template: `
   <div>
       <span class="label">Response: </span>
       @for (language of detectedLanguages(); track language.detectedLanguage) {
         <p>
           <span>Confidence: {{ language.confidence.toFixed(3) }}, </span>
           <span>Detected Language: {{ language.detectedLanguage }}, </span>
           <span>Detected Language Name: {{ language.name }}</span>
         </p>
       }
   </div>
 `,
})
export class LanguageDetectionResultComponent {
   detectedLanguages = input<LanguageDetectionWithNameResult[]>([]);
}
Enter fullscreen mode Exit fullscreen mode

The LanguageDetectionResultComponent is a presentation component that displays the confidences, language codes, and language names.

Add a service to wrap the Language Detection API

@Injectable({
 providedIn: 'root'
})
export class LanguageDetectionService implements OnDestroy  {
   #controller = new AbortController();

   #languageDetectionAPI = inject(AI_LANGUAGE_DETECTION_API_TOKEN);
   #detector = signal<AILanguageDetector| undefined>(undefined);
   detector = this.#detector.asReadonly();
   #capabilities = signal<AILanguageDetectorCapabilities | null>(null);
   capabilities = this.#capabilities.asReadonly();

   async detect(query: string, topNResults = 3): Promise<LanguageDetectionWithNameResult[]> {
      
   }

   destroyDetector() {
      
   }

   private resetDetector() {
       const detector = this.detector();

       if (detector) {
           detector.destroy();
           console.log('Destroy the language detector.');
           this.#detector.set(undefined);
       }
   }

   async createDetector() {
       
   }

   ngOnDestroy() {
      ...
   }
}
Enter fullscreen mode Exit fullscreen mode

The LanguageDetectionService service encapsulates the logic of the Language Detection API. The createDetector method sets the capability status and creates a detector.

async createDetector() {
    if (!this.#languageDetectionAPI) {
        throw new Error(ERROR_CODES.NO_API);
    }

    this.resetDetector();
    const [capabilities, newDetector]  = await Promise.all([
           this.#languageDetectionAPI.capabilities(),
           this.#languageDetectionAPI.create({ signal: this.#controller.signal })
     ]);
     this.#capabilities.set(capabilities);
     this.#detector.set(newDetector);
}
Enter fullscreen mode Exit fullscreen mode

The destroyDetector method clears the capability status and destroys the detector.

destroyDetector() {
       this.#capabilities.set(null);
       this.resetDetector();
}
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 top three results and their language names.

async detect(query: string, topNResults = 3) {
       if (!this.#languageDetectionAPI) {
           throw new Error(ERROR_CODES.NO_API);
       }

       const detector = this.detector();
       if (!detector) {
           throw new Error('Failed to create the LanguageDetector.');
       }

       const minTopNReesults = Math.min(topNResults, MAX_LANGUAGE_RESULTS);
       const results = await detector.detect(query);
       const probablyLanguages = results.slice(0, minTopNReesults);
       return probablyLanguages.map((item) => ({ ...item, name: this.languageTagToHumanReadable(item.detectedLanguage) }))
}
Enter fullscreen mode Exit fullscreen mode

When the service is destroyed, the ngOnDestroy lifecycle method is invoked to destroy the language detector to release the memory.

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

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 (2)

Collapse
 
opensourcee profile image
OpenSource

I always thought it was as easy as getting the navigator.language

Collapse
 
railsstudent profile image
Connie Leung

navigator.language returns the language of the browser. The example detects the language of the input. My browser is English and I can input Chinese that the AI recognizes.