PHP and Symfony

This tutorial will show you how to manage translation files with the Lokalise API and how to implement an OAuth 2 flow.

📘

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

  • Work with Lokalise API tokens
  • Upload translation files with the Lokalise API
  • Download translation files with the Lokalise API
  • Implement an OAuth 2 flow
  • List customer projects
  • Act on the customer's behalf and upload/download translation files

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). In this tutorial, we are going to upload English translations. Therefore, make sure that your project has the English language added with the “en” ISO code.

If you want to follow this guide locally on your computer, you need to have the following software installed:

What we are going to build

In the first part of this tutorial, we are going to create a simple Symfony/Lokalise integration to showcase how easy it is to transfer the translations between your app and Lokalise. It's the "fast track to exchanging translations".

The second part will cover building a simple Symfony app that uses the Lokalise PHP SDK to accomplish this. It will involve writing a bit of PHP code, but will give us much greater control over the translation exchange.

In the third part, we will implement an OAuth 2 flow and act on the user's behalf, instead of using a generic API token. This will improve the audit trail left behind when exchanging translations, and will also take into account the user's specific permissions.

📘

Reading all parts one by one is not required

You can simply jump to the section that you would like to study.

Preparing a new Symfony app

Create a new Symfony app by running the following command:

symfony new symfony-lokalise-app --webapp

This will create the skeleton app in the symfony-lokalise-app folder for you.

Now go into that folder and set the Symfony app to use the translations:

cd symfony-lokalise-app
composer require symfony/translation

Getting the API token and project ID

You should obtain an existing API token or generate a new one. Learn how to get a Lokalise API token.

Also, while you are on Lokalise, get the project ID and write it down. Learn how to get the Lokalise project ID.

Using the Symfony/Lokalise integration

In this section, we will leverage the Lokalise translation provider, so let's go ahead and add that:

composer require symfony/lokalise-translation-provider

Adding the initial translations

The Lokalise translation provider package is a little opinionated about the file format it wants to use. We can either switch to Lokalise and create our translations there, or you can copy-paste the following snippet of code into your translations/messages.en.xlf file – this adds four translation keys already translated into English for you:

<?xml version="1.0" encoding="utf-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
  <file source-language="en" target-language="en" datatype="plaintext" original="file.ext">
    <header>
      <tool tool-id="symfony" tool-name="Symfony"/>
    </header>
    <body>
      <trans-unit id="lDKh8IR" resname="download_btn">
        <source>download_btn</source>
        <target>Download!</target>
      </trans-unit>
      <trans-unit id="GBX8E_C" resname="download_title">
        <source>download_title</source>
        <target>Download translation file</target>
      </trans-unit>
      <trans-unit id="W2gyj8w" resname="upload_btn">
        <source>upload_btn</source>
        <target>Upload!</target>
      </trans-unit>
      <trans-unit id="ZwPonJx" resname="upload_title">
        <source>upload_title</source>
        <target>Upload translation file</target>
      </trans-unit>
    </body>
  </file>
</xliff>

Set up the app to use Lokalise as a translation provider

Make sure Lokalise is enabled as a translation provider. Your config/packages/translation.yaml config file should look something like this:

framework:
    default_locale: en
    translator:
        default_path: '%kernel.project_dir%/translations'
        fallbacks:
            - en
        providers:
            lokalise:
                dsn: '%env(LOKALISE_DSN)%'
                locales: ['en', 'lv']

Two things to note here:

  • We've also added a locale for Latvian. This is just for demonstration purposes – feel free to skip adding new languages, if you're so inclined.
  • We've set the translator DSN to an environment variable of LOKALISE_DSN. Make sure to add that variable to the appropriate .env file in the format of lokalise://<PROJECT_ID>:<API_KEY>@default.

❗️

Never publicly expose your API key!

Specifically, don’t forget to add the .env file to .gitignore.

Uploading and downloading the translations

Now we're set to start uploading and downloading the translations.

Upload the local translations to Lokalise, like so:

symfony console translation:push lokalise --domains messages --force

This tells Symfony to push the translations from the messages domains for the lokalise provider and overwrite any existing translations that you also have locally.

If you don't want to overwrite existing translations, simply omit the --force option. Please note, though, that this will attempt to download the existing translations for comparison from the Lokalise server beforehand, which can result in an error if there are no translations for the web platform yet.

After uploading the translations, head over to Lokalise to make sure they all made it there in one piece. Edit some translations and then download them back to your project, using the following command:

symfony console translation:pull lokalise --domains messages --force

You should see your translations back in one piece with the changes included. If you added some more languages, you should see those files appearing in the translation folder as well.

Using the Lokalise PHP SDK

This is nice and all, but the XLF format might be a little bit heavy for simple apps, as there's a lot of additional metadata in the translation files. This makes manually adding new translations a bit of an issue, as you're developing the app locally. It would be quite useful to use a different format for faster manual addition of new keys as we go. As mentioned above, the Symfony/Lokalise integration is somewhat opinionated, so we'll switch to the Lokalise PHP SDK.

Cleaning up

If you completed the first part using the integration, let's do some cleanup first:

  • Remove the integration with the following command: composer remove symfony/lokalise-translation-provider
  • Remove the translation files in translations/
  • Remove all the added translation keys in the project on Lokalise
  • Clean up the config/packages/translation.yaml file to look like this:
framework:
    default_locale: en
    translator:
        default_path: '%kernel.project_dir%/translations'
        fallbacks:
            - en

Setting up the controllers

We'll set up two controllers: one for the homepage and the other for managing the translations. Let's generate these now by running:

symfony console make:controller HomeController
symfony console make:controller TranslationsController

Open up the src/Controller/HomeController.php file and make sure the route is set to capture requests to the site root:

    #[Route('/', name: 'app_home')]
    public function index(): Response
    {
        return $this->render('home/index.html.twig');
    }

Next, open the src/Controller/TranslationsController.php file and set up the upload and download actions:

    #[Route('/translations/upload', name: 'translations_upload', methods: ['POST'])]
    public function upload(): Response
    {
        return $this->redirect($this->generateUrl('app_home'));
    }

    #[Route('/translations/download', name: 'translations_download', methods: ['POST'])]
    public function download(): Response
    {
        return $this->redirect($this->generateUrl('app_home'));
    }

Note that we're setting both of these actions only to respond to POST requests. Initially, we're making them redirect back home.

Setting up the homepage template

Open up the templates/home/index.html.twig file and make it something simple that allows you to upload and download the translations, while also showcasing the translations themselves:

<h1>Manage the translations here</h1>

<h2>{{'download_title'|trans}}</h2>
<form action="{{ path('translations_download')}}" method="post"><input type="submit" value="{{'download_btn'|trans }}" /></form>

<h2>{{'upload_title'|trans}}</h2>
<form action="{{ path('translations_upload')}}" method="post"><input type="submit" value="{{'upload_btn'|trans }}" /></form>

Setting up the Lokalise PHP SDK

Add the SDK to your app by running the following command:
composer require lokalise/php-lokalise-api

Now is as good a time as any to store your credentials in the .env file. From now on, we'll assume that they're stored as follows:

LOKALISE_PROJECT=123.abc
LOKALISE_TOKEN=123def456

Make sure to add them to your service configuration in config/services.yaml:

parameters:
    lokalise.api_token: '%env(LOKALISE_TOKEN)%'
    lokalise.project_id: '%env(LOKALISE_PROJECT)%'

Adding initial translations

Open the translations/messages.en.yaml file and add the initial translations, as below:

upload_title: Upload translation file
upload_btn: Upload!
download_title: Download translation file
download_btn: Download!

Start up the Symfony app

Simply execute symfony server:start in the root of the project to start the built-in server. Afterward, the app should show up on [<http://localhost:8000>](http://localhost:8000`).

Uploading translation files to your Lokalise project

Let's update the upload action so it actually uploads any translation files it finds:

   
    public function upload(): Response
    {
        // Get all the message translation files
        $finder = new \Symfony\Component\Finder\Finder() 
        $files = $finder->files()
            ->in($this->getParameter('kernel.project_dir') . '/translations')
            ->name('messages.*.yaml');

        // If any files were found
        if ($files->hasResults()) {
            $client = new \Lokalise\LokaliseApiClient($this->getParameter('lokalise.api_token'));

            foreach ($files as $file) {
                $contents = $file->getContents();
                $fileName = $file->getFilename();
                $languageIso = explode('.', $fileName)[1];

                // Upload the file
                $client->files->upload($this->getParameter('lokalise.project_id'), [
                    'data' => base64_encode($contents),
                    'filename' => $fileName,
                    'lang_iso' => $languageIso
                ]);
            }
        }

        // Redirect back home.
        return $this->redirect($this->generateUrl('app_home'));
    }

Now, visit the homepage (http://localhost:8000) and hit "Upload!". It'll take a few seconds to process everything, and then it will return to the homepage.

Visit your Lokalise project online to confirm that the translations have been uploaded.

Downloading translation files from your Lokalise project

Downloading takes a little extra effort, because the download will be a .zip file. Most PHP installations have the .zip extension enabled. If you do not, this is a good time to install it.

Once that is taken care of, everything else is a breeze!

    public function download(): Response
    {
        $client = new \Lokalise\LokaliseApiClient($this->getParameter('lokalise.api_token'));
        $response = $client->files->download($this->getParameter('lokalise.project_id'), [
            'format' => 'yaml',
            'yaml_include_root' => false,
            'original_filenames' => true,
            'directory_prefix' => '',
            'indentation' => '4sp',
        ]);

        $url = $response->getContent()['bundle_url'] ?? null;

        $tempFile = tempnam(sys_get_temp_dir(), 'lokalise');
        copy($url, $tempFile);
        file_put_contents($tempFile, file_get_contents($url));

        $zip = new \ZipArchive();

        if ($zip->open($tempFile)) {
            $zip->extractTo($this->getParameter('kernel.project_dir') . '/translations');
            $zip->close();
        }

        unlink($tempFile);

        return $this->redirect($this->generateUrl('app_home'));
    }

Now, hitting that "Download!" button will download the translation files from Lokalise.

Note: if you are updating existing translations, you might need to clear the cache by running symfony console cache:clear.

🚧

Your existing translations will be overwritten!

Please remember that the current implementation overwrites any data inside the translation files. If you don't want this to happen, save your new translations to a file with another name.

Working with OAuth 2 flow

Yet again, the Lokalise PHP SDK does not currently support OAuth 2, so we'll resort to some manual requests using Guzzle. Let's get ready for that.

Cleaning up

To clean up after the PHP SDK part of this tutorial you should:

  • Remove the SDK with the following command: composer remove lokalise/php-lokalise-api
  • Remove the src/Controller/TranslationsController.php file as we won't need it
  • Remove the templates/translations folder as we won't need it

Keep the translations as we're able to reuse them. Or, if you skipped straight to here without doing the PHP SDK part, perform this step only from the PHP SDK section: Adding initial translations.

Registering an OAuth 2 app

To get started, please reach out to our tech support and ask them to register an OAuth 2 app for you (if you don't have one registered already). You will be presented with the Lokalise client ID and client secret that we will be using in the next steps.

You can store these keys in the .env file:

LOKALISE_OAUTH2_CLIENT_ID=your_id
LOKALISE_OAUTH2_CLIENT_SECRET=your_secret

Make sure to add them to your service configuration in config/services.yaml. While we're editing that file, let's add the OAuth 2 endpoint and requested scopes as well.

parameters:
    lokalise.oauth.client_id: '%env(LOKALISE_OAUTH2_CLIENT_ID)%'
    lokalise.oauth.client_secret: '%env(LOKALISE_OAUTH2_CLIENT_SECRET)%'
    lokalise.oauth.endpoint: 'https://app.lokalise.com/oauth2/'
    lokalise.oauth.scopes:
        - 'read_projects'
        - 'read_files'
        - 'write_files'

Creating the initial OAuth controller

Create the controller with the following command:

symfony console make:controller OAuthController

Let's set up the OAuth home action first – open the src/Controller/OAuthController.php file and set up the index action:

    #[Route('/oauth', name: 'oauth_home')]
    public function index(SessionInterface $session): Response
    {
        return $this->render('o_auth/index.html.twig',[
            'lokaliseToken' => $session->get('lokalise.oauth.token', null),
            'lokaliseRefreshToken' => $session->get('lokalise.oauth.refresh_token', null),
            'lokaliseProjectId' => $session->get('l okalise.oauth.project_id', null),
        ]);
    }

This will look at the current session and render a simple template using the OAuth session variables that we'll store a bit later.

Set up the template to look like this:

{% if lokaliseToken is not null %}
  <h2>OAuth 2 session status</h2>
  <ul>
      <li>Your OAuth 2 token: {{ lokaliseToken }}</li>
      <li>Your OAuth 2 refresh token: {{ lokaliseRefreshToken }}</li>
      <li>Your OAuth 2 project id: {{ lokaliseProjectId }}</li>
      <li><a href="{{ path('oauth_logout')}}">Log out</a></li>
  </ul>
{% else %}
    <a href="{{ path('oauth_authenticate')}}">Log in via Lokalise</a>
{% endif %}

If you try to go to <http://localhost:8000/oauth> now, you should see an error, so let's set up the authentication flow and ensure that the oauth_authenticate route exists.

Implementing the OAuth 2 flow

Once again open the src/Controller/OAuthController.php file so we can implement the authentication flow:

    #[Route('/oauth/authenticate', name: 'oauth_authenticate')]
    public function authenticate(\Symfony\Component\HttpFoundation\Session\SessionInterface $session)
    {
        // Generate a new state and store it for lookup later
        $state = uniqid('lok.oauth', true);
        $session->set('lokalise.oauth.state', $state);

        $redirectUri = $this->generateUrl('oauth_callback');

        // Build the redirect URL and redirect the user to that
        $params = [
            'client_id' => $this->getParameter('lokalise.oauth.client_id'),
            'scope' => implode(' ', $this->getParameter('lokalise.oauth.scopes')),
            'redirect_uri' => $this->generateUrl('oauth_callback', [], \Symfony\Component\Routing\Generator\UrlGeneratorInterface::ABSOLUTE_URL),
            'state' => $state,
        ];

        $authUrl = $this->getParameter('lokalise.oauth.endpoint') . 'auth?' . http_build_query($params);

        return $this->redirect($authUrl);
    }

There's a bit to unpack here, but there are two main steps here:

  • We generate a unique state code, which we can use to prevent CSRF attacks, and store that in the user session.
  • Then, we generate the URL which includes the parameters to set up authentication as well as the redirect URL where the user will be sent.

Now, let's set up redirect handling by adding the following action to our controllers:

      #[Route('/oauth/callback', name: 'oauth_callback')]
    public function callback(\Symfony\Component\HttpFoundation\Session\SessionInterface $session, Symfony\Component\HttpFoundation\Request $request, Symfony\Contracts\HttpClient\HttpClientInterface $client): Response
    {
        $code = $request->get('code', null);

        // Check the integrity of the request
        if (empty($code) || $session->get('lokalise.oauth.state', 'state1') !== $request->get('state', 'state2') ) {
            return $this->redirect($this->generateUrl('oauth_home'));
        }

        // Get the tokens based on the code returned
        $params = [
            'grant_type' => 'authorization_code',
            'code' => $code,
            'client_id' => $this->getParameter('lokalise.oauth.client_id'),
            'client_secret' => $this->getParameter('lokalise.oauth.client_secret'),
        ];

        $response = $client->request('POST', $this->getParameter('lokalise.oauth.endpoint') . 'token', [
            'json' => $params
        ]);

        // Store the token data in session
        $responseData = $response->toArray();
        $session->set('lokalise.oauth.token', $responseData['access_token'] ?? null);
        $session->set('lokalise.oauth.refresh_token', $responseData['refresh_token'] ?? null);
        $session->set('lokalise.oauth.project_id', null);

        return $this->redirect($this->generateUrl('oauth_home'));
    }

This action initially checks whether a one-time authorization code was present in the response and compares the stored OAuth state key with the returned one. This helps us make sure that we're actually talking to the same entity that we sent our request to. When those checks have passed, we create the request parameter for the subsequent request, which will actually return the tokens for us to use.

Finally, let's add another action to the same controller which will allow the user to log out. All that requires is simply nullifying some session variables:

    #[Route('/oauth/logout', name: 'oauth_logout')]
    public function logout(\Symfony\Component\HttpFoundation\Session\SessionInterface $session): Response
    {
        $session->set('lokalise.oauth.token', null);
        $session->set('lokalise.oauth.refresh_token', null);
        $session->set('lokalise.oauth.project_id', null);

        return $this->redirect($this->generateUrl('oauth_home'));
    }

After this, you should be able to authenticate using OAuth 2 successfully – try it by visiting <http://localhost:8000/oauth>.

Setting up for authenticated actions

There will be some shared functionality between all the authenticated actions. In a real-world application you would probably build a service for that or have some helper component or class. However, in this article, we're aiming for easy to write and easy to follow. Sometimes you have to sacrifice the best practices to do this, so just keep in mind that this demonstrates a general application, not the best way of structuring your application.

Before we continue with user actions that only authenticated users can perform, we have to set up some control mechanisms. While in Symfony the common way would be to use events, for simplicity we'll go with a less flexible approach and implement a base controller for such actions. While we're at it, we'll make sure we have an HTTP client at our fingertips that's already primed for authenticated requests.

Create the src/Controller/OAuthAuthenticatedController.php file manually and add some simple code to allow us to check for authentication:

<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;

abstract class OAuthAuthenticatedController extends AbstractController
{
    protected SessionInterface $session;
    protected HttpClientInterface $client;

    public function __construct(RequestStack $requestStack, HttpClientInterface $client) {
        $this->session = $requestStack->getSession();
        $this->client = $client->withOptions([
            'auth_bearer' => $this->session->get('lokalise.oauth.token', 'no-token')
        ]);
    }

    protected function checkToken(): bool
    {
        return !empty($this->session->get('lokalise.oauth.token', null));
    }

    protected function checkProjectId(): bool
    {
        return !empty($this->session->get('lokalise.oauth.project_id', null));
    }
}

Choosing a project to work with

The next step is allowing the customer to choose a project that they'd like to interact with. So, let's add another controller:

symfony console make:controller ProjectsController

Inside that controller we'll make sure to fetch a list of projects. First, let's ensure that it extends the right controller, though:

class ProjectsController extends OAuthAuthenticatedController

Next, be sure to add two actions to the controller that will request the project list and let you select a project, too:

    #[Route('/projects', name: 'list_projects')]
    public function index(): Response
    {
        if (!$this->checkToken()) {
            return $this->redirect($this->generateUrl('oauth_home'));
        }

        $projects = $this->client->request('GET', $this->getParameter('lokalise.api.endpoint') . 'projects', [
                'query' => [
                    'limit' => 50
                ]
            ])?->toArray() ?? [];

        $projects = array_map(fn($data) => ['id' => $data['project_id'], 'name' => $data['name']], $projects['projects']);

        return $this->render('projects/index.html.twig', [
            'projects' => $projects,
        ]);
    }

    #[Route('/select_project/{projectId}', name: 'oauth_select_project')]
    public function select(\Symfony\Component\HttpFoundation\Session\SessionInterface $session, string $projectId): Response
    {
        if ($this->checkToken()) {
            $session->set('lokalise.oauth.project_id', $projectId);
        }

        return $this->redirect($this->generateUrl('oauth_home'));
    }

Also, make sure to add some simple markup to the templates/projects/index.twig.html to make sure the user is able to select a project:

<h2>Select a project</h2>
<ul>
  {% for project in projects %}
    <li><a href="{{ path('oauth_select_project', {projectId: project.id}) }}">{{ project.name }}</a></li>
  {% endfor %}
</ul>

Also, update the templates/o_auth/index.html.twig template so it looks like this:

{% if lokaliseToken is not null %}
    <h2>OAuth 2 session status</h2>
    <ul>
        <li>Your OAuth 2 token: {{ lokaliseToken }}</li>
        <li>Your OAuth 2 refresh token: {{ lokaliseRefreshToken }}</li>
        <li>Your OAuth 2 project id: {{ lokaliseProjectId }}</li>
        <li><a href="{{ path('oauth_logout')}}">Log out</a></li>
    </ul>
    <hr>
    <ul>
        <li><a href="{{ path('list_projects') }}">Select a project</a></li>
    </ul>

{% else %}
    <a href="{{ path('oauth_authenticate')}}">Log in via Lokalise</a>
{% endif %}

Uploading and downloading translations

Lastly, let's set up translation file transfer on the user's behalf.

This will look very similar to using the Lokalise PHP SDK, but that's expected – we are, after all, doing almost exactly the same thing, only authenticating in a different manner.

Set up a new controller using the following command:

symfony console make:controller OAuthTranslationsController

Then, update it to appear like this:

<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Finder\Finder;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Translation\Reader\TranslationReaderInterface;
use Symfony\Contracts\Translation\TranslatorInterface;

class OAuthTranslationsController extends OAuthAuthenticatedController
{
    #[Route('/oauth/upload', name: 'oauth_upload')]
    public function upload(SessionInterface $session): Response
    {
        if (!$this->checkToken() || !$this->checkProjectId()) {
            return $this->redirect($this->generateUrl('oauth_home'));
        }

        // Get all the message translation files
        $finder = new Finder();
        $files = $finder->files()
            ->in($this->getParameter('kernel.project_dir') . '/translations')
            ->name('messages.*.yaml');

        // If any files were found
        if ($files->hasResults()) {
            foreach ($files as $file) {
                $projectId = $session->get('lokalise.oauth.project_id');

                $contents = $file->getContents();
                $fileName = $file->getFilename();
                $languageIso = explode('.', $fileName)[1];

                $this->client->request('POST', $this->getParameter('lokalise.api.endpoint') . 'projects/' . $projectId . '/files/upload', [
                    'json' => [
                        'data' => base64_encode($contents),
                        'filename' => $fileName,
                        'lang_iso' => $languageIso
                    ]
                ]);
            }
        }

        // Redirect back home.
        return $this->redirect($this->generateUrl('oauth_home'));
    }

    #[Route('/oauth/download', name: 'oauth_download')]
    public function download(SessionInterface $session): Response
    {
        if (!$this->checkToken() || !$this->checkProjectId()) {
            return $this->redirect($this->generateUrl('oauth_home'));
        }

        $projectId = $session->get('lokalise.oauth.project_id');

        $response = $this->client->request('POST', $this->getParameter('lokalise.api.endpoint') . 'projects/' . $projectId . '/files/download', [
            'json' => [
                'format' => 'yaml',
                'yaml_include_root' => false,
                'original_filenames' => true,
                'directory_prefix' => '',
                'indentation' => '4sp',
            ]
        ]);

        $url = $response->toArray()['bundle_url'] ?? null;

        $tempFile = tempnam(sys_get_temp_dir(), 'lokalise');
        copy($url, $tempFile);
        file_put_contents($tempFile, file_get_contents($url));

        $zip = new \ZipArchive();

        if ($zip->open($tempFile)) {
            $zip->extractTo($this->getParameter('kernel.project_dir') . '/translations');
            $zip->close();
        }

        unlink($tempFile);

        return $this->redirect($this->generateUrl('oauth_home'));
    }
}

We are setting up the upload and download actions, which uses Symfony's HttpClient, instead of the PHP SDK. This is done because OAuth 2 requests require an authentication token to be sent as a header, which is not yet supported by the PHP SDK.

After this step, you'll also need to update the templates/o_auth/index.html.twig template to:

{% if lokaliseToken is not null %}
    <h2>OAuth 2 session status</h2>
    <ul>
        <li>Your OAuth 2 token: {{ lokaliseToken }}</li>
        <li>Your OAuth 2 refresh token: {{ lokaliseRefreshToken }}</li>
        <li>Your OAuth 2 project id: {{ lokaliseProjectId }}</li>
        <li><a href="{{ path('oauth_logout')}}">Log out</a></li>
    </ul>
    <hr>
    <ul>
        <li><a href="{{ path('list_projects') }}">Select a project</a></li>
        <li><a href="{{ path('oauth_upload')}}">{{'upload_btn'|trans }}</a></li>
        <li><a href="{{ path('oauth_download')}}">{{'download_btn'|trans }}  </a></li>
    </ul>

{% else %}
    <a href="{{ path('oauth_authenticate')}}">Log in via Lokalise</a>
{% endif %}

And that should be it! Going to <http://localhost:8000/oauth> should allow you to upload and download translation files easily!