lib/transifex/client.rb
require 'faraday/follow_redirects'
module Transifex
class Client
class ApiFailure < StandardError; end
class BadRequest < StandardError; end
class NotFound < StandardError; end
class NotAllowed < StandardError; end
class NotAuthorised < StandardError; end
class ResponseError < StandardError; end
class AccessError < StandardError; end
BASE_URL = 'https://rest.api.transifex.com/'.freeze
ORGANIZATION = 'energy-sparks'.freeze
CONTENT_TYPE_JSON = 'application/vnd.api+json'.freeze
def initialize(api_key, project, stubs: nil)
@api_key = api_key
@project = project
@connection = Faraday.new(BASE_URL, headers: headers) do |f|
f.adapter(:test, stubs) if stubs
f.response(:follow_redirects)
end
end
def get_languages
url = make_url("projects/#{project_id}/languages")
get_data(url)
end
def list_resources
url = add_filter('resources')
get_data(url)
end
def get_resource(slug)
url = make_url("resources/#{resource_id(slug)}")
get_data(url)
end
def delete_resource(slug)
raise AccessError.new("#{slug} in #{@project} is not deletable") unless resource_is_deletable
url = make_url("resources/#{resource_id(slug)}")
del_data(url)
end
def create_resource(name, slug, categories = [], priority = 'normal')
url = make_url('resources')
post_data(url, resource_data(name, slug, project_id, categories, priority))
end
def create_resource_strings_async_upload(slug, content)
url = make_url('resource_strings_async_uploads')
post_data(url, resource_strings_async_upload_data(resource_id(slug), content))
end
def get_resource_strings_async_upload(resource_strings_async_upload_id)
url = make_url("resource_strings_async_uploads/#{resource_strings_async_upload_id}")
get_data(url) do |response|
process_async_upload_response(response)
end
end
def create_resource_translations_async_downloads(slug, language, mode = 'onlyreviewed')
url = make_url('resource_translations_async_downloads')
post_data(url, resource_translations_async_downloads_data(resource_id(slug), language, mode))
end
def get_resource_translations_async_download(resource_translations_async_download_id)
url = make_url("resource_translations_async_downloads/#{resource_translations_async_download_id}")
get_data(url) do |response|
process_async_download_response(response)
end
end
def get_resource_language_stats(slug = nil, language = nil)
if slug && language
url = make_url("resource_language_stats/#{resource_language_id(slug, language)}")
else
url = add_filter('resource_language_stats')
end
get_data(url)
end
private
def headers
{
'Authorization' => "Bearer #{@api_key}",
'Content-Type' => CONTENT_TYPE_JSON
}
end
def add_filter(path)
path + "?filter[project]=#{project_id}"
end
def make_url(path)
path
end
def project_id
"o:#{ORGANIZATION}:p:#{@project}"
end
def resource_id(slug)
"#{project_id}:r:#{slug}"
end
def resource_language_id(slug, language)
"#{resource_id(slug)}:l:#{language}"
end
def get_data(url)
response = @connection.get(url)
check_response_status(response)
block_given? ? yield(response) : process_response(response)
end
def post_data(url, data)
response = @connection.post(url, data.to_json)
check_response_status(response)
process_response(response)
end
def del_data(url)
response = @connection.delete(url)
check_response_status(response)
end
def check_response_status(response)
raise BadRequest.new(error_message(response)) if response.status == 400
raise NotAuthorised.new(error_message(response)) if response.status == 401
raise NotAllowed.new(error_message(response)) if response.status == 403
raise NotFound.new(error_message(response)) if response.status == 404
raise ApiFailure.new(error_message(response)) unless response.success?
true
end
def process_response(response)
JSON.parse(response.body)['data']
end
def process_async_download_response(response)
if json?(response)
data = process_response(response)
process_errors(data)
Response.new(completed: false, data: data)
else
Response.new(completed: true, content: response.body)
end
end
def process_async_upload_response(response)
data = process_response(response)
process_errors(data)
completed = (data['attributes']['status'] == 'succeeded')
Response.new(completed: completed, data: data)
end
def process_errors(data)
if data['attributes']['errors'].present?
raise ResponseError.new(error_messages(data['attributes']['errors']))
end
end
def error_messages(errors)
errors.map { |error| error['code'] + ': ' + error['detail'] }.join('\n')
end
def json?(response)
response.headers['content-type'].include?(CONTENT_TYPE_JSON)
end
def error_message(response)
data = JSON.parse(response.body)
if data['errors']
error_messages(data['errors'])
else
response.body
end
rescue
# problem parsing or traversing json, return original api error
response.body
end
def resource_is_deletable
if (deletable_projects = ENV['TRANSIFEX_DELETABLE_PROJECTS'])
return deletable_projects.split(',').include?(@project)
end
false
end
def resource_strings_async_upload_data(resource_id, content)
{
"data": {
"attributes": {
"content": content,
"content_encoding": 'text'
},
"relationships": {
"resource": {
"data": {
"id": resource_id,
"type": 'resources'
}
}
},
"type": 'resource_strings_async_uploads'
}
}
end
def resource_data(name, slug, project_id, categories, priority)
{
"data": {
"attributes": {
"accept_translations": true,
"categories": categories,
"name": name,
"priority": priority,
"slug": slug,
},
"relationships": {
"i18n_format": {
"data": {
"id": 'YML_KEY',
"type": 'i18n_formats'
}
},
"project": {
"data": {
"id": project_id,
"type": 'projects'
}
}
},
"type": 'resources'
}
}
end
def resource_translations_async_downloads_data(resource_id, language, mode)
{
"data": {
"attributes": {
"content_encoding": 'text',
"file_type": 'default',
"mode": mode,
"pseudo": false
},
"relationships": {
"language": {
"data": {
"id": "l:#{language}",
"type": 'languages'
}
},
"resource": {
"data": {
"id": resource_id,
"type": 'resources'
}
}
},
"type": 'resource_translations_async_downloads'
}
}
end
end
end