API access
This package offers a set of entry points that can be remotely called (for instance via cURL calls).
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.
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).
(where not specified, the HTTP Status Code of the response will be 200)
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)"
}
]
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"
]
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).
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 of0
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's0
if and only if there's no translated string, and it's100
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 (ornull
if there's no translations at all). For convenience, the timezone is always set to UTC.
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 bepo
ormo
-
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"
# ...
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
}
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.
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"}
]
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());