Skip to content

API access

Michele Locati edited this page Mar 23, 2016 · 8 revisions

Introduction

This package offers a set of entry points that can be remotely called (for instance via cURL calls).

Access control

To control who can use these entry points, go to the dashboard page /dashboard/system/community_translation/options and assign a user group to the various operations in the API Access section. You can use the special group Guest to allow everyone to use the API functions, or Registered Users to allow all registered users.

If you specify a user group different from Guest, the API calls must include a special header named API-Token, whose value can be created for every user in the /dashboard/users/search dashboard page. If public profiles are enabled, every registered user can create/change/remove their own access token in the /account/edit_profile page.

Errors

In case of errors, the HTTP response code will be 400 or greater, and the response body will be contain the error description (in text/plain format using the UTF-8 encoding).

Entry points

(where not specified, the HTTP Status Code of the response will be 200)

List the available languages

You can call /api/locales/ (with a GET method) to get a list of available languages. The result will be a JSON array like for instance this one:

[  
   {  
      "id":"ar",
      "name":"Arabic"
   },
   {  
      "id":"ast_ES",
      "name":"Asturian (Spain)"
   },

   {  
      "id":"vi_VN",
      "name":"Vietnamese (Vietnam)"
   }
]

List all the packages

You can call /api/packages/ (with a GET method) to get the list of all the packages whose translations are managed by Community Translation. The list will be a simple JSON array with the package handles. Please note that the handle of the concrete5 core will be an empty string.

Sample result:

[
   "",
   "community_translation"
]

List the available versions of a package

You can call /api/package/PACKAGE_HANDLE/versions/ (with a GET method) to get the list of the versions for a package that hare managed by Community Translation. Instead of PACKAGE_HANDLE you have to use the package handle (that's an empty string for the concrete5 core). For instance, to list all the core versions managed you can call /api/package//versions/, obtaining this result:

[
   "5.4.2",
   "5.4.2.1",
   "5.7.5.6",
   "dev-5.6",
   "dev-5.7"
]

(the versions starting with dev- are for the development repositories).

Get some translation progress of a specific package version

In order to retrieve the translation progress of a package version, you have to make a GET call to /api/locales/PACKAGE_HANDLE/PACKAGE_VERSION/MINIMUM_LEVEL/, where:

  • PACKAGE_HANDLE is the handle of the package (that's an empty string for the concrete5 core)
  • PACKAGE_VERSION is the version of the package
  • MINIMUM_LEVEL is the minimum translation progress (from 0 to 100) of the languages (the languages that are below that threshold will be excluded from the results). Use a value of 0 to list all the languages.

For instance, to retrieve the stats about the concrete5 core - 5.7 development series, for the languages that are translated at 90% or above, simply call /api/locales//dev-5.7/90/ to get a result like this:

[  
   {  
      "id":"cs_CZ",
      "name":"Czech (Czech Republic)",
      "total":4335,
      "translated":4246,
      "progressShown":98,
      "updated":"2016-03-08T16:51:32+00:00"
   },
   {  
      "id":"da_DK",
      "name":"Danish (Denmark)",
      "total":4335,
      "translated":4330,
      "progressShown":99,
      "updated":"2016-03-08T16:51:52+00:00"
   }
]

Where:

  • id is the language identifier
  • name is the language name
  • total is the total number of strings that should be translated
  • translated is the number of translated strings
  • progressShown is a sort of rounded progress percentage: it's not exact since it's 0 if and only if there's no translated string, and it's 100 if and only if all the strings are translated.
  • updated is the ISO 8601 representation of the date/time when the last translation has been submitted (or null if there's no translations at all). For convenience, the timezone is always set to UTC.

Retrieve the translations

Translations can be retrieved both in po format (useful when translating) and in mo format (to be used when you need the compiled translations). To retrieve a translation file, simply perform a GET call to /api/FORMAT/PACKAGE_HANDLE/PACKAGE_VERSION/LANGUAGE_ID/, where:

  • FORMAT may be po or mo
  • PACKAGE_HANDLE is the handle of the package (that's an empty string for the concrete5 core)
  • PACKAGE_VERSION is the version of the package
  • LANGUAGE_ID is the language identifier

For instance, to retrieve the Italian translations in the po format of the concrete5 core - 5.7 development series, simply call /api/po//dev-5.7/it_IT, and you'll get this result:

msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"POT-Creation-Date: 2016-03-14T17:44:02+01:00\n"
"PO-Revision-Date: 2016-03-14T17:44:02+01:00\n"
"Language: it_IT\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"

#: concrete/blocks/autonav/templates/responsive_header_navigation
msgctxt "TemplateFileName"
msgid "Responsive Header Navigation"
msgstr "Intestazione navigazione responsive"

# ...

Add/update strings from packages

Every time a new package (or a new package version) gets uploaded to the PRB and/or to the approved marketplace, a call should be made to extract the translatable strings.

The call should be a POST to /api/package/import/translatable/ with the following fields:

  • handle: the package handle
  • version: the package handle
  • archive: the ZIP archive containing the package

If the operation gets completed successfully, you'll receive a response code 200, with a JSON object containing a field named changed whose value is true if new strings have been found, of false if nothing changed.
Example:

{
    "changed": true
}

Update package translations

Every time a user downloads a package from PRB or from the marketplace, a POST call should be made to /api/package/update/translations/ with the following parameters in the body:

  • handle: the package handle
  • version: the package handle
  • archive: the ZIP archive containing the package
  • keepOldTranslations (optional) if this field is set and it's not empty, the existing package translations will be merged with the ones that come from Community Translation

Community Translation will check if the package needs updated translations. If no new translations are needed, the response will have an HTTP Status code of 304 ("Not Modified") and the response body will be empty. If some translation file have been updated, the response will have an HTTP Status code of 201 ("Created") and the response body will be the new ZIP file with the updated language files.

Listing recently updated package translations

To list the packages whose translations have been updated after a certain date, you can perform a GET call to /api/package/updated/translations/, specifying in the since querystring parameter the Unix timestamp to limit the results (for instance, if you want to list the packages updated after 2016-03-22 15:53:13 (UTC), your request should be /api/package/updated/translations/?since=1458661993.

The result will be a JSON array of objects with two keys: handle (the package handle) and version (the package version). For instance:

[
    {"handle": "community_translation", "version": "0.0.1"},
    {"handle": "community_translation", "version": "1.0.0"},
    {"handle": "handyman", "version": "1.2.3"}
]

Sample code to perform API calls

Here's a sample class that makes it easy to perform API calls and retrieve their results:

class TranslateAPI
{
    /**
     * The root URL of the APIs.
     *
     * @var string
     */
    protected $rootURL = 'https://translate.concrete5.org/api';

    /**
     * The API entry point.
     *
     * @var string
     */
    protected $entryPoint;

    /**
     * The API token.
     *
     * @var string
     */
    protected $token;

    /**
     * The fields to post.
     *
     * @var array
     */
    protected $postFields;

    /**
     * The files to post.
     *
     * @var array
     */
    protected $postFiles;

    /**
     * Data from the last response.
     *
     * @var null|array
     */
    protected $lastResponse;

    /**
     * Initializes the instance.
     */
    public function __construct()
    {
        $this->reset(false);
    }

    /**
     * Set the root URL of the APIs.
     *
     * @param string $value
     */
    public function setRootURL($value)
    {
        $this->rootURL = (string) $value;
    }

    /**
     * Set the API entry point.
     *
     * @param string $value
     */
    public function setEntryPoint($value)
    {
        $this->entryPoint = (string) $value;
    }

    /**
     * Set the API token.
     *
     * @param string $value
     */
    public function setToken($value)
    {
        $this->token = (string) $value;
    }

    /**
     * Set a field to be sent via POST.
     *
     * @param string $fieldName
     * @param string $value
     */
    public function setPostField($fieldName, $value)
    {
        $this->postFields[$fieldName] = $value;
    }

    /**
     * (Re)Set all the fields to be sent via POST.
     *
     * @param array $postFields
     */
    public function setPostFields(array $postFields)
    {
        $this->postFields = $postFields;
    }

    /**
     * Set a file to be sent via POST.
     *
     * @param string $fieldName
     * @param string $path
     */
    public function setPostFile($fieldName, $path)
    {
        $this->postFiles[$fieldName] = $path;
    }

    /**
     * (Re)Set all the files to be sent via POST.
     *
     * @param array $postFiles
     */
    public function setPostFiles(array $postFiles)
    {
        $this->postFiles = $postFiles;
    }

    /**
     * Reset all the data.
     *
     * @param bool $keepToken Keep the API token?
     */
    public function reset($keepToken = true)
    {
        $this->lastResponse = null;
        $this->entryPoint = null;
        if (!$keepToken) {
            $this->token = '';
        }
        $this->postFields = array();
        $this->postFiles = array();
    }

    /**
     * Perform the API call.
     *
     * @throws \Exception
     *
     * @return array{
     *
     *     @var int $code
     *     @var mixed $result
     * }
     */
    public function exec()
    {
        $this->lastResponse = null;
        if ($this->entryPoint === null) {
            throw new \Exception('API entry point not set.');
        }
        $ch = curl_init();
        $url = rtrim($this->rootURL, '/').'/'.ltrim($this->entryPoint, '/');
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_FAILONERROR, false);
        curl_setopt($ch, CURLOPT_HEADER, false);
        if (empty($this->postFields) && empty($this->postFiles)) {
            curl_setopt($ch, CURLOPT_POST, false);
        } else {
            curl_setopt($ch, CURLOPT_POST, true);
            if (defined('CURLOPT_SAFE_UPLOAD')) {
                curl_setopt($ch, CURLOPT_SAFE_UPLOAD, true);
            }
            if (empty($this->postFiles)) {
                curl_setopt($ch, CURLOPT_POSTFIELDS, $this->postFields);
            } else {
                $fields = $this->postFields;
                if (class_exists('\CURLFile')) {
                    foreach ($this->postFiles as $fieldName => $path) {
                        $fields[$fieldName] = new \CURLFile($path);
                    }
                } else {
                    if (defined('CURLOPT_SAFE_UPLOAD')) {
                        curl_setopt($ch, CURLOPT_SAFE_UPLOAD, false);
                    }
                    foreach ($this->postFiles as $fieldName => $path) {
                        $fields[$fieldName] = '@'.$path;
                    }
                }
                curl_setopt($ch, CURLOPT_POSTFIELDS, $fields);
            }
        }
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        $headers = array(
            'Expect: ',
        );
        if ($this->token !== '') {
            $headers[] = 'API-Token: '.$this->token;
        }
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
        $responseBody = curl_exec($ch);
        if (!is_string($responseBody)) {
            $err = @curl_error($ch) ?: 'Unknown cURL error';
            @curl_close($ch);
            throw new \Exception($err);
        }
        $info = curl_getinfo($ch);
        curl_close($ch);
        if (!is_array($info)) {
            throw new \Exception('Failed to get response info');
        }
        if (!isset($info['http_code'])) {
            throw new \Exception('Failed to get HTTP response code');
        }
        $responseCode = @intval($info['http_code']);
        $contentType = isset($info['content_type']) ? (string) $info['content_type'] : '';
        $contentLength = (isset($info['size_download']) && is_numeric($info['size_download'])) ? @intval($info['size_download']) : null;
        if ($responseCode < 200) {
            throw new \Exception('Invalid HTTP response code received: '.$info['http_code']);
        }
        if ($responseCode >= 400) {
            if (strpos($contentType, 'text/plain') !== 0) {
                $errorMessage = 'Wrong content type of error '.$responseCode.': '.$contentType;
            } else {
                $errorMessage = $responseBody;
            }
            throw new \Exception($errorMessage, $responseCode);
        }
        if ($contentLength !== null && strlen($responseBody) !== $contentLength) {
            throw new \Exception("Wrong response size (expected: $contentLength, received: ".strlen($responseBody).")");
        }
        if (strpos($contentType, 'application/json') === 0) {
            if (strcasecmp(trim($responseBody), 'null') === 0) {
                $result = null;
            } else {
                $result = @json_decode($responseBody, true);
                if ($result === null) {
                    throw new \Exception('Failed to decode JSON response');
                }
            }
        } else {
            $result = $responseBody;
        }

        $this->lastResponse = array(
            'code' => $responseCode,
            'type' => $contentType,
            'data' => $result,
        );
    }

    /**
     * Return the HTTP code of the last good API call.
     *
     * @throws \Exception
     *
     * @return int
     */
    public function getLastResponseCode()
    {
        if ($this->lastResponse === null) {
            throw new \Exception('No last good API call data.');
        }

        return $this->lastResponse['code'];
    }

    /**
     * Return the Content Type of the last good API call.
     *
     * @throws \Exception
     *
     * @return string
     */
    public function getLastResponseType()
    {
        if ($this->lastResponse === null) {
            throw new \Exception('No last good API call data.');
        }

        return $this->lastResponse['type'];
    }

    /**
     * Return the last response data.
     *
     * @throws \Exception
     *
     * @return mixed
     */
    public function getLastResponseData()
    {
        if ($this->lastResponse === null) {
            throw new \Exception('No last good API call data.');
        }

        return $this->lastResponse['data'];
    }
}

Sample usage:

$api = new TranslateAPI();
$api->setEntryPoint('package/update/translations');
$api->setToken('your api token');
$api->setPostFields(array(
    'handle' => 'community_translation',
    'version' => '0.0.1',
));
$api->setPostFile('archive', '/path/to/community_translation-0.0.1.zip');
try {
    $api->exec();
} catch (\Exception $x) {
    echo $x->getMessage();
    return;
}

echo 'HTTP response code: ', $api->getLastResponseCode(), "\n";
echo 'HTTP response content-type: ', $api->getLastResponseType(), "\n";
echo 'HTTP response: ', "\n";
var_dump($api->getLastResponseData());