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:
- Python (version 3.7 or above)
- 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
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 providelocalhost: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.
Updated 5 days ago