Node
This tutorial will show you how to use the Lokalise Node SDK to create webhooks, listen to webhook events in third-party apps, and handle the incoming notifications.
In this tutorial you'll learn how to...
- Register webhooks with the Lokalise API
- Listen to incoming notifications
- React to webhooks notifications
You can find the source code on GitHub.
Prerequisites
This guide assumes that you have a Lokalise project (if not, learn how to create your Lokalise project here). Also, you'll need a read/write Lokalise API token; you can learn how to get one in the corresponding article.
If you want to follow this guide locally on your computer, you need to have the following software installed:
- Node.js (version 10 or above)
- Npm
- Code editor
- Heroku CLI
Finally, please note that in order to listen and respond to webhook events, your app must be publicly accessible. In this tutorial, we'll deploy our app to Heroku (see below).
What we are going to build
We are going to create a simple application that will allow users to register webhooks in their Lokalise projects. The app will also listen to incoming notifications generated by these webhooks and react to them by sending API requests.
Preparing a new app
About Node SDK v9
Please note that starting from version 9 our Node SDK is a pure ESM module. It does not provide a CommonJS export (
require
) anymore. Therefore you should either convert your project to ESM, use dynamic import, or use version 8 which we are fully supporting.
To get started, create a new Node app:
mkdir YOUR_PROJECT && cd YOUR_PROJECT && npm init -y
Now install the necessary dependencies:
npm install --save connect-flash cookie-session express hbs @lokalise/node-api
Add a start
script to package.json
and set the app type to module
:
"type": "module",
"scripts": {
"start": "node index.js"
}
Also add a Procfile
with the following content:
web: node index.js
This file will be used to boot your app on Heroku. If you are using a different hosting provider, you might need to take additional steps.
Project skeleton
Now let's prepare the necessary files. First of all, create index.js
in the project root:
import cookieSession from 'cookie-session'
import express from 'express'
import flash from 'connect-flash'
import { router } from "./config/routes.js"
import { setupAssets } from "./config/assets.js"
const app = express()
app.use(express.urlencoded({extended: true}))
app.use(express.json())
app.use(cookieSession({
name: 'session',
secret: 'my_super_secret',
maxAge: 24 * 60 * 60 * 1000
}))
app.use(flash())
app.use('/', router)
const port = process.env.PORT || 3000
app.listen(port, () => {
console.log(`Express web app on port ${port}`)
})
setupAssets(app)
This is the main file where we load all the necessary modules, set up session store, routes, etc.
Now create a config/assets.js
file:
import hbs from "hbs"
import path from "path"
const __dirname = path.resolve()
export function setupAssets(app) {
app.set('view engine', 'hbs')
hbs.registerPartials(path.join(__dirname, 'views/partials'))
}
We'll be using the Handlebars templating engine, but of course you can take advantage of any other solution.
The next step is adding the root route inside the config/routes.js
file:
import express from "express"
import { StaticPagesController } from "../controllers/staticPagesController.js"
export const router = express.Router()
router.get('/', (req, res) => {
StaticPagesController.index(req, res)
})
Create controllers/staticPagesController.js
file:
import { ApplicationController } from "./application_controller.js"
export class StaticPagesController extends ApplicationController {
static index(req, res) {
this.renderView(req, res, 'static_pages/index', {title: 'Register Lokalise webhook'})
}
}
This controller inherits from ApplicationController
, so let's also add it now under controllers/applicationController.js
:
export class ApplicationController {
static renderView(req, res, view, data = {}) {
data.flash_messages = req.flash('data')
res.render(view, data)
}
}
Next, let's take care of the main layout under views/layout.hbs
:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>{{title}}</title>
</head>
<body>
<div>
{{> header}}
{{{body}}}
</div>
</body>
</html>
Also add views/partials/header.hbs
:
<header>
<h1>{{title}}</h1>
</header>
{{#each flash_messages as |msg|}}
{{#each msg}}
<div>
{{this}}
</div>
{{/each}}
{{/each}}
Finally, add a views/static_pages/index.hbs
file:
<form method="POST" action="/webhooks">
<label for="project_id"></label>
<input type="text" name="project_id" id="project_id">
<input type="submit" value="Register">
</form>
We are simply displaying a form to register a new webhook in the specified Lokalise project.
Before wrapping up this section, let's initialize a new Git repository and create a new Heroku project:
git init
heroku create HEROKU_PROJECT_NAME
That's it, now let's proceed to the next section!
Registering webhooks
The next step is to actually register a webhook for the specified project ID. Therefore, create a new route:
// ...
import { WebhooksController } from "../controllers/webhooksController.js"
// ...
router.post('/webhooks', (req, res) => {
WebhooksController.create(req, res)
})
Now add a webhooksController.js
file to the controllers
directory:
import { ApplicationController } from "./application_controller.js"
export class WebhooksController extends ApplicationController {
static async create(req, res) {
try {
await this.lokaliseApi().webhooks().create(
{url: process.env.NOTIFY_URL, events: ['project.key.added']},
{project_id: req.body.project_id}
)
} catch(e) {
console.error(e)
}
req.flash('data', {success: `Registered webhook for ${req.body.project_id} project ID`})
res.redirect('/')
}
}
We use the lokaliseApi()
method which should be defined inside the ApplicationController
, as below:
import { LokaliseApi } from '@lokalise/node-api'
export class ApplicationController {
// ...
static lokaliseApi() {
return new LokaliseApi({ apiKey: process.env.LOKALISE_API_TOKEN})
}
}
Additionally, note that we are using two environment variables so let's add those to Heroku:
heroku config:add LOKALISE_API_TOKEN=123abc NOTIFY_URL=https://YOUR_HEROKU_PROJECT/notify
Your app must be publicly available!
You won't be able to register a new Lokalise webhook if your app is running locally. That's because Lokalise will try to send a special "ping" request to make sure the provided notification URL is actually available.
Listening to events
The last thing to do is listen to incoming Lokalise events, therefore create a new route:
// ...
import { NotificationsController } from "../controllers/notificationsController.js"
// ...
router.post('/notify', (req, res) => {
NotificationsController.create(req, res)
})
Create a new notificationsController.js
file:
import { ApplicationController } from "./application_controller.js"
export class NotificationsController extends ApplicationController {
static async create(req, res) {
const payload = req.body
if (Array.isArray(payload)) {
if (payload[0] === "ping") {
res.sendStatus(200)
return
} else {
res.sendStatus(400)
return
}
}
if(payload.event === 'project.key.added') {
await this.lokaliseApi().comments().create([
{ comment: "@Bob please review this new key" }
],
{ project_id: payload.project.id, key_id: payload.key.id }
)
await this.lokaliseApi().keys().update(payload.key.id, {
"is_hidden": true
}, { project_id: payload.project.id })
}
res.sendStatus(200)
}
}
First of all, we are making sure that the received request is not "ping" (otherwise do nothing). If the request event is project.key.added
then we are adding a new comment for this key and hiding it from non-admins.
You can introduce an additional level of security by checking the request headers. Specifically, each reqest with contain a special "secret" (that you can customize when creating a webhook) as well as the Lokalise project ID and the webhook ID. Please find more information in the corresponding document.
A notice on notification handlers
Please note that your notification handler routes must respond to HTTP POST and return 2xx status codes. Otherwise, Lokalise will try to re-send notifications multiple times before giving up.
Testing it
Now everything is ready! Deploy your app on Heroku:
git commit -m "Initial"
git push heroku master
heroku open
Enter your Lokalise project ID in the text input and press "Register".
Now open Lokalise, proceed to your project, and click "Apps" in the top menu. Find the "Webhooks" app, click on it, and then press "Manage". You'll see that a new webhook was registered for you:
Note the "X-Secret header" hidden value. You can use this value inside your app and compare it with the one sent to the notify
route for extra protection (thus filtering out malicious requests).
Now return to the Lokalise project editor and create a new translation key. Reload the page and make sure the key was hidden:
Click on the "Comments" button (the first button on the screen above) and make sure the comment is displayed properly:
That's it, great job!
Webhooks and bulk actions
Webhooks support Lokalise bulk actions as well as the find/replace operation. You can find more info the Webhooks article but let's see how to listen to such events.
First of all, it's important to enable the proper events in the webhooks configuration. For instance, if you'd like to monitor find/replace operation as well as applying pseudolocalization using bulk actions, you should tick the translation updated event:
Please keep in mind that as long as the bulk actions may involve multiple keys, the event payload will contain data from 1 and up to 300 keys. If more than 300 keys were involved in the operation, you'll receive multiple events.
Also note that in case of bulk actions, the event name will be pluralized: project.translations.updated
, not project.translation.updated
. For example, if you would like to display information about all the updated translations as well as project and user name, you can use the following code snippet:
static async create(req, res) {
const payload = req.body
if (Array.isArray(payload)) {
if (payload[0] === "ping") {
res.sendStatus(200)
return
} else {
res.sendStatus(400)
return
}
}
if(payload.event === 'project.translations.updated') {
console.log(`Project name: #{payload.project.name`)
console.log(`User name: #{payload.user.full_name`)
payload.translations.forEach((translation) => {
console.log(`Translation ID: #{translation.id`)
console.log(`Translation value: #{translation.value`)
console.log(`Language: #{translation.language.name`)
console.log(`Key: #{translation.key.name`)
}
}
res.sendStatus(200)
}
So, payload.translations
contain an array with all the translations data, including the language and key details.
Updated about 2 months ago