Class: PhraseAppUpdater::PhraseAppAPI

Inherits:
Object
  • Object
show all
Defined in:
lib/phraseapp_updater/phraseapp_api.rb

Defined Under Namespace

Classes: BadAPIKeyError, BadProjectIDError, Locale, MissingGitParentError, ProjectNameTakenError, ProjectNotFoundError, RateLimitError

Constant Summary collapse

GIT_TAG_PREFIX =
'gitancestor_'
PAGE_SIZE =
100

Instance Method Summary collapse

Constructor Details

#initialize(api_key, project_id, locale_file_class) ⇒ PhraseAppAPI

Returns a new instance of PhraseAppAPI.



17
18
19
20
21
22
23
24
25
26
27
# File 'lib/phraseapp_updater/phraseapp_api.rb', line 17

def initialize(api_key, project_id, locale_file_class)
  config = Phrase::Configuration.new do |c|
    c.api_key['Authorization'] = api_key
    c.api_key_prefix['Authorization'] = 'token'
    c.debugging = false
  end

  @client            = Phrase::ApiClient.new(config)
  @project_id        = project_id
  @locale_file_class = locale_file_class
end

Instance Method Details

#create_locale(name, default: false) ⇒ Object



80
81
82
83
84
85
86
87
# File 'lib/phraseapp_updater/phraseapp_api.rb', line 80

def create_locale(name, default: false)
  params = Phrase::LocaleCreateParameters.new(
    name: name,
    code: name,
    default: default,
  )
  phraseapp_request(Phrase::LocalesApi, :locale_create, @project_id, params)
end

#create_project(name, parent_commit, **opts) ⇒ Object

Parameters:



30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# File 'lib/phraseapp_updater/phraseapp_api.rb', line 30

def create_project(name, parent_commit, **opts)
  params = Phrase::ProjectCreateParameters.new(
    # Merges name and main_format into opts to prevent overriding these properties
    opts.merge(
      name: name,
      main_format: @locale_file_class.phraseapp_type
    )
  )

  project = phraseapp_request(Phrase::ProjectsApi, :project_create, params)

  STDERR.puts "Created project #{name} for #{parent_commit}"

  @project_id = project.id
  store_parent_commit(parent_commit)

  project.id
end

#download_files(locales, skip_unverified:) ⇒ Object



89
90
91
92
93
94
95
96
97
98
# File 'lib/phraseapp_updater/phraseapp_api.rb', line 89

def download_files(locales, skip_unverified:)
  results = threaded_request(locales) do |locale|
    STDERR.puts "Downloading file for #{locale}"
    download_locale(locale, skip_unverified)
  end

  locales.zip(results).map do |locale, file_contents|
    @locale_file_class.from_file_content(locale.name, file_contents)
  end
end

#download_locale(locale, skip_unverified) ⇒ Object



196
197
198
199
200
201
202
203
204
# File 'lib/phraseapp_updater/phraseapp_api.rb', line 196

def download_locale(locale, skip_unverified)
  opts = {
    file_format: @locale_file_class.phraseapp_type,
    skip_unverified_translations: skip_unverified,
  }

  # Avoid allocating a tempfile (and emitting unnecessary warnings) by using `return_type` of `String`
  phraseapp_request(Phrase::LocalesApi, :locale_download, @project_id, locale.id, return_type: 'String', **opts)
end

#fetch_localesObject



75
76
77
78
# File 'lib/phraseapp_updater/phraseapp_api.rb', line 75

def fetch_locales
  locales = paginated_request(Phrase::LocalesApi, :locales_list, @project_id)
  locales.map { |pa_locale| Locale.new(pa_locale) }
end

#lookup_project_id(name) ⇒ Object



49
50
51
52
53
54
55
# File 'lib/phraseapp_updater/phraseapp_api.rb', line 49

def lookup_project_id(name)
  result, = paginated_request(Phrase::ProjectsApi, :projects_list, per_page: PAGE_SIZE, limit: 1) { |p| p.name == name }

  raise ProjectNotFoundError.new(name) if result.nil?

  result.id
end

#read_parent_commitObject

We mark projects with their parent git commit using a tag with a well-known prefix. We only allow one tag with this prefix at once.



59
60
61
62
63
64
65
66
67
# File 'lib/phraseapp_updater/phraseapp_api.rb', line 59

def read_parent_commit
  git_tag, = paginated_request(Phrase::TagsApi, :tags_list, @project_id, limit: 1) do |t|
    t.name.start_with?(GIT_TAG_PREFIX)
  end

  raise MissingGitParentError.new if git_tag.nil?

  git_tag.name.delete_prefix(GIT_TAG_PREFIX)
end

#remove_keys_not_in_upload(upload_id) ⇒ Object



229
230
231
232
# File 'lib/phraseapp_updater/phraseapp_api.rb', line 229

def remove_keys_not_in_upload(upload_id)
  delete_pattern = "unmentioned_in_upload:#{upload_id}"
  phraseapp_request(Phrase::KeysApi, :keys_delete_collection, @project_id, q: delete_pattern)
end

#remove_keys_not_in_uploads(upload_ids) ⇒ Object



189
190
191
192
193
194
# File 'lib/phraseapp_updater/phraseapp_api.rb', line 189

def remove_keys_not_in_uploads(upload_ids)
  threaded_request(upload_ids) do |upload_id|
    STDERR.puts "Removing keys not in upload #{upload_id}"
    remove_keys_not_in_upload(upload_id)
  end
end

#update_parent_commit(commit_hash) ⇒ Object



69
70
71
72
73
# File 'lib/phraseapp_updater/phraseapp_api.rb', line 69

def update_parent_commit(commit_hash)
  previous_parent = read_parent_commit
  phraseapp_request(Phrase::TagsApi, :tag_delete, @project_id, GIT_TAG_PREFIX + previous_parent)
  store_parent_commit(commit_hash)
end

#upload_file(locale_file) ⇒ Object



206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
# File 'lib/phraseapp_updater/phraseapp_api.rb', line 206

def upload_file(locale_file)
  # The PhraseApp gem only accepts a filename to upload,
  # so we need to write the file out and pass it the path
  Tempfile.create([locale_file.locale_name, ".json"]) do |f|
    f.write(locale_file.content)
    f.close

    opts = {
      file:                 f,
      file_encoding:        'UTF-8',
      file_format:          @locale_file_class.phraseapp_type,
      locale_id:            locale_file.locale_name,
      skip_unverification:  false,
      update_translations:  true,
      tags:                 [generate_upload_tag],
    }

    result = phraseapp_request(Phrase::UploadsApi, :upload_create, @project_id, **opts)

    result.id
  end
end

#upload_files(locale_files, default_locale:) ⇒ Object



100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/phraseapp_updater/phraseapp_api.rb', line 100

def upload_files(locale_files, default_locale:)
  locale_files = locale_files.sort_by(&:locale_name)
  default_locale_file = locale_files.detect { |l| l.locale_name == default_locale }
  locale_files.delete(default_locale_file) if default_locale_file

  known_locales = fetch_locales.index_by(&:name)

  # Phraseapp appears to use to use the first file uploaded to resolve conflicts
  # between pluralized and non-pluralized keys. Upload and verify the canonical
  # default locale first before uploading translated locales.
  if default_locale_file
    unless known_locales.has_key?(default_locale_file.locale_name)
      STDERR.puts("Creating default locale (#{default_locale_file})")
      create_locale(default_locale_file.locale_name, default: true)
    end

    STDERR.puts("Uploading default locale (#{default_locale_file})")
    upload_id = upload_file(default_locale_file)

    successful_default_upload = verify_uploads({ upload_id => default_locale_file })
  else
    STDERR.puts("No upload for default locale (#{default_locale})")
  end

  # Ensure the locales all exist
  STDERR.puts('Creating translation locales')
  threaded_request(locale_files) do |locale_file|
    unless known_locales.has_key?(locale_file.locale_name)
      create_locale(locale_file.locale_name, default: false)
    end
  end

  uploads = Concurrent::Hash.new

  threaded_request(locale_files) do |locale_file|
    STDERR.puts("Uploading #{locale_file}")
    upload_id = upload_file(locale_file)
    uploads[upload_id] = locale_file
  end

  successful_uploads = verify_uploads(uploads)

  if default_locale_file
    successful_uploads = successful_uploads.merge(successful_default_upload)
  end

  successful_uploads
end

#verify_uploads(uploads) ⇒ Object

Given a map of => locale_file pairs, use the upload_show API to verify that they’re complete, and re-upload them if they failed. Return a map of locale name to upload id.



152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
# File 'lib/phraseapp_updater/phraseapp_api.rb', line 152

def verify_uploads(uploads)
  successful_upload_ids = Concurrent::Hash.new
  attempts = 1
  STDERR.puts('Verifying uploads...')
  until uploads.empty?
    threaded_request(uploads.to_a) do |upload_id, locale_file|
      upload = phraseapp_request(Phrase::UploadsApi, :upload_show, @project_id, upload_id)

      case upload.state
      when "enqueued", "processing"
        STDERR.puts("#{locale_file}: still processing")
      when "success"
        STDERR.puts("#{locale_file}: success")
        successful_upload_ids[locale_file.locale_name] = upload_id
        uploads.delete(upload_id)
      when "error"
        STDERR.puts("#{locale_file}: upload failure, retrying")
        new_upload_id = upload_file(locale_file)
        uploads.delete(upload_id)
        uploads[new_upload_id] = locale_file
      else
        raise RuntimeError.new("Unknown upload state: #{upload.state}")
      end
    end

    unless uploads.empty?
      delay = attempts ** 1.6 + 1
      STDERR.puts("#{uploads.size} remaining, waiting #{delay.round} seconds...")
      sleep(delay)
      attempts += 1
    end
  end

  successful_upload_ids
end