Illustration of Lokalise APIv2 in practice

Lokalise APIv2 in practice

In one of my previous articles I was covering Lokalise APIv2 basics, explaining its key features, and how to get started with it. Today we are going to work a bit more with the API using Node, Ruby, and Python SDKs. We are going to cover some real-world scenarios and discuss different code samples. By the end of this article you’ll be able to put your knowledge to work in real projects.

    Getting started

    As explained in the introductory post, you’ll need to complete the following steps in order to get started with Lokalise API:

    • Register a https://app.lokalise.com/signup.
    • Proceed to your profile by clicking on the avatar in the bottom left corner and choosing Profile settings.
    • Open the API tokens section and generate a new read/write token. Alternatively, you can obtain an access token via our OAuth 2 flow and act on other user’s behalf.

    In this article I’ll be using the following:

    • Node SDK (with Node v20)
    • Ruby SDK (with Ruby 3.2)
    • Python SDK (with Python 3.12)

    Of course, we provide official SDKs for other programming languages, namely PHP, Go, Python, and Elixir.

    Next, let’s create three folders to host our sample projects: ts, ruby, and python.

    You can find the source code for this article at github.com/bodrovis-learning/Lokalise-APIv2-Samples. You can find even more samples in our DevHub.

    Ruby

    Here’s the contents of the ruby folder:

    • Gemfile
    • .env
    • src
    • src/main.rb
    • src/i18n
    • src/i18n/en.yaml

    Put the following code inside your Gemfile:

    source 'https://rubygems.org'
    
    gem 'dotenv', '~> 2.8'
    gem 'lokalise_manager', '~> 5.0'

    You might be wondering why we’re also installing the lokalise_manager gem. Well, that’s because this solution simplifies the file uploading and downloading process, as you’ll see next. Dotenv will be utilized to load environment variables.

    Run bundle install to install all the necessary dependencies.

    Next, open the .env file and place your API token inside:

    API_KEY=123456abc

    Now, let’s take care of the main.rb file:

    require 'dotenv/load'
    require 'lokalise_manager'
    require 'ruby_lokalise_api'

    dotenv/load will automatically fetch all environment variables from the .env file so you’ll be able to access them easily: ENV['API_TOKEN'].

    Finally, here’s the contents of the en.yaml file:

    en:
      demo: This is just a demo...
      welcome: Welcome to the app!

    Great, our setup for Ruby is finished!

    You may also be interested in checking out this video tutorial which shows more complex examples with Ruby and Rails:

    Node and TypeScript

    Our ts folder is going to have a very similar structure:

    • src
    • src/api_demo.ts
    • src/i18n
    • src/i18n/en.json
    • .env
    • package.json
    • tsconfig.json

    I really recommend using the boilerplate code I’ve prepared for this article to help you get started.

    Run npm install to install all the necessary dependencies.

    Put your API key inside the .env file:

    API_KEY=7890zyxf

    Now, edit the api_demo.ts file:

    import 'dotenv/config';
    import { LokaliseApi } from "@lokalise/node-api";
    
    async function main() {
    
    }
    
    main()
      .then(() => process.exit(0))
      .catch((error) => {
        console.error(error)
        process.exit(1)
      });

    Finally, provide some sample translations inside the en.json file:

    {
      "welcome": "Welcome to the app!",
      "demo": "This is just a demo..."
    }

    Python

    Our python folder is going to have the following structure:

    • src
    • src/api_demo.py
    • src/i18n
    • src/i18n/en.json
    • .env
    • Pipfile

    I’ll be using pipenv in this project so let’s install it:

    pip install --user pipenv

    Now populate the Pipfile with the following contents:

    [[source]]
    url = "https://pypi.org/simple"
    verify_ssl = true
    name = "pypi"
    
    [packages]
    requests = "~=2.31"
    python-lokalise-api = "~=2.1"
    python-dotenv = "~=1.0"
    
    [requires]
    python_version = "3.12"

    We are going to use requests to, well, send some HTTP requests, Python SDK, and python-dotenv to load environment variables.

    Install everything by running:

    pipenv install

    Next, edit the api_demo.py file:

    import lokalise
    import requests
    import io
    import zipfile
    import os
    import base64
    import time
    from dotenv import load_dotenv
    load_dotenv()

    Finally, let’s add some sample translations to src/i18n/en.json file:

    {
      "welcome": "Welcome to the app!",
      "demo": "This is just a demo..."
    }

    Nice job!

    Let’s create a new project

    In Ruby

    To create a new translation project in Lokalise, paste the following Ruby code to the main.rb file:

    # ...
    
    @client = RubyLokaliseApi.client ENV.fetch('API_KEY', nil)
    
    puts 'Creating project...'
    
    project = @client.create_project name: 'Ruby Sample Project',
                                     description: 'My Ruby project',
                                     languages: [
                                       {
                                         lang_iso: 'en'
                                       },
                                       {
                                         lang_iso: 'fr'
                                       }
                                     ],
                                     base_lang_iso: 'en'
    
    project_id = project.project_id
    
    puts project_id
    puts project.name
    puts project.description

    So, Lokalise.client creates a new API client to send API requests. Please note that if you are using a token obtained via OAuth 2 flow, you’ll have to instantiate the client in a different way:

    @client = RubyLokaliseApi.oauth_client 'YOUR_OAUTH_TOKEN_HERE'

    create_project is the method you’ll need to call. It accepts a hash with project parameters: please note that these parameters have names identical to the ones listed in the API docs. In the code sample above we are specifying a project name, description, provide an array of languages (English and French), and set the base project language to English.

    The project variable will contain a Ruby object representing this newly created project (we call such objects as “models”). project responds to methods named after the project attributes, for example project_id, name, team_id, settings, and others. In the example above we store project ID in a separate variable as we will need it later.

    In TypeScript

    Let’s see how to create a new project in TypeScript. Modify your your api_demo.ts file in the following way:

    // ... your imports here ...
    
    async function main() {
      const lokaliseApi = new LokaliseApi({ apiKey: process.env.API_KEY });
    
      console.log("Creating project...");
    
      const project = await lokaliseApi.projects().create({
        name: "Node.js Sample Project",
        description: "Here's my Node.js project",
        languages: [
          {
              "lang_iso": "en"
          },
          {
              "lang_iso": "fr"
          }
        ],
        "base_lang_iso": "en"
      });
    
      const projectId = project.project_id;
    
      console.log(projectId);
      console.log(project.name);
    }
    
    // ...

    A few things to note:

    • new LokaliseApi instantiates a new client object. If you are using a token obtained via OAuth 2 flow, the client has to be created in a different way: new LokaliseApiOAuth({ apiKey: 'YOUR_OAUTH_TOKEN' }).
    • process.env.API_KEY fetches an environment variable containing your API token.
    • The projects().create() method adds a new Lokalise project. It accepts an object with project attributes, and these attributes are named exactly the same as the ones listed in the API docs. Our project will host two languages: English (set as the base language) and French.
    • The project constant will contain an object representing the newly created project. You can find a full list of project attributes in the API docs.

    In Python

    Now let’s try creating a new translation project in Python. To achieve that, paste the following code to the api_demo.py file:

    client = lokalise.Client(os.getenv('API_KEY'))
    
    print("Creating a new project...")
    
    project = client.create_project({
        "name": "Python Sample Project",
        "description": "Here's my Python project",
        "languages": [
            {"lang_iso": "en"},
            {"lang_iso": "fr"}
        ],
        "base_lang_iso": "en"
    })
    
    project_id = project.project_id
    • lokalise.Client instantiates a new API client with an API token. If you are using an access token obtained via OAuth 2 flow, the client has to be created in a different way: lokalise.OAuthClient('YOUR_ACCESS_TOKEN').
    • os.getenv() fetches an environment variables with your API token.
    • The create_project() method creates a new Lokalise project with the given params. It accepts an object with project attributes, and these attributes are named exactly the same as the ones listed in the API docs. Our project will host two languages: English (set as the base language) and French.
    • The project variable will contain an object representing the newly created project. You can find a full list of project attributes in the API docs.

    To run this script in a virtual environment created by pipenv, use the following command:

    pipenv run python src\api_demo.py

    Invite contributors

    Okay, so we have just created a new project, and it’s time to invite some contributors!

    In Ruby

    To add project contributors, use the create_contributors method. It accepts a project ID and a hash or array of hashes (if you are creating multiple contributors in one go). The hash should contain contributor attributes:

    # ...
    puts 'Inviting contributors...'
    
    contributors = @client.create_contributors project_id,
                                               email: 'sample_ms_translator@example.com',
                                               fullname: 'Ms. Translator',
                                               languages: [
                                                 {
                                                   lang_iso: 'en',
                                                   is_writable: false
                                                 },
                                                 {
                                                   lang_iso: 'fr',
                                                   is_writable: true
                                                 }
                                               ]
    
    contributor = contributors.collection.first
    
    puts contributor.fullname
    puts contributor.user_id

    Key points:

    • We are adding one contributor, therefore the second argument is a hash, not an array of hashes (though you could provide an array with a single hash inside).
    • This contributor will have read-only access to English translations and will be able to read and modify French translations.
    • create_contributors method always returns a collection of objects representing newly added contributors. Collections are usually paginated and respond to methods like total_pages, next_page? (is there a next page available), prev_page (fetch items on the previous page), and some others. To get access to the actual data (that is, our new contributor), you’ll have to use the collection method. It returns an array of models, and to fetch the first contributor we simply say first.
    • Contributor model responds to methods named after the attributes that you can find in the API docs. For example, you can call fullname, email, is_admin, and so on.

    In TypeScript

    Next let’s achieve the same goal in TypeScript. Use the contributors().create() method inside the main() function and pass an array of objects with contributor attributes:

    console.log("Inviting contributors...");
    
    const contributors = await lokaliseApi.contributors().create(
      [
        {
          email: "translator@example.com",
          fullname: "Mr. Translator",
          is_admin: false,
          is_reviewer: true,
          languages: [
            {
              lang_iso: "en",
              is_writable: false,
            },
            {
              lang_iso: "fr",
              is_writable: true,
            },
          ],
        },
      ],
      { project_id: projectId }
    );
    
    console.log(contributors[0].email);
    console.log(contributors[0].user_id);

    We are adding a new contributor “Mr. Translator” who is going to have full access to French language and read-only access to English. Don’t forget to provide your project ID as the last argument to the create method.

    contributors constant will contain an array of newly created contributors (even if you are inviting only one person). Therefore, to grab the first contributor we’re saying [0]. You can find contributor attributes in the API docs.

    In Python

    Finally, let’s invite contributors with Python SDK. Use the create_contributors() method and pass an array of objects with contributor attributes:

    print("Inviting contributors...")
    
    contributors = client.create_contributors(project_id, [
        {
            "email": "translator@example.com",
            "fullname": "Mr. Translator",
            "is_admin": False,
            "is_reviewer": True,
            "languages": [
                {
                    "lang_iso": "en",
                    "is_writable": False
                },
                {
                    "lang_iso": "fr",
                    "is_writable": True
                }
            ]
        }
    ])
    
    contributor = contributors.items[0]
    
    print(contributor.email)
    print(contributor.user_id)

    We are adding a new contributor “Mr. Translator” who is going to have full access to French language and read-only access to English.

    contributors variable will contain a collection of newly created contributors (even if you are inviting only one person). Therefore, to grab the first contributor we’re saying contributors.items[0]. You can find contributor attributes in the API docs. The contributors collection responds to other methods as explained in the docs.

    Uploading translation files

    Translation files uploading via API is a slightly more complex task:

    • You have to encode translation files content in base64.
    • The actual uploading process happens in the background, so you’ll have to poll the API to update the process status.
    • Lokalise API has rate limits: you cannot send more than six requests per second.

    But, fear not, we’ll overcome these issues in no time!

    In Ruby

    If you are using Ruby then you’re in luck because all the heavy lifting will be done by the lokalise_manager gem. This solution allows us to exchange translation files between your project and Lokalise TMS easily. On top of that, there’s a dedicated Rails integration. Cool, eh?

    All you need to do is configure lokalise_manager:

    LokaliseManager::GlobalConfig.config do |c|
      c.api_token = ENV.fetch('API_KEY', nil)
      c.locales_path = "#{Dir.getwd}/src/i18n"
    end

    This gem enables you to set many other options that you can find in the docs (for example, you can choose i18n directory, use other file formats, provide timeouts, and more). Options can also be set on a per-client basis, not globally.

    Now perform the actual export:

    puts 'Uploading translations...'
    
    exporter = LokaliseManager.exporter(project_id:)
    
    processes = exporter.export!

    processes will contain an array of objects. These object respond to the following methods:

    • success — returns true or false. If true, the uploading was successfully scheduled on Lokalise.
    • path — full path to the file being uploaded (instance of the Pathname class). Please note that each process will take care of uploading a single translation file.
    • process — an object representing the actual queued process.

    LokaliseManager will automatically encode your files in base64 format, upload them in parallel (up to 6 concurrent threads), and even take care of the rate limiting: if the limit is hit, an exponential backoff mechanism will be applied.

    Now we need to update the process status:

    exporter = LokaliseManager.exporter(project_id:)
    
    processes = exporter.export!
    
    def uploaded?(process)
      5.times do # try to check the status 5 times
        process = process.reload_data # load new data
        return(true) if process.status == 'finished' # return true is the upload has finished
    
        sleep 1 # wait for 1 second, adjust this number with regards to the upload size
      end
    
      false # if all 5 checks failed, return false (probably something is wrong)
    end
    
    uploaded? processes.first.process
    • We are doing five checks with a 1 second delay (if your translation files are large, you’ll probably need to increase this delay).
    • reload_data will update the process status.
    • As long as the processes variable contains a collection, we fetch the first process by saying .first.
    • Once process.status returns finished, we exit from the loop.

    Please note that by default if something goes wrong during the exporting process, the script will raise an exception and exit. However, you can set the raise_on_export_fail option to false, and in this case even if one or more files cannot be uploaded, LokaliseManager will still try to process other files. In this case you’ll have to make sure that success is true:

    if processes.first.success
      puts "Checking status for the #{processes.first.path} file..."
      uploaded? processes.first.process
    end

    Of course, you can manually check each process:

    processes = exporter.export!
    
    processes.each do |proc_data|
      if proc_data.success
        # Everything is good, the uploading is queued
        puts "#{proc_data.path} is sent to Lokalise!"
        process = proc_data.process
        puts "Current process status is #{process.status}"
      else
        # Something bad has happened
        puts "Could not send #{proc_data.path} to Lokalise"
        puts "Error #{proc_data.error.class}: #{proc_data.error.message}"
        # Or you could re-raise this exception:
        # raise proc_data.error.class
      end
    end

    You can make this example even more complex. For example, you can collect all the files that were successfully queued, then re-create the exporter object and adjust the skip_file_export option (please check the docs to learn more about it). This option allows you to provide exclusion criteria, and you can instruct the exporter to skip all the files that were already uploaded. Then, just run the exporter! method again to restart the whole process.

    In TypeScript

    To upload translation files in TypeScript, we have to encode them in base64 format first:

    import 'dotenv/config';
    import { LokaliseApi } from "@lokalise/node-api";
    import path from "path"; // <--- add this
    import fs from "fs/promises"; // <--- add this
    import {fileURLToPath} from 'url'; // <--- add this
    
    async function main() {
      // ...
    
      console.log("Uploading translations...");
    
      const __filename = fileURLToPath(import.meta.url);
      const __dirname = path.dirname(__filename);
      const i18nFolder = path.resolve(__dirname, 'i18n');
      const i18nFile = path.join(i18nFolder, 'en.json');
      const data = await fs.readFile(i18nFile, 'utf8');
      const buff = Buffer.from(data, 'utf8');
      const base64I18n = buff.toString('base64');
    }
    
    // ...

    Great, now the base64I18n constant contains properly encoded translations. Note that the __dirname is not available in the ESM projects so we have to add it manually. It’s not the case with CommonJS though.

    Next upload this data to Lokalise:

    // ...
    const base64I18n = buff.toString('base64');
    
    const bgProcess = await lokaliseApi.files().upload(projectId, { // <--- add this
      data: base64I18n,
      filename: "en.json",
      lang_iso: "en",
    });

    data, filename, and lang_iso are required attributes but the upload() method accepts some other options like convert_placeholders, tags, apply_tm, and so on.

    Finally, we have to wait until the background process status changes to finished:

    console.log("Updating process status...");
    
    await waitUntilUploadingDone(lokaliseApi, bgProcess.process_id, projectId);
    
    console.log("Uploading is done!");

    Here we are using a waitUntilUploadingDone function so let’s create it:

    async function waitUntilUploadingDone(
    	lokaliseApi: LokaliseApi,
    	processId: string,
    	projectId: string,
    ): Promise<string> {
    	return new Promise<string>((resolve, reject) => {
    		const interval = setInterval(async () => {
    			try {
    				const reloadedProcess = await lokaliseApi
    					.queuedProcesses()
    					.get(processId, {
    						project_id: projectId,
    					});
    
    				if (reloadedProcess.status === "finished") {
    					clearInterval(interval);
    					resolve(reloadedProcess.status);
    				}
    			} catch (error) {
    				clearInterval(interval);
    				console.error("An error occurred:", error);
    				reject("error");
    			}
    		}, 1000);
    	});
    }

    Here we take advantage of the queuedProcesses().get() method to load information about the background process. Once it changes to finished, we resolve the promise and clear the internal. Of course, you can enhance this code further and, for example, introduce an exponential backoff.

    In Python

    First of all, let’s encode our translation file in base64 format:

    print("Uploading translation file...")
    filename = os.path.join(os.path.dirname(__file__), 'i18n/en.json')
    with open(filename) as f:
        content = f.read()
        file_data = base64.b64encode(content.encode()).decode()

    Next, perform the actual upload and wait for the background process to finish:

    filename = os.path.join(os.path.dirname(__file__), 'i18n/en.json')
    with open(filename) as f:
        content = f.read()
        file_data = base64.b64encode(content.encode()).decode()
        bg_process = client.upload_file(project_id, {
            "data": file_data,
            "filename": 'en.json',
            "lang_iso": 'en'
        })
        print(f"Checking status for process {bg_process.process_id}...")
        result = is_uploaded(client, project_id, bg_process)
        print(result)

    data, filename, and lang_iso are required attributes but the upload method accepts some other options like convert_placeholders, tags, apply_tm, and so on.

    Finally, code the is_uploaded method which is going to read background process status five times with 1 second delay. If all five checks failed, we return False:

    def is_uploaded(api_client, project, process):
        for _i in range(5):
            process = api_client.queued_process(project, process.process_id)
            if process.status == 'finished':
                return True
            time.sleep(1)
        return False

    Listing translation keys

    The next thing I would like to do is assign a new translation key tasks to our contributors: specifically, I would like them to translate English texts into French. However, in order to do that, we firstly have to fetch translation key ids to add to the task. Therefore, let’s perform this step now.

    In Ruby

    To list translation keys with Ruby SDK, use the following approach:

    puts 'Getting translation keys...'
    
    key_ids = @client.keys(project_id).collection.map(&:key_id)
    
    puts key_ids

    The keys method returns a collection of keys (it can also accept a hash with options as a second argument), therefore we have to say collection to gain access to the actual data. As long as we are interested only in key ids (please note that each key responds to other methods named after the key attributes), we are calling key_id on each object.

    In TypeScript

    Now let’s fetch keys using Node SDK:

    console.log("Getting created translation keys...");
    
    const keys = await lokaliseApi.keys().list({
      project_id: projectId
    });
    
    const keyIds = keys.items.map((currentValue) => currentValue.key_id);
    
    console.log(keyIds);

    The keys constant will contain an array of translation keys, and each key has attributes listed in the API docs. In our case we are interested only in the key ids, so we call key_id on each object.

    In Python

    Finally, let’s fetch keys using Python SDK:

    print("Fetching translation keys...")
    keys = client.keys(project_id).items
    key_ids = list(map(lambda k: k.key_id, keys))
    print(key_ids)

    The keys() method returns a collection of keys (please note that it also accepts a second argument with a dict of options). To gain access to the actual keys data, we have to say .items. Then we simply fetch all key ids from every key object.

    Assigning translation tasks

    Once you have translation key ids, it’s time to assign tasks to our contributors.

    In Ruby

    puts 'Assigning translation task...'
    
    task = @client.create_task project_id,
                               title: 'Translate French',
                               keys: key_ids,
                               languages: [
                                 {
                                   language_iso: 'fr',
                                   users: [contributor.user_id]
                                 }
                               ]
    
    puts task.title

    The create_task method accepts a project ID and a hash with task attributes. We provide the task title, the key ids that should be added to this task (please note that the keys attribute must contain an array of integers or strings representing ids), and the language to translate into. Each language is represented as a hash, and you must provide an array of user ids to assign to this language. The contributor variable was already defined when we were inviting new contributors to the project, so here we simply fetch the id of this person.

    In TypeScript

    console.log("Assinging a translation task...");
    
    const task = await lokaliseApi.tasks().create(
      {
        title: "Translate French",
        keys: keyIds, // use ids obtained on the previous step
        languages: [
          {
            language_iso: "fr",
            users: [contributors[0].user_id], // an array of task assignee, we add the previously invited user
          },
        ],
      },
      { project_id: projectId }
    );
    
    console.log(task.title);
    console.log(task.languages[0].language_iso);

    The create method accepts an object with task attributes (title, an array of key ids to add to the task, and an array of languages). Please note that for each language you have to specify its ISO code and an array of user ids. The contributors constant is already defined so we simply get the first user id. Also don’t forget to specify the project id.

    The task constant contains an object with the newly assigned task. Please find a full list of task attributes in the API docs.

    In Python

    Finally, let’s create a new task with Python SDK:

    print("Assigning a translation task...")
    task = client.create_task(project_id, {
        "title": "Translate French",
        "keys": key_ids,
        "languages": [
            {
                "language_iso": "fr",
                "users": [contributor.user_id]
            }
        ]
    })
    print(task.title)
    print(task.languages[0].language_iso)

    The create_task method accepts a project ID and a dictionary with task parameters (title, an array of key ids to add to the task, and an array of languages). Please note that for each language you have to specify its ISO code and an array of user ids.

    The task variable contains an object with the newly assigned task. Please find a full list of task attributes in the API docs.

    Downloading translation files

    Once translations are completed, you’ll probably want to download them back to your project. Let’s see how to tackle this task.

    In Ruby

    To download translation files in Ruby, we’ll take advantage of the lokalise_manager gem once again:

    puts 'Downloading translations...'
    
    importer = LokaliseManager.importer project_id:,
                                        import_opts: {filter_langs: ['fr']}
    
    importer.import!

    Lokalise_manager will automatically unpack the downloaded ZIP archive with your translations and paste files into the i18n folder as dictated by the global configuration. Please note that in the example above I’m adding a custom option import_opts to download only the French translations. Please find other available options in the API docs.

    In TypeScript

    Downloading translations in TypeScript is a more involved task but all in all there’s nothing too complex. First, we are going to send a download request:

    console.log("Downloading translations...");
    
    const downloadResponse = await lokaliseApi.files().download(projectId, {
      format: "json",
      original_filenames: true,
      directory_prefix: '',
      filter_langs: ['fr'],
      indentation: '2sp',
    });

    The downloadResponse constant will contain an object with two properties: project_id and bundle_url. bundle_url contains a URL pointing to the archive with your translation files, so it’s our job to properly download and unpack this archive:

    // import adm-zip outside of the main() function:
    
    import AdmZip from "adm-zip";
    
    // ...then inside the main() function:
    
    const translationsUrl = downloadResponse.bundle_url;
    
    const zip = new AdmZip(await zipBuffer(translationsUrl));
    
    zip.extractAllTo(i18nFolder, true);

    Code the zipBuffer() function:

    async function zipBuffer(translationsUrl: string): Promise<Buffer> {
      const response = await fetch(translationsUrl);
      const arrayBuffer = await response.arrayBuffer();
    
      return Buffer.from(new Uint8Array(arrayBuffer));
    }

    Please note that the actual implementation might vary depending on what solutions you utilize to send HTTP requests and work with the archives. Here I’m using the native Fetch API but you can, for example, utilize axios, got, or any other library of your choice.

    In Python

    Downloading translation files in Python is quite simple. First, we should request a download request:

    print("Downloading translation file...")
    response = client.download_files(project_id, {
        "format": "json",
        "filter_langs": ["fr"],
        "original_filenames": True,
        "directory_prefix": ""
    })

    The response variable will contain an object with two properties: project_id and bundle_url. bundle_url contains a URL to the archive with your translation files, so it’s our job to properly download and unpack this archive.

    Let’s read the archive data using requests library:

    data = io.BytesIO(requests.get(response['bundle_url']).content)

    Finally, use zipfile to unpack the archive to the i18n folder:

    with zipfile.ZipFile(data) as archive:
        archive.extract("fr.json", path="src/i18n/")

    This is it, now your i18n folder should contain a new fr.json file!

    Conclusion

    In this article we have learned how to work with Lokalise APIv2 using Ruby, Python, and Node SDKs. We have discussed how to create projects, invite contributors, assign tasks, and exchange translation files. Please note that Lokalise API allows you to perform many other actions like adding comments, uploading screenshots, creating snapshots, and so on. Therefore, make sure to check the API docs.

    I thank you for staying with me today and until next time!

    Related articles
    Stop wasting time with manual localization tasks. 

    Launch global products days from now.