Skip to content

Commit 3d7bdef

Browse files
authored
export/import draft translation (#666)
1 parent 050c807 commit 3d7bdef

5 files changed

Lines changed: 192 additions & 3 deletions

File tree

app/admin/content/resource_content.rb

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,32 @@
217217
redirect_to [:cms, resource], notice: "#{resource.name} will be exported and sent to you via email. You can also download the export from this page after few minutes."
218218
end
219219

220+
member_action :export_draft_translations, method: 'get' do
221+
authorize! :export, resource
222+
223+
data = resource.export_draft_translations
224+
225+
send_data JSON.generate(data, state: JsonNoEscapeHtmlState.new),
226+
filename: "draft-translations-#{resource.id}.json",
227+
type: 'application/json'
228+
end
229+
230+
member_action :import_draft_translations, method: 'post' do
231+
authorize! :manage, resource
232+
233+
file = params.dig(:resource_content, :draft_file)
234+
235+
if file.blank?
236+
flash[:error] = 'Please select a draft translations JSON file to import.'
237+
return redirect_to [:cms, resource]
238+
end
239+
240+
stats = resource.import_draft_translations(file.read)
241+
242+
redirect_to [:cms, resource],
243+
notice: "Draft translations imported: #{stats[:created]} created, #{stats[:updated]} updated, #{stats[:skipped]} skipped."
244+
end
245+
220246
index do
221247
id_column
222248

@@ -490,6 +516,12 @@
490516
end
491517
end
492518

519+
sidebar 'Draft translations', only: :show, if: -> { can?(:manage, resource) && resource.translation? && !resource.one_word? } do
520+
div do
521+
render 'admin/draft_translations_transfer'
522+
end
523+
end
524+
493525
sidebar 'Contribution access', only: :show, if: -> { can?(:manage, resource) } do
494526
table do
495527
thead do

app/assets/stylesheets/shared/content.scss

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,3 +120,29 @@ body {
120120
.b{
121121
font-weight: bold;
122122
}
123+
124+
sup.footnote-marker {
125+
cursor: pointer;
126+
font-size: 0.7em;
127+
padding: 0.1em 0.3em;
128+
margin: 0 0.15em;
129+
color: #006d98;
130+
font-weight: 700;
131+
border-radius: 4px;
132+
background-color: rgba(0, 109, 152, 0.08);
133+
transition: background-color 0.15s ease;
134+
}
135+
136+
sup.footnote-marker:hover {
137+
background-color: rgba(0, 109, 152, 0.2);
138+
}
139+
140+
sup.footnote-grammar {
141+
color: #707070;
142+
font-style: italic;
143+
background-color: rgba(112, 112, 112, 0.1);
144+
}
145+
146+
sup.footnote-grammar:hover {
147+
background-color: rgba(112, 112, 112, 0.22);
148+
}
Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,33 @@
11
import {Controller} from "@hotwired/stimulus";
22

3+
const HARDCODED_FOOTNOTES = {
4+
pl: "Plural — the Arabic address or verb is plural",
5+
sg: "Singular — the Arabic address or verb is singular",
6+
dl: "Dual — the Arabic address or verb is dual",
7+
};
8+
39
export default class extends Controller {
410
connect() {
511
this.inlineFootnotes()
612
}
713

814
inlineFootnotes() {
915
const footnotes = this.element.querySelectorAll('sup')
10-
footnotes.forEach((dom, _i ) => {
11-
$(dom).append(`(${dom.getAttribute('foot_note')})`)
16+
footnotes.forEach((dom) => {
17+
const footNote = dom.getAttribute('foot_note')
18+
const marker = (dom.textContent || '').trim().toLowerCase()
19+
20+
dom.classList.add('footnote-marker')
21+
22+
if (!footNote && HARDCODED_FOOTNOTES[marker]) {
23+
dom.classList.add('footnote-grammar')
24+
dom.setAttribute('title', HARDCODED_FOOTNOTES[marker])
25+
return
26+
}
27+
28+
if (footNote) {
29+
$(dom).append(`(${footNote})`)
30+
}
1231
})
1332
}
14-
}
33+
}

app/models/resource_content.rb

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -648,6 +648,99 @@ def import_draft_tafsir(id)
648648
resource.run_draft_import_hooks
649649
end
650650

651+
def export_draft_translations
652+
translations = Draft::Translation
653+
.where(resource_content_id: id)
654+
.includes(:verse, :foot_notes)
655+
.order(:verse_id)
656+
657+
{
658+
resource_content_id: id,
659+
name: name,
660+
language_name: language_name,
661+
records_count: translations.size,
662+
translations: translations.map do |translation|
663+
{
664+
verse_key: translation.verse.verse_key,
665+
draft_text: translation.draft_text,
666+
current_text: translation.current_text,
667+
text_matched: translation.text_matched,
668+
need_review: translation.need_review,
669+
footnotes_count: translation.footnotes_count,
670+
current_footnotes_count: translation.current_footnotes_count,
671+
meta_data: translation.meta_data,
672+
foot_notes: translation.foot_notes.map do |foot_note|
673+
{
674+
id: foot_note.id,
675+
resource_content_id: foot_note.resource_content_id,
676+
draft_text: foot_note.draft_text,
677+
current_text: foot_note.current_text,
678+
text_matched: foot_note.text_matched
679+
}
680+
end
681+
}
682+
end
683+
}
684+
end
685+
686+
def import_draft_translations(json)
687+
data = json.is_a?(String) ? JSON.parse(json) : json
688+
stats = { created: 0, updated: 0, skipped: 0 }
689+
690+
(data['translations'] || []).each do |row|
691+
verse = Verse.find_by(verse_key: row['verse_key'])
692+
693+
if verse.nil?
694+
stats[:skipped] += 1
695+
next
696+
end
697+
698+
draft = Draft::Translation
699+
.where(resource_content_id: id, verse_id: verse.id)
700+
.first_or_initialize
701+
702+
was_new = draft.new_record?
703+
704+
draft.draft_text = row['draft_text']
705+
draft.current_text = row['current_text']
706+
draft.text_matched = row['text_matched']
707+
draft.need_review = row['need_review']
708+
draft.meta_data = row['meta_data']
709+
draft.imported = false
710+
draft.save(validate: false)
711+
712+
draft.foot_notes.destroy_all
713+
id_map = {}
714+
715+
(row['foot_notes'] || []).each do |note|
716+
foot_note = draft.foot_notes.create(
717+
resource_content_id: note['resource_content_id'],
718+
draft_text: note['draft_text'],
719+
current_text: note['current_text']
720+
)
721+
foot_note.update_columns(
722+
current_text: note['current_text'],
723+
text_matched: note['text_matched']
724+
)
725+
id_map[note['id'].to_s] = foot_note.id if note['id']
726+
end
727+
728+
if id_map.present?
729+
text = draft.draft_text.to_s
730+
id_map.each do |old_id, new_id|
731+
text = text.sub(/foot_note=(['"]?)#{old_id}\1/, "foot_note=#{new_id}")
732+
end
733+
draft.update_column(:draft_text, text)
734+
end
735+
736+
was_new ? stats[:created] += 1 : stats[:updated] += 1
737+
end
738+
739+
run_draft_import_hooks
740+
741+
stats
742+
end
743+
651744
def export_file_name
652745
ExportService.new(self).get_export_file_name
653746
end
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<div class="mt-2">
2+
<% count = resource.draft_translations.count %>
3+
4+
<% if count > 0 %>
5+
<p class="mb-1"><%= count %> draft translation<%= 's' unless count == 1 %> available.</p>
6+
<%= link_to 'Export draft translations', export_draft_translations_cms_resource_content_path(resource), class: 'button' %>
7+
<% else %>
8+
<p class="mb-1">No draft translations to export.</p>
9+
<% end %>
10+
11+
<hr class="my-3">
12+
13+
<%= form_with model: resource, url: import_draft_translations_cms_resource_content_path(resource),
14+
method: :post, enctype: 'multipart/form-data' do |form| %>
15+
<h4>Import draft translations</h4>
16+
<%= form.file_field :draft_file, required: true, accept: 'application/json,.json' %>
17+
<%= form.submit 'Upload & import', data: { disable_with: 'Importing...', confirm: 'This will create or overwrite draft translations for this resource. Continue?' } %>
18+
<% end %>
19+
</div>

0 commit comments

Comments
 (0)