Python and Flask

This tutorial will show you how to use the Lokalise Python SDK to create webhooks, listen to webhook events in third-party apps, and handle incoming notifications.

📘

In this tutorial you'll learn how to...

  • Work with Lokalise API tokens
  • Create webhooks with the Lokalise API
  • Listen to webhooks events
  • Handle incoming 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:

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

First of all, let's create a virtual environment in the app's root and install Flask, Gunicorn, and the Python SDK by running the following commands:

python3 -m venv venv
. venv/bin/activate
pip install Flask gunicorn python-lokalise-api

If you are on Windows, run:

py -3 -m venv venv
venv\Scripts\activate
pip install Flask gunicorn python-lokalise-api

Thecreate a Procfile in the project root with the following contents:

web: gunicorn wsgi:app

This file will be used by Heroku to properly start our app. If you are using another hosting platform, you might need to use a different approach.

Next, create a runtime.txt file with the Python version:

python-3.10.4

Create a file called wsgi.py:

from app.main import app
 
if __name__ == "__main__":
    app.run()

Now create a folder called app with a main.py file in it:

from flask import Flask, render_template, request, redirect, url_for, flash
 
app = Flask(__name__, template_folder='../templates')
app.secret_key = b'_5#y2L"F4Q8z\n\xec]/'
 
@app.route("/")
def home():
    return render_template('home.html')

Please note that the app.secret_key should be long enough and should be kept privately.

Let's add a template. Create a templates/home.html file:

<!doctype html>
<html lang="en">
<head>
	<title>Register a webhook</title>
</head>
<body>
	<h1>Register a webhook</h1>

  {% with messages = get_flashed_messages() %}
    {% if messages %}
      <ul class=flashes>
      {% for message in messages %}
        <li>{{ message }}</li>
      {% endfor %}
      </ul>
    {% endif %}
  {% endwith %}

  <form action="/webhooks" method="POST">
    <label for="project_id">Enter your Lokalise project ID:</label>
    <input type="text" name="project_id" id="project_id">
    <input type="submit" value="Register">
  </form>
</body>
</html>

Also we'll add a .gitignore file to the project's root:

venv/
.env
app/__pycache__

Now you can run:

pip freeze > requirements.txt
git init
git add .
git commit -m "first commit"

Then log in to Heroku, create a new app, and deploy and open it:

heroku login
heroku create YOUR_APP_NAME
git push heroku master
heroku open

Great job! Now that everything is ready, let's proceed to the main part of this tutorial.

Storing the Lokalise API token

In order to interact with the Lokalise API you'll need a special token. Alternatively, you can implement an OAuth 2 flow as explained in the corresponding article and act on the user's behalf.

The token should not be publicly exposed, therefore let's store it inside the .env file which was already added to .gitignore:

LOKALISE_API_TOKEN=123abc

We'll use the python-dotenv package to work with environment variables, therefore install it:

pip install python-dotenv

Open app/main.py, load all the necessary modules, and create a new method to create a Lokalise API client:

import os
import lokalise

from dotenv import load_dotenv
load_dotenv()

# ...

def __client():
    return lokalise.Client(os.getenv('LOKALISE_API_KEY'))

To add a new environment variable on Heroku, run:

heroku config:add LOKALISE_API_KEY=123abc

Registering webhooks

Now let's add a method to register a webhook on Lokalise. This is a common pattern when a third-party app registers required webhooks and then listens to incoming events.

We've already added a form to enter the project ID, thus add a handler method:

@app.route('/webhooks', methods=['POST'])
def webhooks():
    __client().create_webhook(request.form['project_id'], {
        "url": os.getenv('NOTIFY_URL'),
        "events": ["project.key.added"]
    })
    return redirect(url_for('home'))

In this example, we will be listening to a single event: project.key.added. However, there are many more events that you can subscribe to. You can find detailed information in our webhooks docs.

We are also using a NOTIFY_URL environment variable that should contain an app URL on Heroku with a /notify path:

NOTIFY_URL=https://YOUR_HEROKU_URL/notify

Make sure to add this environment variable to Heroku:

heroku config:add NOTIFY_URL=https://YOUR_HEROKU_URL/notify

📘

Your app must be publicly accessible

If you run your Flask app locally (python wsgi.py) and provide localhost:5000/notify as a notification URL, it won't be possible to register a Lokalise webhook. That's because Lokalise sends a special "ping" request to the provided URL and expects to receive a 2xx status code. If the URL is not accessible, the webhook won't be created.

Responding to notifications

Next up we should add a notify route and listen to incoming notifications. For example, each time a key is created I would like to add a key comment (which will be displayed in Lokalise project chat), and hide this key making it visible to project admins only. Of course, you can perform any other actions as needed:

@app.route('/notify', methods=['POST'])
def notify():
    data = request.get_json()
    if data != ['ping'] and data['event'] == 'project.key.added':
        __client().create_key_comments(data['project']['id'], data['key']['id'], [
            {
                "comment": "@Bob please double check this newly added key!"
            }
        ])

        __client().update_key(data['project']['id'], data['key']['id'], {
            "is_hidden": True
        })
    return "", 200

Webhook data is sent in JSON format. We make sure that the received notification is not a ping and has a proper event name. Then we just add a key comment and update the key using event's data.

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 out

Now everything is ready! Freeze requirements by running the pip freeze > requirements.txt command and push your code to Heroku. Run heroku open, enter your Lokalise project ID in the text input, and press "Register".

Then 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).

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:

This is 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:

@app.route('/notify', methods=['POST'])
def notify():
    data = request.get_json()
    if data['event'] == 'project.translations.updated':
        print(f"Project name: {data['project']['name']}")
        print(f"User name: {data['user']['full_name']}")
        
        for translation in data['translations']:
            print(f"Translation ID: {translation['id']}")
            print(f"Translation value: {translation['value']}")
            print(f"Language: {translation['language']['name']}")
            print(f"Key: {translation['key']['name']}")
    
    return 'ok', 200

So, 'translations' contain an array with all the translations data, including the language and key details.