i18n_proofreading 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +214 -0
  3. data/MIT-LICENSE +21 -0
  4. data/README.md +325 -0
  5. data/Rakefile +16 -0
  6. data/app/controllers/i18n_proofreading/application_controller.rb +32 -0
  7. data/app/controllers/i18n_proofreading/suggestions_controller.rb +92 -0
  8. data/app/helpers/i18n_proofreading/tag_helper.rb +19 -0
  9. data/app/models/i18n_proofreading/application_record.rb +7 -0
  10. data/app/models/i18n_proofreading/suggestion.rb +27 -0
  11. data/app/views/i18n_proofreading/suggestions/index.html.erb +77 -0
  12. data/app/views/layouts/i18n_proofreading/application.html.erb +96 -0
  13. data/config/locales/i18n_proofreading.ar.yml +16 -0
  14. data/config/locales/i18n_proofreading.bg.yml +16 -0
  15. data/config/locales/i18n_proofreading.bn.yml +16 -0
  16. data/config/locales/i18n_proofreading.de.yml +16 -0
  17. data/config/locales/i18n_proofreading.el.yml +16 -0
  18. data/config/locales/i18n_proofreading.en.yml +35 -0
  19. data/config/locales/i18n_proofreading.es.yml +16 -0
  20. data/config/locales/i18n_proofreading.fr.yml +16 -0
  21. data/config/locales/i18n_proofreading.hi.yml +16 -0
  22. data/config/locales/i18n_proofreading.hr.yml +16 -0
  23. data/config/locales/i18n_proofreading.id.yml +16 -0
  24. data/config/locales/i18n_proofreading.it.yml +16 -0
  25. data/config/locales/i18n_proofreading.ja.yml +16 -0
  26. data/config/locales/i18n_proofreading.ko.yml +16 -0
  27. data/config/locales/i18n_proofreading.lb.yml +16 -0
  28. data/config/locales/i18n_proofreading.nl.yml +16 -0
  29. data/config/locales/i18n_proofreading.pl.yml +16 -0
  30. data/config/locales/i18n_proofreading.pt.yml +16 -0
  31. data/config/locales/i18n_proofreading.ro.yml +16 -0
  32. data/config/locales/i18n_proofreading.ru.yml +16 -0
  33. data/config/locales/i18n_proofreading.th.yml +16 -0
  34. data/config/locales/i18n_proofreading.tr.yml +16 -0
  35. data/config/locales/i18n_proofreading.uk.yml +16 -0
  36. data/config/locales/i18n_proofreading.ur.yml +16 -0
  37. data/config/locales/i18n_proofreading.vi.yml +16 -0
  38. data/config/locales/i18n_proofreading.zh-CN.yml +16 -0
  39. data/config/routes.rb +13 -0
  40. data/lib/generators/i18n_proofreading/install/install_generator.rb +40 -0
  41. data/lib/generators/i18n_proofreading/install/templates/create_i18n_proofreading_suggestions.rb.tt +22 -0
  42. data/lib/generators/i18n_proofreading/install/templates/initializer.rb +56 -0
  43. data/lib/i18n_proofreading/configuration.rb +91 -0
  44. data/lib/i18n_proofreading/engine.rb +26 -0
  45. data/lib/i18n_proofreading/marking.rb +46 -0
  46. data/lib/i18n_proofreading/middleware.rb +145 -0
  47. data/lib/i18n_proofreading/version.rb +5 -0
  48. data/lib/i18n_proofreading/widget.js +443 -0
  49. data/lib/i18n_proofreading/widget.rb +96 -0
  50. data/lib/i18n_proofreading.rb +37 -0
  51. metadata +111 -0
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module I18nProofreading
4
+ class SuggestionsController < ApplicationController
5
+ PER_PAGE = 50
6
+
7
+ # Public widget API is available-gated; the read-only dashboard is admin-gated.
8
+ before_action :require_available, only: %i[context create]
9
+ before_action :require_admin, only: :index
10
+
11
+ layout 'i18n_proofreading/application', only: :index
12
+
13
+ # Throttle the public submission endpoint per IP so one user or bot can't
14
+ # flood the table. Uses the rate limiter built into Rails 7.2+ (backed by
15
+ # Rails.cache); a no-op on Rails 7.1. Tune or disable via config.rate_limit —
16
+ # read once at boot, after the host's initializer.
17
+ if respond_to?(:rate_limit) && I18nProofreading.config.rate_limit
18
+ rate_limit(**I18nProofreading.config.rate_limit,
19
+ only: :create,
20
+ with: lambda {
21
+ render json: { errors: ['Too many suggestions. Please slow down and try again.'] },
22
+ status: :too_many_requests
23
+ })
24
+ end
25
+
26
+ # --- triage dashboard (admin) --------------------------------------------
27
+
28
+ def index
29
+ @status = Suggestion::STATUSES.include?(params[:status]) ? params[:status] : 'pending'
30
+ @locale = params[:locale].presence
31
+ @counts = Suggestion.group(:status).count
32
+ @locales = Suggestion.distinct.pluck(:locale).compact.sort
33
+
34
+ scope = Suggestion.where(status: @status)
35
+ scope = scope.where(locale: @locale) if @locale
36
+ @page = [params[:page].to_i, 1].max
37
+ rows = scope.newest_first.offset((@page - 1) * PER_PAGE).limit(PER_PAGE + 1).to_a
38
+ @more = rows.size > PER_PAGE
39
+ @suggestions = rows.first(PER_PAGE)
40
+ end
41
+
42
+ # --- widget API (public) -------------------------------------------------
43
+
44
+ # Pending suggestions for one key/locale, shown as read-only context when the
45
+ # proofreader reopens the popover for a string someone already flagged.
46
+ def context
47
+ suggestions = Suggestion
48
+ .where(translation_key: params[:key], locale: params[:locale])
49
+ .status_pending
50
+ .newest_first
51
+ .limit(20)
52
+
53
+ render json: suggestions.map { |suggestion| suggestion_json(suggestion) }
54
+ end
55
+
56
+ def create
57
+ suggestion = Suggestion.new(suggestion_params)
58
+ attribute_author(suggestion)
59
+
60
+ if suggestion.save
61
+ I18nProofreading.config.on_submit.call(suggestion)
62
+ head :created
63
+ else
64
+ render json: { errors: suggestion.errors.full_messages }, status: :unprocessable_entity
65
+ end
66
+ end
67
+
68
+ private
69
+
70
+ def attribute_author(suggestion)
71
+ author = current_author
72
+ return unless author
73
+
74
+ suggestion.author_id = author.id.to_s if author.respond_to?(:id)
75
+ suggestion.author_label = I18nProofreading.config.author_label.call(author)
76
+ end
77
+
78
+ def suggestion_json(suggestion)
79
+ {
80
+ proposed_value: suggestion.proposed_value,
81
+ author_label: suggestion.author_label,
82
+ created_at: suggestion.created_at.iso8601
83
+ }
84
+ end
85
+
86
+ def suggestion_params
87
+ params
88
+ .require(:suggestion)
89
+ .permit(:translation_key, :locale, :old_value, :proposed_value, :comment, :page_url)
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module I18nProofreading
4
+ # Lets a host place the widget explicitly (`<%= i18n_proofreading_tag %>` at the end
5
+ # of a layout) instead of relying on auto-injection. Renders nothing unless the
6
+ # tool is available for the current request.
7
+ module TagHelper
8
+ def i18n_proofreading_tag
9
+ return ''.html_safe unless I18nProofreading.available?(request)
10
+
11
+ Widget.snippet(
12
+ endpoint: I18nProofreading.config.suggestions_endpoint,
13
+ locale: I18n.locale,
14
+ active: I18nProofreading::Marking.enabled?,
15
+ nonce: (content_security_policy_nonce if respond_to?(:content_security_policy_nonce))
16
+ ).html_safe
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module I18nProofreading
4
+ class ApplicationRecord < ActiveRecord::Base
5
+ self.abstract_class = true
6
+ end
7
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module I18nProofreading
4
+ # A proposed wording for one translation key in one locale. Author attribution
5
+ # is optional and stored as loose fields (no foreign key to the host's user
6
+ # table) so the model is portable across apps with different user models.
7
+ class Suggestion < ApplicationRecord
8
+ # Lifecycle of a proposed wording: a fresh suggestion is `pending` until a
9
+ # developer applies it to the locale files or rejects it. String-backed so
10
+ # the column stays human-readable; prefixed so the generated methods read as
11
+ # `status_pending?` / `Suggestion.status_applied` and never clash.
12
+ STATUSES = %w[pending applied rejected].freeze
13
+
14
+ enum :status, STATUSES.index_by(&:itself), prefix: :status, default: :pending
15
+
16
+ validates :translation_key, presence: true, length: { maximum: 500 }
17
+ validates :proposed_value, presence: true, length: { maximum: 5_000 }
18
+ validates :old_value, length: { maximum: 5_000 }
19
+ validates :comment, length: { maximum: 2_000 }
20
+ validates :page_url, length: { maximum: 2_000 }
21
+ validates :locale,
22
+ presence: true,
23
+ inclusion: { in: ->(_) { I18nProofreading.config.available_locales.call.map(&:to_s) } }
24
+
25
+ scope :newest_first, -> { order(id: :desc) }
26
+ end
27
+ end
@@ -0,0 +1,77 @@
1
+ <header class="page">
2
+ <h1><%= t('i18n_proofreading.dashboard.title', default: 'Translation suggestions') %></h1>
3
+ <p class="lede"><%= t('i18n_proofreading.dashboard.subtitle', default: 'Review what reviewers proposed, then apply it to your locale files.') %></p>
4
+ </header>
5
+
6
+ <nav class="tabs">
7
+ <% I18nProofreading::Suggestion::STATUSES.each do |status| %>
8
+ <%= link_to suggestions_path(status: status, locale: @locale),
9
+ class: ('active' if status == @status) do %>
10
+ <%= t("i18n_proofreading.statuses.#{status}", default: status.humanize) %>
11
+ <span class="count"><%= @counts.fetch(status, 0) %></span>
12
+ <% end %>
13
+ <% end %>
14
+ </nav>
15
+
16
+ <% if @locales.many? %>
17
+ <form class="filters" method="get">
18
+ <input type="hidden" name="status" value="<%= @status %>">
19
+ <label for="i18np-locale"><%= t('i18n_proofreading.dashboard.locale', default: 'Locale') %></label>
20
+ <select id="i18np-locale" name="locale" onchange="this.form.submit()">
21
+ <option value=""><%= t('i18n_proofreading.dashboard.all_locales', default: 'All locales') %></option>
22
+ <% @locales.each do |locale| %>
23
+ <option value="<%= locale %>" <%= 'selected' if locale == @locale %>><%= locale %></option>
24
+ <% end %>
25
+ </select>
26
+ <%# Inline onchange auto-submits where allowed; this button is the reliable
27
+ path where a strict CSP blocks inline handlers, or with JS off. %>
28
+ <button><%= t('i18n_proofreading.dashboard.filter', default: 'Filter') %></button>
29
+ </form>
30
+ <% end %>
31
+
32
+ <% if @suggestions.any? %>
33
+ <% @suggestions.each do |suggestion| %>
34
+ <article class="card">
35
+ <div class="row-top">
36
+ <code class="key"><%= suggestion.translation_key %></code>
37
+ <span class="badge locale"><%= suggestion.locale %></span>
38
+ <span class="badge status-<%= suggestion.status %>">
39
+ <%= t("i18n_proofreading.statuses.#{suggestion.status}", default: suggestion.status.humanize) %>
40
+ </span>
41
+ <span class="spacer"></span>
42
+ <span class="meta">
43
+ <% if suggestion.author_label.present? %><%= suggestion.author_label %> · <% end %>
44
+ <span title="<%= suggestion.created_at %>"><%= time_ago_in_words(suggestion.created_at) %> <%= t('i18n_proofreading.dashboard.ago', default: 'ago') %></span>
45
+ </span>
46
+ </div>
47
+
48
+ <dl class="diff">
49
+ <dt><%= t('i18n_proofreading.dashboard.current', default: 'Current') %></dt>
50
+ <dd><span class="old" dir="auto"><%= suggestion.old_value.presence || '—' %></span></dd>
51
+ <dt><%= t('i18n_proofreading.dashboard.suggested', default: 'Suggested') %></dt>
52
+ <dd><span class="new" dir="auto"><%= suggestion.proposed_value %></span></dd>
53
+ </dl>
54
+
55
+ <% if suggestion.comment.present? %>
56
+ <div class="comment" dir="auto"><%= suggestion.comment %></div>
57
+ <% end %>
58
+ </article>
59
+ <% end %>
60
+
61
+ <nav class="pager">
62
+ <span>
63
+ <% if @page > 1 %>
64
+ <%= link_to t('i18n_proofreading.dashboard.newer', default: '← Newer'),
65
+ suggestions_path(status: @status, locale: @locale, page: @page - 1) %>
66
+ <% end %>
67
+ </span>
68
+ <span>
69
+ <% if @more %>
70
+ <%= link_to t('i18n_proofreading.dashboard.older', default: 'Older →'),
71
+ suggestions_path(status: @status, locale: @locale, page: @page + 1) %>
72
+ <% end %>
73
+ </span>
74
+ </nav>
75
+ <% else %>
76
+ <div class="empty"><%= t('i18n_proofreading.dashboard.empty', default: 'Nothing here yet.') %></div>
77
+ <% end %>
@@ -0,0 +1,96 @@
1
+ <!DOCTYPE html>
2
+ <html lang="<%= I18n.locale %>">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title><%= t('i18n_proofreading.dashboard.title', default: 'Translation suggestions') %></title>
6
+ <meta name="viewport" content="width=device-width, initial-scale=1">
7
+ <%= csrf_meta_tags %>
8
+ <style>
9
+ :root {
10
+ color-scheme: light dark;
11
+ --bg: #f6f7f9; --surface: #fff; --text: #1c2024; --muted: #6b7280;
12
+ --border: #e5e7eb; --accent: #2563eb; --accent-text: #fff; --code: #f0f1f3;
13
+ --pending: #d97706; --applied: #16a34a; --rejected: #6b7280;
14
+ }
15
+ @media (prefers-color-scheme: dark) {
16
+ :root {
17
+ --bg: #111418; --surface: #1a1f26; --text: #e6e8ea; --muted: #9aa2ab;
18
+ --border: #2a313a; --accent: #3b82f6; --code: #232a33;
19
+ }
20
+ }
21
+ * { box-sizing: border-box; }
22
+ body {
23
+ margin: 0; background: var(--bg); color: var(--text);
24
+ font: 15px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif;
25
+ }
26
+ a { color: var(--accent); text-decoration: none; }
27
+ a:hover { text-decoration: underline; }
28
+ .container { max-width: 960px; margin: 0 auto; padding: 24px 16px 64px; }
29
+ h1 { font-size: 22px; margin: 0; }
30
+ .lede { color: var(--muted); margin: 4px 0 0; }
31
+ header.page { margin-bottom: 20px; }
32
+
33
+ .tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--border); margin-bottom: 16px; flex-wrap: wrap; }
34
+ .tabs a { padding: 8px 14px; border-radius: 8px 8px 0 0; color: var(--muted); }
35
+ .tabs a.active {
36
+ color: var(--text); font-weight: 600; border: 1px solid var(--border);
37
+ border-bottom: 2px solid var(--surface); background: var(--surface); margin-bottom: -1px;
38
+ }
39
+ .tabs a:hover { text-decoration: none; color: var(--text); }
40
+ .count {
41
+ display: inline-block; min-width: 20px; padding: 0 6px; margin-left: 4px; border-radius: 999px;
42
+ background: var(--border); font-size: 12px; text-align: center; font-variant-numeric: tabular-nums;
43
+ }
44
+
45
+ .filters { margin-bottom: 16px; display: flex; gap: 8px; align-items: center; }
46
+ .filters label { color: var(--muted); font-size: 13px; }
47
+ .filters select {
48
+ padding: 6px 8px; border: 1px solid var(--border); border-radius: 8px;
49
+ background: var(--surface); color: var(--text); font: inherit;
50
+ }
51
+
52
+ .card {
53
+ background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
54
+ padding: 14px 16px; margin-bottom: 12px;
55
+ }
56
+ .row-top { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 10px; }
57
+ code.key {
58
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12.5px;
59
+ background: var(--code); padding: 2px 7px; border-radius: 6px; overflow-wrap: anywhere;
60
+ }
61
+ .badge {
62
+ display: inline-block; padding: 2px 10px; border-radius: 999px; font-size: 12px; font-weight: 600;
63
+ color: #fff; white-space: nowrap;
64
+ }
65
+ .badge.status-pending { background: var(--pending); }
66
+ .badge.status-applied { background: var(--applied); }
67
+ .badge.status-rejected { background: var(--rejected); }
68
+ .badge.locale { background: transparent; color: var(--muted); border: 1px solid var(--border); }
69
+ .spacer { flex: 1; }
70
+ .meta { color: var(--muted); font-size: 13px; }
71
+
72
+ dl.diff { display: grid; grid-template-columns: max-content 1fr; gap: 6px 16px; margin: 0; }
73
+ dl.diff dt { color: var(--muted); font-size: 13px; }
74
+ dl.diff dd { margin: 0; overflow-wrap: anywhere; white-space: pre-wrap; }
75
+ dd .old { color: var(--muted); }
76
+ dd .new { font-weight: 600; }
77
+ .comment { margin-top: 10px; padding: 8px 10px; background: var(--code); border-radius: 8px; font-size: 14px; overflow-wrap: anywhere; }
78
+
79
+ button {
80
+ padding: 7px 13px; border: 1px solid var(--border); border-radius: 8px; cursor: pointer;
81
+ background: var(--surface); color: var(--text); font: inherit; font-size: 14px; font-weight: 600;
82
+ }
83
+ button:hover { border-color: var(--muted); }
84
+ :focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
85
+
86
+ .empty { padding: 48px 16px; text-align: center; color: var(--muted); border: 1px dashed var(--border); border-radius: 12px; }
87
+ .pager { margin-top: 16px; display: flex; justify-content: space-between; }
88
+ .pager .muted { color: var(--muted); }
89
+ </style>
90
+ </head>
91
+ <body>
92
+ <div class="container">
93
+ <%= yield %>
94
+ </div>
95
+ </body>
96
+ </html>
@@ -0,0 +1,16 @@
1
+ ar:
2
+ i18n_proofreading:
3
+ pill: "اقترح تعديلات"
4
+ start: "تحسين الترجمة"
5
+ stop: "إيقاف الاقتراح (Esc)"
6
+ pill_active: "إيقاف الاقتراح (Esc)"
7
+ title: "اقترح تصحيحًا للترجمة"
8
+ current_text: "النص الحالي"
9
+ suggested_text: "النص المقترح"
10
+ comment: "تعليق"
11
+ comment_placeholder: "ملاحظة اختيارية للمطور"
12
+ prior_title: "تم اقتراحه مسبقًا (قيد الانتظار)"
13
+ cancel: "إلغاء"
14
+ save: "إرسال الاقتراح"
15
+ error_blank: "يرجى إدخال اقتراح."
16
+ error_save: "تعذّر حفظ الاقتراح."
@@ -0,0 +1,16 @@
1
+ bg:
2
+ i18n_proofreading:
3
+ pill: "Предложи промени"
4
+ pill_active: "Спиране на предложенията (Esc)"
5
+ start: "Подобряване на превода"
6
+ stop: "Спиране на предложенията (Esc)"
7
+ title: "Предложете корекция на превода"
8
+ current_text: "Текущ текст"
9
+ suggested_text: "Предложен текст"
10
+ comment: "Коментар"
11
+ comment_placeholder: "Незадължителна бележка за разработчика"
12
+ prior_title: "Вече предложено (чакащо)"
13
+ cancel: "Отказ"
14
+ save: "Изпрати предложение"
15
+ error_blank: "Моля, въведете предложение."
16
+ error_save: "Предложението не можа да бъде запазено."
@@ -0,0 +1,16 @@
1
+ bn:
2
+ i18n_proofreading:
3
+ pill: "সম্পাদনা প্রস্তাব করুন"
4
+ start: "অনুবাদ উন্নত করুন"
5
+ stop: "প্রস্তাব দেওয়া বন্ধ করুন (Esc)"
6
+ pill_active: "প্রস্তাব দেওয়া বন্ধ করুন (Esc)"
7
+ title: "অনুবাদ সংশোধন প্রস্তাব করুন"
8
+ current_text: "বর্তমান লেখা"
9
+ suggested_text: "প্রস্তাবিত লেখা"
10
+ comment: "মন্তব্য"
11
+ comment_placeholder: "ডেভেলপারের জন্য ঐচ্ছিক নোট"
12
+ prior_title: "ইতিমধ্যে প্রস্তাবিত (অপেক্ষমাণ)"
13
+ cancel: "বাতিল"
14
+ save: "প্রস্তাব পাঠান"
15
+ error_blank: "অনুগ্রহ করে একটি প্রস্তাব লিখুন।"
16
+ error_save: "প্রস্তাব সংরক্ষণ করা যায়নি।"
@@ -0,0 +1,16 @@
1
+ de:
2
+ i18n_proofreading:
3
+ pill: "Änderungen vorschlagen"
4
+ start: "Übersetzung verbessern"
5
+ stop: "Vorschläge beenden (Esc)"
6
+ pill_active: "Vorschläge beenden (Esc)"
7
+ title: "Übersetzungskorrektur vorschlagen"
8
+ current_text: "Aktueller Text"
9
+ suggested_text: "Vorgeschlagener Text"
10
+ comment: "Kommentar"
11
+ comment_placeholder: "Optionale Notiz für die Entwicklung"
12
+ prior_title: "Bereits vorgeschlagen (ausstehend)"
13
+ cancel: "Abbrechen"
14
+ save: "Vorschlag senden"
15
+ error_blank: "Bitte gib einen Vorschlag ein."
16
+ error_save: "Der Vorschlag konnte nicht gespeichert werden."
@@ -0,0 +1,16 @@
1
+ el:
2
+ i18n_proofreading:
3
+ pill: "Προτείνετε αλλαγές"
4
+ pill_active: "Διακοπή προτάσεων (Esc)"
5
+ start: "Βελτίωση μετάφρασης"
6
+ stop: "Διακοπή προτάσεων (Esc)"
7
+ title: "Προτείνετε διόρθωση μετάφρασης"
8
+ current_text: "Τρέχον κείμενο"
9
+ suggested_text: "Προτεινόμενο κείμενο"
10
+ comment: "Σχόλιο"
11
+ comment_placeholder: "Προαιρετική σημείωση για τον προγραμματιστή"
12
+ prior_title: "Έχει ήδη προταθεί (σε εκκρεμότητα)"
13
+ cancel: "Άκυρο"
14
+ save: "Αποστολή πρότασης"
15
+ error_blank: "Παρακαλώ εισαγάγετε μια πρόταση."
16
+ error_save: "Δεν ήταν δυνατή η αποθήκευση της πρότασης."
@@ -0,0 +1,35 @@
1
+ en:
2
+ i18n_proofreading:
3
+ pill: "Suggest edits"
4
+ start: "Improve translation"
5
+ stop: "Stop suggesting (Esc)"
6
+ pill_active: "Stop suggesting (Esc)"
7
+ title: "Suggest a translation fix"
8
+ current_text: "Current text"
9
+ suggested_text: "Suggested text"
10
+ comment: "Comment"
11
+ comment_placeholder: "Optional note for the developer"
12
+ prior_title: "Already suggested (pending)"
13
+ cancel: "Cancel"
14
+ save: "Send suggestion"
15
+ error_blank: "Please enter a suggestion."
16
+ error_save: "Could not save the suggestion."
17
+ # Triage dashboard. English-only by default (an admin-facing surface); every
18
+ # string is rendered through I18n with these as fallbacks, so a host can
19
+ # translate or reword any of them under its own `i18n_proofreading.*` scope.
20
+ statuses:
21
+ pending: "Pending"
22
+ applied: "Applied"
23
+ rejected: "Rejected"
24
+ dashboard:
25
+ title: "Translation suggestions"
26
+ subtitle: "Review what reviewers proposed, then apply it to your locale files."
27
+ locale: "Locale"
28
+ filter: "Filter"
29
+ all_locales: "All locales"
30
+ current: "Current"
31
+ suggested: "Suggested"
32
+ ago: "ago"
33
+ empty: "Nothing here yet."
34
+ newer: "← Newer"
35
+ older: "Older →"
@@ -0,0 +1,16 @@
1
+ es:
2
+ i18n_proofreading:
3
+ pill: "Sugerir cambios"
4
+ start: "Mejorar traducción"
5
+ stop: "Dejar de sugerir (Esc)"
6
+ pill_active: "Dejar de sugerir (Esc)"
7
+ title: "Sugerir una corrección de traducción"
8
+ current_text: "Texto actual"
9
+ suggested_text: "Texto sugerido"
10
+ comment: "Comentario"
11
+ comment_placeholder: "Nota opcional para el desarrollador"
12
+ prior_title: "Ya sugerido (pendiente)"
13
+ cancel: "Cancelar"
14
+ save: "Enviar sugerencia"
15
+ error_blank: "Introduce una sugerencia."
16
+ error_save: "No se pudo guardar la sugerencia."
@@ -0,0 +1,16 @@
1
+ fr:
2
+ i18n_proofreading:
3
+ pill: "Proposer des corrections"
4
+ start: "Améliorer la traduction"
5
+ stop: "Arrêter de proposer (Esc)"
6
+ pill_active: "Arrêter de proposer (Échap)"
7
+ title: "Proposer une correction de traduction"
8
+ current_text: "Texte actuel"
9
+ suggested_text: "Texte proposé"
10
+ comment: "Commentaire"
11
+ comment_placeholder: "Note facultative pour le développeur"
12
+ prior_title: "Déjà proposé (en attente)"
13
+ cancel: "Annuler"
14
+ save: "Envoyer la suggestion"
15
+ error_blank: "Veuillez saisir une suggestion."
16
+ error_save: "Impossible d'enregistrer la suggestion."
@@ -0,0 +1,16 @@
1
+ hi:
2
+ i18n_proofreading:
3
+ pill: "बदलाव सुझाएँ"
4
+ start: "अनुवाद सुधारें"
5
+ stop: "सुझाव देना बंद करें (Esc)"
6
+ pill_active: "सुझाव देना बंद करें (Esc)"
7
+ title: "अनुवाद सुधार सुझाएँ"
8
+ current_text: "वर्तमान टेक्स्ट"
9
+ suggested_text: "सुझाया गया टेक्स्ट"
10
+ comment: "टिप्पणी"
11
+ comment_placeholder: "डेवलपर के लिए वैकल्पिक टिप्पणी"
12
+ prior_title: "पहले से सुझाया गया (लंबित)"
13
+ cancel: "रद्द करें"
14
+ save: "सुझाव भेजें"
15
+ error_blank: "कृपया एक सुझाव दर्ज करें।"
16
+ error_save: "सुझाव सहेजा नहीं जा सका।"
@@ -0,0 +1,16 @@
1
+ hr:
2
+ i18n_proofreading:
3
+ pill: "Predloži izmjene"
4
+ pill_active: "Prestani predlagati (Esc)"
5
+ start: "Poboljšaj prijevod"
6
+ stop: "Prestani predlagati (Esc)"
7
+ title: "Predložite ispravak prijevoda"
8
+ current_text: "Trenutni tekst"
9
+ suggested_text: "Predloženi tekst"
10
+ comment: "Komentar"
11
+ comment_placeholder: "Neobavezna napomena za razvojni tim"
12
+ prior_title: "Već predloženo (na čekanju)"
13
+ cancel: "Odustani"
14
+ save: "Pošalji prijedlog"
15
+ error_blank: "Unesite prijedlog."
16
+ error_save: "Prijedlog nije moguće spremiti."
@@ -0,0 +1,16 @@
1
+ id:
2
+ i18n_proofreading:
3
+ pill: "Sarankan perubahan"
4
+ start: "Perbaiki terjemahan"
5
+ stop: "Berhenti menyarankan (Esc)"
6
+ pill_active: "Berhenti menyarankan (Esc)"
7
+ title: "Sarankan perbaikan terjemahan"
8
+ current_text: "Teks saat ini"
9
+ suggested_text: "Teks yang disarankan"
10
+ comment: "Komentar"
11
+ comment_placeholder: "Catatan opsional untuk pengembang"
12
+ prior_title: "Sudah disarankan (menunggu)"
13
+ cancel: "Batal"
14
+ save: "Kirim saran"
15
+ error_blank: "Silakan masukkan saran."
16
+ error_save: "Tidak dapat menyimpan saran."
@@ -0,0 +1,16 @@
1
+ it:
2
+ i18n_proofreading:
3
+ pill: "Suggerisci modifiche"
4
+ start: "Migliora la traduzione"
5
+ stop: "Interrompi i suggerimenti (Esc)"
6
+ pill_active: "Interrompi i suggerimenti (Esc)"
7
+ title: "Suggerisci una correzione di traduzione"
8
+ current_text: "Testo attuale"
9
+ suggested_text: "Testo suggerito"
10
+ comment: "Commento"
11
+ comment_placeholder: "Nota facoltativa per lo sviluppatore"
12
+ prior_title: "Già suggerito (in sospeso)"
13
+ cancel: "Annulla"
14
+ save: "Invia suggerimento"
15
+ error_blank: "Inserisci un suggerimento."
16
+ error_save: "Impossibile salvare il suggerimento."
@@ -0,0 +1,16 @@
1
+ ja:
2
+ i18n_proofreading:
3
+ pill: "修正を提案"
4
+ start: "翻訳を改善"
5
+ stop: "提案を終了 (Esc)"
6
+ pill_active: "提案を終了(Esc)"
7
+ title: "翻訳の修正を提案"
8
+ current_text: "現在のテキスト"
9
+ suggested_text: "提案するテキスト"
10
+ comment: "コメント"
11
+ comment_placeholder: "開発者への任意のメモ"
12
+ prior_title: "提案済み(保留中)"
13
+ cancel: "キャンセル"
14
+ save: "提案を送信"
15
+ error_blank: "提案を入力してください。"
16
+ error_save: "提案を保存できませんでした。"
@@ -0,0 +1,16 @@
1
+ ko:
2
+ i18n_proofreading:
3
+ pill: "수정 제안"
4
+ start: "번역 개선"
5
+ stop: "제안 중지 (Esc)"
6
+ pill_active: "제안 중지 (Esc)"
7
+ title: "번역 수정 제안"
8
+ current_text: "현재 텍스트"
9
+ suggested_text: "제안 텍스트"
10
+ comment: "의견"
11
+ comment_placeholder: "개발자를 위한 선택 메모"
12
+ prior_title: "이미 제안됨 (대기 중)"
13
+ cancel: "취소"
14
+ save: "제안 보내기"
15
+ error_blank: "제안을 입력하세요."
16
+ error_save: "제안을 저장할 수 없습니다."
@@ -0,0 +1,16 @@
1
+ lb:
2
+ i18n_proofreading:
3
+ pill: "Ännerunge virschloen"
4
+ pill_active: "Virschléi stoppen (Esc)"
5
+ start: "Iwwersetzung verbesseren"
6
+ stop: "Virschléi stoppen (Esc)"
7
+ title: "Eng Iwwersetzungskorrektur virschloen"
8
+ current_text: "Aktuellen Text"
9
+ suggested_text: "Virgeschloenen Text"
10
+ comment: "Kommentar"
11
+ comment_placeholder: "Optional Notiz fir den Entwéckler"
12
+ prior_title: "Scho virgeschloen (a Waardschleef)"
13
+ cancel: "Ofbriechen"
14
+ save: "Virschlag schécken"
15
+ error_blank: "Gitt wannechgelift e Virschlag an."
16
+ error_save: "De Virschlag konnt net gespäichert ginn."
@@ -0,0 +1,16 @@
1
+ nl:
2
+ i18n_proofreading:
3
+ pill: "Wijzigingen voorstellen"
4
+ start: "Vertaling verbeteren"
5
+ stop: "Stoppen met voorstellen (Esc)"
6
+ pill_active: "Stoppen met voorstellen (Esc)"
7
+ title: "Een vertaalcorrectie voorstellen"
8
+ current_text: "Huidige tekst"
9
+ suggested_text: "Voorgestelde tekst"
10
+ comment: "Opmerking"
11
+ comment_placeholder: "Optionele notitie voor de ontwikkelaar"
12
+ prior_title: "Al voorgesteld (in behandeling)"
13
+ cancel: "Annuleren"
14
+ save: "Suggestie versturen"
15
+ error_blank: "Voer een suggestie in."
16
+ error_save: "Kan de suggestie niet opslaan."
@@ -0,0 +1,16 @@
1
+ pl:
2
+ i18n_proofreading:
3
+ pill: "Zaproponuj zmiany"
4
+ start: "Popraw tłumaczenie"
5
+ stop: "Zakończ sugerowanie (Esc)"
6
+ pill_active: "Zakończ sugerowanie (Esc)"
7
+ title: "Zaproponuj poprawkę tłumaczenia"
8
+ current_text: "Bieżący tekst"
9
+ suggested_text: "Proponowany tekst"
10
+ comment: "Komentarz"
11
+ comment_placeholder: "Opcjonalna notatka dla programisty"
12
+ prior_title: "Już zaproponowano (oczekujące)"
13
+ cancel: "Anuluj"
14
+ save: "Wyślij sugestię"
15
+ error_blank: "Wprowadź sugestię."
16
+ error_save: "Nie udało się zapisać sugestii."