Text Translator With Firebase Cloud Functions Onwrite and Angular
⚠️ This lesson has been archived! Check out the Full Angular Course for the latest best practices about building a CRUD app.
In this lesson, we are going to use Firebase Cloud Functions to run code in the background when new data is created in a specific part of the database, using the onWrite event handler. This will allow us to abstract CPU or memory intensive tasks outside of the frontend Angular app.
We are going to build simple text translator using the Google Translate API. The user can paste write something into a textarea, then click the translate button to trigger the function. Running multiple translations client-side would add way too much overhead to our app, so were going to delegate it to a cloud function.
Building the Translator App
The Angular app is going to use a service to push the user’s initial English text to the database. The component will subscribe to the data and update the translations in realtime when the Google Translate finishes its work.
ng g service translate
ng g component text-translate
The NoSQL database structure looks like this. User input must be in english, and translations are correspond to their language code (i.e. fr==French, ar==Arabic)
translations
$translationId
english: string
fr: string
es: string
ar: string
The Translate Service
Our service has one simple task - Create a new translation in the database and return it as a `FirebaseObjectObservable`.
import { Injectable } from '@angular/core';
import { AngularFireDatabase, FirebaseObjectObservable } from 'angularfire2/database';
@Injectable()
export class TranslateService {
constructor(private db: AngularFireDatabase) { }
createTranslation(text: string): FirebaseObjectObservable<any> {
// create new translation, then return it as an object observable
const data = { 'english': text }
const key = this.db.list('/translations').push(data).key
return this.db.object(`translations/${key}`)
}
}
The Text Translate Component
Nothing too fancy here, just a `textarea` to accept the user input, then a function that will push it to the database and observe the newly created object.
import { Component } from '@angular/core';
import { TranslateService } from '../translate.service';
@Component({
selector: 'text-translate',
templateUrl: './text-translate.component.html',
styleUrls: ['./text-translate.component.scss']
})
export class TextTranslateComponent {
userText: string;
currentTranslation;
constructor(private translateSvc: TranslateService) { }
handleTranslation() {
this.currentTranslation = this.translateSvc.createTranslation(this.userText)
}
defaultMessage() {
if (!this.currentTranslation) return "Enter text and click run translation"
else return "Running translation in the cloud..."
}
}
In the template, we display the translation or say "translation running the cloud" (this could also be replaced with a loading spinner).
<h1>Translate Your Text</h1>
<textarea rows="8" cols="40" class="input" [(ngModel)]="userText">
</textarea>
<button (click)="handleTranslation()" class="button is-primary">Run Translation</button>
<h3>French</h3>
{{ (currentTranslation | async)?.fr || defaultMessage() }}
<h3>Spanish</h3>
{{ (currentTranslation | async)?.es || defaultMessage() }}
<h3>Arabic</h3>
{{ (currentTranslation | async)?.ar || defaultMessage() }}
Firebase Cloud Function using onWrite Handler
In order to use Google Translate, you need to active the API from your GCP console.
If this is your first cloud function, run `firebase init`, then select functions.
Go into the `functions/` directory and run `npm install` with the following packages.
{
"name": "functions",
"description": "Cloud Functions for Firebase",
"dependencies": {
"firebase-admin": "^4.1.2",
"firebase-functions": "^0.5",
"lodash": "^4.17.4",
"request-promise": "^2.0.0"
},
"private": true
}
The cloud function does all of the heavy lifting. This is a translation microservice - completely isolated from the Angular app. It has its own NodeJS environment and has zero impact on Angular's frontend performance. It is loosely based on the official cloud function sample.
The function is trigged during the `onWrite` event, which occurs when data is changed at the corresponding Firebase database reference point - in this case `/translations/{translationId}`
When triggered, it will take a snapshot of the data, then create a `Promise` for each language we want to translate. Each promise represents a request to the Google Translate API. Google Translate expects a `source` language (english), a `target` language to translate to, and `q` the text to translate.
After the response is received from Google Translate, the Firebase database is updated with the translated data for each language. The Firebase `admin` package is used to overwrite any database rules scoped to the user.
functions/index.js
var functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
const firebaseConfig = JSON.parse(process.env.FIREBASE_CONFIG);
const request = require('request-promise');
const _ = require('lodash');
// List of output languages.
const LANGUAGES = ['es', 'fr', 'ar'];
exports.translate = functions.database.ref('/translations/{translationId}').onWrite(event => {
const snapshot = event.data;
const promises = [];
_.each(LANGUAGES, (lang) => {
console.log(lang)
promises.push(createTranslationPromise(lang, snapshot));
})
return Promise.all(promises)
});
// URL to the Google Translate API.
function createTranslateUrl(lang, text) {
return `https://www.googleapis.com/language/translate/v2?key=${firebaseConfig.firebase.apiKey}&source=en&target=${lang}&q=${text}`;
}
function createTranslationPromise(lang, snapshot) {
const key = snapshot.key;
const text = snapshot.val().english;
let translation = {}
return request(createTranslateUrl(lang, text), {resolveWithFullResponse: true}).then(
response => {
if (response.statusCode === 200) {
const resData = JSON.parse(response.body).data;
translation[lang] = resData.translations[0].translatedText
return admin.database().ref(`/translations/${key}`)
.update(translation);
}
else throw response.body;
});
}