shiplog 0.3.2 → 0.4.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +36 -0
- data/README.md +170 -2
- data/app/controllers/shiplog/admin/announcements_controller.rb +19 -0
- data/app/controllers/shiplog/admin/entries/announcement_notifications_controller.rb +18 -0
- data/app/controllers/shiplog/admin/entries/exports_controller.rb +45 -0
- data/app/controllers/shiplog/admin/entries/newsletter_retries_controller.rb +17 -0
- data/app/controllers/shiplog/admin/entries/newsletter_tests_controller.rb +39 -0
- data/app/controllers/shiplog/admin/entries/newsletters_controller.rb +40 -0
- data/app/controllers/shiplog/admin/entries/translations_controller.rb +1 -0
- data/app/controllers/shiplog/admin/entries_controller.rb +7 -3
- data/app/controllers/shiplog/admin/newsletter_subscribers_controller.rb +47 -0
- data/app/controllers/shiplog/changelog_entries_controller.rb +14 -2
- data/app/controllers/shiplog/changelog_feeds_controller.rb +6 -2
- data/app/controllers/shiplog/changelogs_controller.rb +11 -1
- data/app/controllers/shiplog/frontends_controller.rb +2 -1
- data/app/controllers/shiplog/newsletter/confirmations_controller.rb +27 -0
- data/app/controllers/shiplog/newsletter/preferences_controller.rb +50 -0
- data/app/controllers/shiplog/newsletter/subscriptions_controller.rb +70 -0
- data/app/controllers/shiplog/newsletter/unsubscriptions_controller.rb +36 -0
- data/app/controllers/shiplog/newsletter_controller.rb +34 -0
- data/app/controllers/shiplog/public_controller.rb +6 -0
- data/app/frontend/shiplog/admin.css +5 -0
- data/app/frontend/shiplog/once.js +31 -0
- data/app/frontend/shiplog/shiplog.css +72 -0
- data/app/helpers/shiplog/admin/entries_helper.rb +2 -0
- data/app/helpers/shiplog/newsletter_helper.rb +28 -0
- data/app/helpers/shiplog/newsletter_mailer_helper.rb +37 -0
- data/app/helpers/shiplog/permalink_helper.rb +20 -2
- data/app/helpers/shiplog/unseen_helper.rb +4 -0
- data/app/jobs/shiplog/announce_job.rb +30 -0
- data/app/jobs/shiplog/draft_announcement_job.rb +9 -0
- data/app/jobs/shiplog/purge_newsletter_job.rb +15 -0
- data/app/jobs/shiplog/send_newsletter_job.rb +64 -0
- data/app/jobs/shiplog/translate_entry_job.rb +2 -0
- data/app/mailers/shiplog/newsletter_mailer.rb +51 -0
- data/app/models/shiplog/announcements.rb +34 -0
- data/app/models/shiplog/area_detector.rb +2 -1
- data/app/models/shiplog/change_candidates.rb +4 -0
- data/app/models/shiplog/drafter.rb +4 -2
- data/app/models/shiplog/entry.rb +76 -6
- data/app/models/shiplog/markdown.rb +233 -0
- data/app/models/shiplog/newsletter/content.rb +172 -0
- data/app/models/shiplog/newsletter/csv.rb +32 -0
- data/app/models/shiplog/newsletter/recipients.rb +92 -0
- data/app/models/shiplog/newsletter/tokens.rb +77 -0
- data/app/models/shiplog/newsletter.rb +180 -0
- data/app/models/shiplog/newsletter_delivery.rb +83 -0
- data/app/models/shiplog/newsletter_subscriber.rb +158 -0
- data/app/models/shiplog/period.rb +129 -0
- data/app/models/shiplog/public_links.rb +47 -0
- data/app/models/shiplog/rewriter/announcement_prompt.rb +70 -0
- data/app/models/shiplog/rewriter.rb +23 -3
- data/app/models/shiplog/translator/prompt.rb +2 -2
- data/app/models/shiplog/translator.rb +1 -1
- data/app/views/shiplog/admin/_nav.html.erb +3 -0
- data/app/views/shiplog/admin/entries/_announcement.html.erb +52 -0
- data/app/views/shiplog/admin/entries/_badges.html.erb +1 -1
- data/app/views/shiplog/admin/entries/_newsletter.html.erb +42 -0
- data/app/views/shiplog/admin/entries/edit.html.erb +8 -2
- data/app/views/shiplog/admin/entries/index.html.erb +9 -0
- data/app/views/shiplog/admin/entries/newsletters/show.html.erb +46 -0
- data/app/views/shiplog/admin/newsletter_subscribers/index.html.erb +83 -0
- data/app/views/shiplog/changelog_entries/announcement.html.erb +16 -0
- data/app/views/shiplog/changelogs/_month.html.erb +10 -3
- data/app/views/shiplog/changelogs/show.html.erb +4 -1
- data/app/views/shiplog/newsletter/_form.html.erb +25 -0
- data/app/views/shiplog/newsletter/_page.html.erb +14 -0
- data/app/views/shiplog/newsletter/confirmations/create.html.erb +5 -0
- data/app/views/shiplog/newsletter/confirmations/show.html.erb +5 -0
- data/app/views/shiplog/newsletter/invalid.html.erb +4 -0
- data/app/views/shiplog/newsletter/preferences/destroy.html.erb +4 -0
- data/app/views/shiplog/newsletter/preferences/show.html.erb +23 -0
- data/app/views/shiplog/newsletter/rate_limited.html.erb +4 -0
- data/app/views/shiplog/newsletter/subscriptions/show.html.erb +16 -0
- data/app/views/shiplog/newsletter/unsubscriptions/create.html.erb +4 -0
- data/app/views/shiplog/newsletter/unsubscriptions/show.html.erb +5 -0
- data/app/views/shiplog/newsletter_mailer/announcement.html.erb +113 -0
- data/app/views/shiplog/newsletter_mailer/announcement.text.erb +38 -0
- data/app/views/shiplog/newsletter_mailer/confirmation.html.erb +43 -0
- data/app/views/shiplog/newsletter_mailer/confirmation.text.erb +12 -0
- data/config/admin_routes.rb +12 -0
- data/config/locales/en.yml +147 -0
- data/config/locales/es.yml +147 -0
- data/config/routes.rb +13 -0
- data/db/migrate/20261028000001_shiplog_0_4_0.rb +19 -0
- data/db/migrate/20261028000002_shiplog_0_4_0_newsletter.rb +44 -0
- data/lib/generators/shiplog/install/install_generator.rb +14 -1
- data/lib/generators/shiplog/install/templates/initializer.rb.tt +50 -0
- data/lib/shiplog/classes.rb +14 -1
- data/lib/shiplog/configuration.rb +132 -1
- data/lib/shiplog/engine.rb +2 -0
- data/lib/shiplog/previews/shiplog/newsletter_mailer_preview.rb +77 -0
- data/lib/shiplog/version.rb +1 -1
- data/lib/shiplog.rb +32 -0
- data/lib/tasks/shiplog.rake +22 -0
- metadata +56 -1
data/config/admin_routes.rb
CHANGED
|
@@ -5,6 +5,15 @@ Shiplog::Admin::Engine.routes.draw do
|
|
|
5
5
|
resource :approval, only: :create
|
|
6
6
|
resource :discard, only: %i[create destroy]
|
|
7
7
|
resource :rewrite, only: :create
|
|
8
|
+
# An announcement's text as a file to paste elsewhere: ?as=markdown, html or text, in the default locale or
|
|
9
|
+
# ?export_locale= (not :locale, which a host's locale-switching around_action would read; see translations).
|
|
10
|
+
get "export", to: "exports#show", as: :export
|
|
11
|
+
resource :announcement_notification, only: :create
|
|
12
|
+
# The newsletter of an announcement: the confirmation step with the recipient counts (show), the send
|
|
13
|
+
# (create), a test to the admin's address, and the retry of failed deliveries.
|
|
14
|
+
resource :newsletter, only: %i[show create]
|
|
15
|
+
resource :newsletter_test, only: :create, path: "newsletter/test"
|
|
16
|
+
resource :newsletter_retry, only: :create, path: "newsletter/retry"
|
|
8
17
|
# The locale segment is :translation_locale, not :locale: a host's around_action that switches the locale
|
|
9
18
|
# from params[:locale] must not read it (a configured translation locale may not be an available one).
|
|
10
19
|
locale = { translation_locale: /[A-Za-z][A-Za-z0-9_-]*/ }
|
|
@@ -15,6 +24,9 @@ Shiplog::Admin::Engine.routes.draw do
|
|
|
15
24
|
end
|
|
16
25
|
end
|
|
17
26
|
end
|
|
27
|
+
resources :announcements, only: :create
|
|
28
|
+
# The newsletter's subscribers: counts, a searchable list (?q=), removal, and the CSV export (.csv).
|
|
29
|
+
resources :newsletter_subscribers, only: %i[index destroy], path: "newsletter"
|
|
18
30
|
resource :area_detection, only: :create, path: "areas/detection"
|
|
19
31
|
resource :area_translation, only: :create, path: "areas/translation"
|
|
20
32
|
resources :areas, only: %i[index update destroy] do
|
data/config/locales/en.yml
CHANGED
|
@@ -14,6 +14,7 @@ en:
|
|
|
14
14
|
categories:
|
|
15
15
|
feature: New
|
|
16
16
|
change: Improved
|
|
17
|
+
announcement: Monthly update
|
|
17
18
|
empty: Nothing here yet
|
|
18
19
|
date_format: "%-d %B %Y"
|
|
19
20
|
roadmap:
|
|
@@ -55,17 +56,161 @@ en:
|
|
|
55
56
|
nav_label: More changelog entries
|
|
56
57
|
newer: "← Newer: %{title}"
|
|
57
58
|
older: "Older: %{title} →"
|
|
59
|
+
announcements:
|
|
60
|
+
read: "Read the %{month} summary"
|
|
61
|
+
back: "← Back to the changelog"
|
|
62
|
+
skeleton:
|
|
63
|
+
features: New
|
|
64
|
+
also_improved: Also improved
|
|
58
65
|
feed:
|
|
59
66
|
title_with_product: "%{product} changelog"
|
|
60
67
|
link_title: Changelog (Atom)
|
|
61
68
|
subscribe: Subscribe (Atom)
|
|
69
|
+
newsletter:
|
|
70
|
+
title: Newsletter
|
|
71
|
+
product: Product updates
|
|
72
|
+
back: "← Back to the changelog"
|
|
73
|
+
form:
|
|
74
|
+
title: Get the monthly summary by email
|
|
75
|
+
hint: One email a month, no more. Unsubscribe any time.
|
|
76
|
+
email_label: Email address
|
|
77
|
+
email_placeholder: you@example.com
|
|
78
|
+
submit: Subscribe
|
|
79
|
+
trap_label: Leave this field empty
|
|
80
|
+
invalid_email: Enter a valid email address.
|
|
81
|
+
sent:
|
|
82
|
+
heading: Check your inbox
|
|
83
|
+
text: We sent you a link to confirm your subscription. Open it to start receiving the monthly summary. If nothing arrives within a few minutes, look in your spam folder.
|
|
84
|
+
confirm:
|
|
85
|
+
heading: Confirm your subscription
|
|
86
|
+
text: "Click the button to receive the monthly summary at %{email}."
|
|
87
|
+
button: Confirm
|
|
88
|
+
done_heading: You're subscribed
|
|
89
|
+
done_text: "The monthly summary will arrive at %{email}. You can change the language or unsubscribe any time from your preferences."
|
|
90
|
+
preferences:
|
|
91
|
+
heading: Your subscription
|
|
92
|
+
email: Email
|
|
93
|
+
status: Status
|
|
94
|
+
statuses:
|
|
95
|
+
pending: Waiting for confirmation
|
|
96
|
+
confirmed: Subscribed
|
|
97
|
+
unsubscribed: Unsubscribed
|
|
98
|
+
language: Language
|
|
99
|
+
save: Save
|
|
100
|
+
saved: Preferences saved.
|
|
101
|
+
unsubscribe: Unsubscribe
|
|
102
|
+
unsubscribed: You're unsubscribed.
|
|
103
|
+
resubscribe: Resubscribe
|
|
104
|
+
resubscribed: You're subscribed again.
|
|
105
|
+
delete: Delete my data
|
|
106
|
+
delete_help: Deleting your data removes your address from our records entirely, including the record that you unsubscribed.
|
|
107
|
+
deleted_heading: Your data was deleted
|
|
108
|
+
deleted_text: Your email address is no longer stored. You can subscribe again from the changelog at any time.
|
|
109
|
+
unsubscribe:
|
|
110
|
+
heading: Unsubscribe
|
|
111
|
+
text: "Stop receiving the monthly summary at %{email}?"
|
|
112
|
+
button: Unsubscribe
|
|
113
|
+
done_heading: You're unsubscribed
|
|
114
|
+
done_text: "%{email} will not receive the monthly summary anymore."
|
|
115
|
+
invalid:
|
|
116
|
+
heading: This link is not valid
|
|
117
|
+
text: It may have expired or already been used. Subscribe again from the changelog to get a new one.
|
|
118
|
+
rate_limited:
|
|
119
|
+
heading: Too many attempts
|
|
120
|
+
text: Please try again in an hour.
|
|
121
|
+
mail:
|
|
122
|
+
view_in_browser: View in browser
|
|
123
|
+
changelog_button: See everything that shipped
|
|
124
|
+
highlight_button: See how it works
|
|
125
|
+
reason: "You receive this email because you subscribed to the %{product} monthly summary."
|
|
126
|
+
unsubscribe: Unsubscribe
|
|
127
|
+
preferences: Preferences
|
|
128
|
+
confirm_subject: "Confirm your subscription to the %{product} newsletter"
|
|
129
|
+
confirm_heading: One more step
|
|
130
|
+
confirm_intro: "Confirm that you want to receive the %{product} monthly summary at %{email}."
|
|
131
|
+
confirm_button: Confirm my subscription
|
|
132
|
+
confirm_expiry: "The link works for 7 days. If you did not ask for this, ignore this email: nothing is sent without confirmation."
|
|
133
|
+
confirm_link_text: "Or copy this link:"
|
|
62
134
|
admin:
|
|
135
|
+
newsletter:
|
|
136
|
+
title: Newsletter
|
|
137
|
+
help: The visitors who subscribed through the changelog's form. Your own users (config.newsletter_recipients) are not listed here.
|
|
138
|
+
total: Total
|
|
139
|
+
search: Search
|
|
140
|
+
search_placeholder: Search by email
|
|
141
|
+
clear: Clear
|
|
142
|
+
export: Download CSV
|
|
143
|
+
remove: Remove
|
|
144
|
+
removed: "%{email} removed."
|
|
145
|
+
empty: No subscribers yet.
|
|
146
|
+
no_match: "No subscriber matches “%{query}”."
|
|
147
|
+
statuses:
|
|
148
|
+
pending: Pending
|
|
149
|
+
confirmed: Confirmed
|
|
150
|
+
unsubscribed: Unsubscribed
|
|
151
|
+
columns:
|
|
152
|
+
email: Email
|
|
153
|
+
locale: Language
|
|
154
|
+
status: Status
|
|
155
|
+
source: Signed up on
|
|
156
|
+
subscribed_at: Signed up
|
|
157
|
+
recipients: Recipients
|
|
158
|
+
heading: Newsletter
|
|
159
|
+
not_sent: Not sent yet.
|
|
160
|
+
progress: "%{sent} sent, %{failed} failed, %{pending} pending."
|
|
161
|
+
stalled:
|
|
162
|
+
one: "%{count} has been pending for more than an hour (the worker may have died): Retry sends it again."
|
|
163
|
+
other: "%{count} have been pending for more than an hour (the worker may have died): Retry sends them again."
|
|
164
|
+
retry: Retry failed
|
|
165
|
+
retry_queued: Retrying the failed deliveries.
|
|
166
|
+
send: Send…
|
|
167
|
+
send_blocked: "Sending is not possible yet: %{reasons}."
|
|
168
|
+
blockers:
|
|
169
|
+
not_approved: the announcement is not approved
|
|
170
|
+
translations: "a translation is missing or stale (%{locales})"
|
|
171
|
+
from: config.newsletter_from is not set
|
|
172
|
+
address: config.newsletter_address is not set
|
|
173
|
+
base_url: config.canonical_url is not set (the links in emails must be absolute)
|
|
174
|
+
test: "Send a test to %{email}"
|
|
175
|
+
test_locale: Language
|
|
176
|
+
test_sent: "Test sent to %{email} (%{language})."
|
|
177
|
+
test_failed: "The test could not be sent (%{error}). See the log."
|
|
178
|
+
test_no_email: config.current_admin gives no email address, so no test can be sent.
|
|
179
|
+
confirm_title: "Send the newsletter: %{title}"
|
|
180
|
+
recipients: Recipients
|
|
181
|
+
recipients_none: Nobody to send to yet.
|
|
182
|
+
recipients_error: "The recipients could not be listed (%{error}). See the log for details."
|
|
183
|
+
confirm_help: Each recipient receives the email once, in their language. Sending again later only reaches the recipients who have not received it.
|
|
184
|
+
confirm_resend_help: Recipients who already received it are skipped.
|
|
185
|
+
confirm_button:
|
|
186
|
+
one: "Send to %{count} recipient"
|
|
187
|
+
other: "Send to %{count} recipients"
|
|
188
|
+
queued: The newsletter is being sent. Refresh in a moment.
|
|
189
|
+
back: Back to the announcement
|
|
190
|
+
announcements:
|
|
191
|
+
draft: "Draft announcement for %{month}"
|
|
192
|
+
drafted: Announcement draft ready.
|
|
193
|
+
nothing_to_announce: That month has no approved feature or change to announce yet.
|
|
194
|
+
publishing: Announcement
|
|
195
|
+
view_page: View the public page
|
|
196
|
+
external: Published by your app (config.announcements = :external).
|
|
197
|
+
announced: "Your app was notified (on_announcement_approved) at %{time}."
|
|
198
|
+
waiting_for_translations: Your app will be notified once every translation is current.
|
|
199
|
+
announcing: Notifying your app…
|
|
200
|
+
not_approved: Approve it to publish it and notify your app.
|
|
201
|
+
export: "Export:"
|
|
202
|
+
plain_text: Plain text
|
|
203
|
+
preview: Preview
|
|
204
|
+
notify_now: Notify now
|
|
205
|
+
notify_now_hint: Calls your app's hook now, without waiting for missing translations (English is used for them).
|
|
206
|
+
notify_queued: Your app will be notified in a moment.
|
|
63
207
|
title: Shiplog admin
|
|
64
208
|
nav:
|
|
65
209
|
drafts: Drafts
|
|
66
210
|
board: Roadmap board
|
|
67
211
|
sync: Sync
|
|
68
212
|
areas: Areas
|
|
213
|
+
newsletter: Newsletter
|
|
69
214
|
areas:
|
|
70
215
|
title: Product areas
|
|
71
216
|
help: The AI groups entries into areas (at most %{max}) while it rewrites them. Visitors can filter the changelog by area once two areas have published entries.
|
|
@@ -126,6 +271,7 @@ en:
|
|
|
126
271
|
area_gone: no longer exists
|
|
127
272
|
regenerate_warning: Regenerating unpublishes this entry until re-approved.
|
|
128
273
|
source_changed: GitHub text changed since this draft
|
|
274
|
+
entries_changed: The month's entries changed since this draft
|
|
129
275
|
rewrite_error: Rewrite failed
|
|
130
276
|
not_rewritten: Not rewritten yet
|
|
131
277
|
public_text: Public text
|
|
@@ -175,6 +321,7 @@ en:
|
|
|
175
321
|
roadmap: Roadmap
|
|
176
322
|
feature: Feature
|
|
177
323
|
change: Change
|
|
324
|
+
announcement: Announcement
|
|
178
325
|
columns:
|
|
179
326
|
entry: Entry
|
|
180
327
|
kind: Kind
|
data/config/locales/es.yml
CHANGED
|
@@ -18,6 +18,7 @@ es:
|
|
|
18
18
|
categories:
|
|
19
19
|
feature: Nuevo
|
|
20
20
|
change: Mejorado
|
|
21
|
+
announcement: Resumen mensual
|
|
21
22
|
empty: Todavía no hay nada
|
|
22
23
|
date_format: "%-d de %B de %Y"
|
|
23
24
|
roadmap:
|
|
@@ -59,17 +60,161 @@ es:
|
|
|
59
60
|
nav_label: Más cambios
|
|
60
61
|
newer: "← Más reciente: %{title}"
|
|
61
62
|
older: "Anterior: %{title} →"
|
|
63
|
+
announcements:
|
|
64
|
+
read: "Leer el resumen de %{month}"
|
|
65
|
+
back: "← Volver al registro de cambios"
|
|
66
|
+
skeleton:
|
|
67
|
+
features: Novedades
|
|
68
|
+
also_improved: También mejoramos
|
|
62
69
|
feed:
|
|
63
70
|
title_with_product: "Registro de cambios de %{product}"
|
|
64
71
|
link_title: Registro de cambios (Atom)
|
|
65
72
|
subscribe: Suscribirse (Atom)
|
|
73
|
+
newsletter:
|
|
74
|
+
title: Boletín
|
|
75
|
+
product: Novedades del producto
|
|
76
|
+
back: "← Volver al registro de cambios"
|
|
77
|
+
form:
|
|
78
|
+
title: Recibe el resumen mensual por correo
|
|
79
|
+
hint: Un correo al mes, nada más. Date de baja cuando quieras.
|
|
80
|
+
email_label: Dirección de correo
|
|
81
|
+
email_placeholder: tu@ejemplo.com
|
|
82
|
+
submit: Suscribirme
|
|
83
|
+
trap_label: Deja este campo vacío
|
|
84
|
+
invalid_email: Escribe una dirección de correo válida.
|
|
85
|
+
sent:
|
|
86
|
+
heading: Revisa tu bandeja de entrada
|
|
87
|
+
text: Te hemos enviado un enlace para confirmar la suscripción. Ábrelo para empezar a recibir el resumen mensual. Si no llega en unos minutos, mira en la carpeta de spam.
|
|
88
|
+
confirm:
|
|
89
|
+
heading: Confirma tu suscripción
|
|
90
|
+
text: "Pulsa el botón para recibir el resumen mensual en %{email}."
|
|
91
|
+
button: Confirmar
|
|
92
|
+
done_heading: Suscripción confirmada
|
|
93
|
+
done_text: "El resumen mensual llegará a %{email}. Puedes cambiar el idioma o darte de baja cuando quieras desde tus preferencias."
|
|
94
|
+
preferences:
|
|
95
|
+
heading: Tu suscripción
|
|
96
|
+
email: Correo
|
|
97
|
+
status: Estado
|
|
98
|
+
statuses:
|
|
99
|
+
pending: Pendiente de confirmación
|
|
100
|
+
confirmed: Suscrito
|
|
101
|
+
unsubscribed: Dado de baja
|
|
102
|
+
language: Idioma
|
|
103
|
+
save: Guardar
|
|
104
|
+
saved: Preferencias guardadas.
|
|
105
|
+
unsubscribe: Darme de baja
|
|
106
|
+
unsubscribed: Te has dado de baja.
|
|
107
|
+
resubscribe: Volver a suscribirme
|
|
108
|
+
resubscribed: Vuelves a estar suscrito.
|
|
109
|
+
delete: Borrar mis datos
|
|
110
|
+
delete_help: Borrar tus datos elimina tu dirección de nuestros registros por completo, incluido el registro de tu baja.
|
|
111
|
+
deleted_heading: Tus datos se han borrado
|
|
112
|
+
deleted_text: Tu dirección de correo ya no está guardada. Puedes volver a suscribirte desde el registro de cambios cuando quieras.
|
|
113
|
+
unsubscribe:
|
|
114
|
+
heading: Darse de baja
|
|
115
|
+
text: "¿Dejar de recibir el resumen mensual en %{email}?"
|
|
116
|
+
button: Darme de baja
|
|
117
|
+
done_heading: Te has dado de baja
|
|
118
|
+
done_text: "%{email} ya no recibirá el resumen mensual."
|
|
119
|
+
invalid:
|
|
120
|
+
heading: Este enlace no es válido
|
|
121
|
+
text: Puede que haya caducado o que ya se haya usado. Vuelve a suscribirte desde el registro de cambios para recibir otro.
|
|
122
|
+
rate_limited:
|
|
123
|
+
heading: Demasiados intentos
|
|
124
|
+
text: Inténtalo de nuevo dentro de una hora.
|
|
125
|
+
mail:
|
|
126
|
+
view_in_browser: Ver en el navegador
|
|
127
|
+
changelog_button: Ver todo lo que hemos lanzado
|
|
128
|
+
highlight_button: Ver cómo funciona
|
|
129
|
+
reason: "Recibes este correo porque te suscribiste al resumen mensual de %{product}."
|
|
130
|
+
unsubscribe: Darme de baja
|
|
131
|
+
preferences: Preferencias
|
|
132
|
+
confirm_subject: "Confirma tu suscripción al boletín de %{product}"
|
|
133
|
+
confirm_heading: Un paso más
|
|
134
|
+
confirm_intro: "Confirma que quieres recibir el resumen mensual de %{product} en %{email}."
|
|
135
|
+
confirm_button: Confirmar mi suscripción
|
|
136
|
+
confirm_expiry: "El enlace funciona durante 7 días. Si no has pedido esto, ignora este correo: no se envía nada sin confirmación."
|
|
137
|
+
confirm_link_text: "O copia este enlace:"
|
|
66
138
|
admin:
|
|
139
|
+
newsletter:
|
|
140
|
+
title: Boletín
|
|
141
|
+
help: Los visitantes que se suscribieron desde el formulario del registro de cambios. Tus propios usuarios (config.newsletter_recipients) no aparecen aquí.
|
|
142
|
+
total: Total
|
|
143
|
+
search: Buscar
|
|
144
|
+
search_placeholder: Buscar por correo
|
|
145
|
+
clear: Limpiar
|
|
146
|
+
export: Descargar CSV
|
|
147
|
+
remove: Eliminar
|
|
148
|
+
removed: "%{email} eliminado."
|
|
149
|
+
empty: Todavía no hay suscriptores.
|
|
150
|
+
no_match: "Ningún suscriptor coincide con “%{query}”."
|
|
151
|
+
statuses:
|
|
152
|
+
pending: Pendiente
|
|
153
|
+
confirmed: Confirmado
|
|
154
|
+
unsubscribed: Dado de baja
|
|
155
|
+
columns:
|
|
156
|
+
email: Correo
|
|
157
|
+
locale: Idioma
|
|
158
|
+
status: Estado
|
|
159
|
+
source: Página de alta
|
|
160
|
+
subscribed_at: Alta
|
|
161
|
+
recipients: Destinatarios
|
|
162
|
+
heading: Boletín
|
|
163
|
+
not_sent: Todavía no se ha enviado.
|
|
164
|
+
progress: "%{sent} enviados, %{failed} fallidos, %{pending} pendientes."
|
|
165
|
+
stalled:
|
|
166
|
+
one: "%{count} lleva más de una hora pendiente (puede que el worker se haya caído): Reintentar lo envía de nuevo."
|
|
167
|
+
other: "%{count} llevan más de una hora pendientes (puede que el worker se haya caído): Reintentar los envía de nuevo."
|
|
168
|
+
retry: Reintentar los fallidos
|
|
169
|
+
retry_queued: Reintentando los envíos fallidos.
|
|
170
|
+
send: Enviar…
|
|
171
|
+
send_blocked: "Todavía no se puede enviar: %{reasons}."
|
|
172
|
+
blockers:
|
|
173
|
+
not_approved: el anuncio no está aprobado
|
|
174
|
+
translations: "falta una traducción o está desactualizada (%{locales})"
|
|
175
|
+
from: config.newsletter_from no está configurado
|
|
176
|
+
address: config.newsletter_address no está configurado
|
|
177
|
+
base_url: config.canonical_url no está configurado (los enlaces de los correos deben ser absolutos)
|
|
178
|
+
test: "Enviar una prueba a %{email}"
|
|
179
|
+
test_locale: Idioma
|
|
180
|
+
test_sent: "Prueba enviada a %{email} (%{language})."
|
|
181
|
+
test_failed: "No se pudo enviar la prueba (%{error}). Consulta el registro."
|
|
182
|
+
test_no_email: config.current_admin no devuelve una dirección de correo, así que no se puede enviar ninguna prueba.
|
|
183
|
+
confirm_title: "Enviar el boletín: %{title}"
|
|
184
|
+
recipients: Destinatarios
|
|
185
|
+
recipients_none: Todavía no hay nadie a quien enviarlo.
|
|
186
|
+
recipients_error: "No se pudo obtener la lista de destinatarios (%{error}). Consulta el registro para más detalles."
|
|
187
|
+
confirm_help: Cada destinatario recibe el correo una sola vez, en su idioma. Si lo envías de nuevo más tarde, solo llega a quienes no lo han recibido.
|
|
188
|
+
confirm_resend_help: Se omite a quienes ya lo recibieron.
|
|
189
|
+
confirm_button:
|
|
190
|
+
one: "Enviar a %{count} destinatario"
|
|
191
|
+
other: "Enviar a %{count} destinatarios"
|
|
192
|
+
queued: El boletín se está enviando. Actualiza la página en un momento.
|
|
193
|
+
back: Volver al anuncio
|
|
194
|
+
announcements:
|
|
195
|
+
draft: "Redactar el anuncio de %{month}"
|
|
196
|
+
drafted: El borrador del anuncio está listo.
|
|
197
|
+
nothing_to_announce: Ese mes aún no tiene ninguna función o cambio aprobado que anunciar.
|
|
198
|
+
publishing: Anuncio
|
|
199
|
+
view_page: Ver la página pública
|
|
200
|
+
external: Lo publica tu aplicación (config.announcements = :external).
|
|
201
|
+
announced: "Se avisó a tu aplicación (on_announcement_approved) el %{time}."
|
|
202
|
+
waiting_for_translations: Se avisará a tu aplicación cuando todas las traducciones estén al día.
|
|
203
|
+
announcing: Avisando a tu aplicación…
|
|
204
|
+
not_approved: Apruébalo para publicarlo y avisar a tu aplicación.
|
|
205
|
+
export: "Exportar:"
|
|
206
|
+
plain_text: Texto sin formato
|
|
207
|
+
preview: Vista previa
|
|
208
|
+
notify_now: Avisar ahora
|
|
209
|
+
notify_now_hint: Llama ahora al hook de tu aplicación, sin esperar las traducciones que falten (se usa el inglés).
|
|
210
|
+
notify_queued: Se avisará a tu aplicación en un momento.
|
|
67
211
|
title: Administración de Shiplog
|
|
68
212
|
nav:
|
|
69
213
|
drafts: Borradores
|
|
70
214
|
board: Tablero de la hoja de ruta
|
|
71
215
|
sync: Sincronización
|
|
72
216
|
areas: Áreas
|
|
217
|
+
newsletter: Boletín
|
|
73
218
|
areas:
|
|
74
219
|
title: Áreas del producto
|
|
75
220
|
help: La IA agrupa las entradas en áreas (como máximo %{max}) mientras las reescribe. Los visitantes pueden filtrar el registro de cambios por área cuando hay dos áreas con entradas publicadas.
|
|
@@ -130,6 +275,7 @@ es:
|
|
|
130
275
|
area_gone: ya no existe
|
|
131
276
|
regenerate_warning: Al regenerar, esta entrada deja de estar publicada hasta que se apruebe de nuevo.
|
|
132
277
|
source_changed: El texto de GitHub cambió desde este borrador
|
|
278
|
+
entries_changed: Las entradas del mes cambiaron desde este borrador
|
|
133
279
|
rewrite_error: Falló la reescritura
|
|
134
280
|
not_rewritten: Todavía sin reescribir
|
|
135
281
|
public_text: Texto público
|
|
@@ -179,6 +325,7 @@ es:
|
|
|
179
325
|
roadmap: Hoja de ruta
|
|
180
326
|
feature: Función
|
|
181
327
|
change: Cambio
|
|
328
|
+
announcement: Anuncio
|
|
182
329
|
columns:
|
|
183
330
|
entry: Entrada
|
|
184
331
|
kind: Tipo
|
data/config/routes.rb
CHANGED
|
@@ -7,5 +7,18 @@ Shiplog::Engine.routes.draw do
|
|
|
7
7
|
format: false, constraints: { slug: /[a-z0-9-]+/ }
|
|
8
8
|
get Shiplog.config.changelog_path, to: "changelogs#show", as: :changelog
|
|
9
9
|
post "shiplog/github/webhook", to: "webhooks#create", as: :github_webhook
|
|
10
|
+
# The newsletter's pages live under shiplog/ (like the webhook), so a host that constrains the engine's mount to
|
|
11
|
+
# the changelog, the roadmap and /shiplog/ lets them through. Links from emails open a page with a button
|
|
12
|
+
# (GET), and the button POSTs: nothing changes state on a GET. A token is URL-safe base64 or base58.
|
|
13
|
+
token = { token: /[A-Za-z0-9_-]+/ }
|
|
14
|
+
get "shiplog/newsletter/subscription", to: "newsletter/subscriptions#show", as: :newsletter_subscription
|
|
15
|
+
post "shiplog/newsletter/subscription", to: "newsletter/subscriptions#create"
|
|
16
|
+
get "shiplog/newsletter/confirm/:token", to: "newsletter/confirmations#show", as: :newsletter_confirmation, constraints: token, format: false
|
|
17
|
+
post "shiplog/newsletter/confirm/:token", to: "newsletter/confirmations#create", constraints: token, format: false
|
|
18
|
+
get "shiplog/newsletter/preferences/:token", to: "newsletter/preferences#show", as: :newsletter_preferences, constraints: token, format: false
|
|
19
|
+
patch "shiplog/newsletter/preferences/:token", to: "newsletter/preferences#update", constraints: token, format: false
|
|
20
|
+
delete "shiplog/newsletter/preferences/:token", to: "newsletter/preferences#destroy", constraints: token, format: false
|
|
21
|
+
get "shiplog/newsletter/unsubscribe/:token", to: "newsletter/unsubscriptions#show", as: :newsletter_unsubscription, constraints: token, format: false
|
|
22
|
+
post "shiplog/newsletter/unsubscribe/:token", to: "newsletter/unsubscriptions#create", constraints: token, format: false
|
|
10
23
|
get "shiplog/assets/:version/:name", to: "frontends#show", as: :frontend, constraints: { version: /[^\/]+/, name: /[a-z_]+\.(css|js)/ }
|
|
11
24
|
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Everything 0.4.0 adds to the schema, one section per feature. Additive and reversible.
|
|
2
|
+
class Shiplog040 < ActiveRecord::Migration[8.1]
|
|
3
|
+
def change
|
|
4
|
+
# --- Monthly announcements ---
|
|
5
|
+
# The subject of an announcement entry: one row per month (starts_on is its first day, ends_on its last), so
|
|
6
|
+
# Shiplog::Entry keeps its polymorphic subject and its unique (subject, kind) index makes drafting idempotent.
|
|
7
|
+
create_table :shiplog_periods do |t|
|
|
8
|
+
t.date :starts_on, null: false
|
|
9
|
+
t.date :ends_on, null: false
|
|
10
|
+
t.timestamps
|
|
11
|
+
end
|
|
12
|
+
add_index :shiplog_periods, :starts_on, unique: true
|
|
13
|
+
|
|
14
|
+
# When config.on_announcement_approved was called for the announcement's current approval: set once it is
|
|
15
|
+
# approved and every configured locale has a current translation, cleared when it returns to draft, so the
|
|
16
|
+
# hook runs once per approval.
|
|
17
|
+
add_column :shiplog_entries, :announced_at, :datetime
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# The newsletter's tables (0.4.0, second part). Additive and reversible.
|
|
2
|
+
class Shiplog040Newsletter < ActiveRecord::Migration[8.1]
|
|
3
|
+
def change
|
|
4
|
+
# --- Newsletter ---
|
|
5
|
+
# Visitors who subscribed through Shiplog's form, and the addresses that unsubscribed (a suppression record,
|
|
6
|
+
# also for the host's own users, who are otherwise never stored). Double opt-in: confirmation_nonce is the
|
|
7
|
+
# single-use secret of the pending confirmation link, cleared when it is used; confirmed_at is set then;
|
|
8
|
+
# unsubscribed_at keeps an unsubscribed address suppressed; token reaches the preferences page without an
|
|
9
|
+
# account (rotated after each change there); source is the page the visitor signed up on (a consent record).
|
|
10
|
+
# Unique on the lowercased email.
|
|
11
|
+
create_table :shiplog_newsletter_subscribers do |t|
|
|
12
|
+
t.string :email, null: false
|
|
13
|
+
t.string :locale, null: false
|
|
14
|
+
t.string :token, null: false
|
|
15
|
+
t.string :confirmation_nonce
|
|
16
|
+
t.datetime :confirmed_at
|
|
17
|
+
t.datetime :unsubscribed_at
|
|
18
|
+
t.string :source
|
|
19
|
+
t.timestamps
|
|
20
|
+
end
|
|
21
|
+
add_index :shiplog_newsletter_subscribers, "lower(email)", unique: true, name: "index_shiplog_newsletter_subscribers_on_lower_email"
|
|
22
|
+
add_index :shiplog_newsletter_subscribers, :token, unique: true
|
|
23
|
+
add_index :shiplog_newsletter_subscribers, :confirmation_nonce, unique: true
|
|
24
|
+
add_index :shiplog_newsletter_subscribers, [ :confirmed_at, :unsubscribed_at ], name: "index_shiplog_newsletter_subscribers_on_status"
|
|
25
|
+
|
|
26
|
+
# One row per announcement and recipient ("User/42" for the host's users, "Subscriber/7" for Shiplog's
|
|
27
|
+
# subscribers), inserted before the email is sent: a retried job or a second click never sends twice.
|
|
28
|
+
# status is pending (claimed), sent or failed (error says why, without the address, for the admin's "Retry
|
|
29
|
+
# failed"). No email address is kept here; rows older than 180 days are purged.
|
|
30
|
+
create_table :shiplog_newsletter_deliveries do |t|
|
|
31
|
+
t.references :announcement, null: false, foreign_key: { to_table: :shiplog_entries, on_delete: :cascade }, index: false
|
|
32
|
+
t.string :recipient_key, null: false
|
|
33
|
+
t.string :locale, null: false
|
|
34
|
+
t.string :status, null: false, default: "pending"
|
|
35
|
+
t.datetime :sent_at
|
|
36
|
+
t.text :error
|
|
37
|
+
t.timestamps
|
|
38
|
+
end
|
|
39
|
+
add_index :shiplog_newsletter_deliveries, [ :announcement_id, :recipient_key ], unique: true, name: "index_shiplog_newsletter_deliveries_on_announcement_and_key"
|
|
40
|
+
add_index :shiplog_newsletter_deliveries, [ :announcement_id, :status ], name: "index_shiplog_newsletter_deliveries_on_announcement_and_status"
|
|
41
|
+
add_index :shiplog_newsletter_deliveries, :recipient_key
|
|
42
|
+
add_index :shiplog_newsletter_deliveries, :created_at
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -68,7 +68,20 @@ module Shiplog
|
|
|
68
68
|
area_link: "rounded-full border border-gray-300 px-3 py-0.5 text-sm hover:border-indigo-500 dark:border-gray-700 [&.shiplog-areas__link--current]:border-indigo-600 [&.shiplog-areas__link--current]:bg-indigo-600 [&.shiplog-areas__link--current]:text-white",
|
|
69
69
|
entry_area: "text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400",
|
|
70
70
|
entry_images: "mt-3 grid w-full gap-3",
|
|
71
|
-
entry_image: "h-auto max-w-full rounded-lg border border-gray-200 dark:border-gray-800"
|
|
71
|
+
entry_image: "h-auto max-w-full rounded-lg border border-gray-200 dark:border-gray-800",
|
|
72
|
+
announcement: "rounded-xl border border-gray-200 border-l-4 border-l-indigo-600 bg-white p-5 dark:border-gray-800 dark:border-l-indigo-500 dark:bg-gray-900",
|
|
73
|
+
announcement_title: "mt-2 mb-4 text-2xl font-semibold text-gray-900 dark:text-white",
|
|
74
|
+
announcement_body: "leading-relaxed text-gray-700 dark:text-gray-300 [&_h3]:mt-6 [&_h3]:mb-2 [&_h3]:text-lg [&_h3]:font-semibold [&_h4]:mt-4 [&_h4]:font-semibold [&_ul]:list-disc [&_ul]:pl-5 [&_ol]:list-decimal [&_ol]:pl-5 [&_a]:text-indigo-600 [&_a]:underline dark:[&_a]:text-indigo-400 [&_p]:my-2",
|
|
75
|
+
month_announcement: "-mt-1 mb-3",
|
|
76
|
+
month_announcement_link: "text-sm font-semibold text-indigo-600 hover:underline dark:text-indigo-400",
|
|
77
|
+
newsletter: "my-10 rounded-xl border border-gray-200 bg-gray-50 p-5 dark:border-gray-800 dark:bg-gray-900/50",
|
|
78
|
+
newsletter_title: "text-lg font-semibold",
|
|
79
|
+
newsletter_hint: "mt-1 mb-3 text-sm text-gray-500 dark:text-gray-400",
|
|
80
|
+
newsletter_form: "flex flex-wrap items-center gap-2",
|
|
81
|
+
newsletter_label: "text-sm font-medium",
|
|
82
|
+
newsletter_input: "min-w-0 flex-1 rounded-md border border-gray-300 bg-white px-3 py-1.5 text-sm dark:border-gray-700 dark:bg-gray-900",
|
|
83
|
+
newsletter_button: "rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-indigo-500 [&.shiplog-newsletter__button--secondary]:bg-transparent [&.shiplog-newsletter__button--secondary]:text-gray-700 [&.shiplog-newsletter__button--secondary]:ring-1 [&.shiplog-newsletter__button--secondary]:ring-gray-300 dark:[&.shiplog-newsletter__button--secondary]:text-gray-300",
|
|
84
|
+
newsletter_notice: "mb-3 text-sm [&.shiplog-newsletter__notice--error]:text-red-600"
|
|
72
85
|
}.freeze
|
|
73
86
|
|
|
74
87
|
def copy_initializer
|
|
@@ -174,6 +174,56 @@ Shiplog.configure do |config|
|
|
|
174
174
|
# decide which URLs set the locale (e.g. a /es scope). The default locale is dropped from the list.
|
|
175
175
|
# config.locales = [] # e.g. [:es]
|
|
176
176
|
|
|
177
|
+
# Monthly announcements: a short summary of what shipped in a month, drafted by config.rewriter from the month's
|
|
178
|
+
# approved entries and the small changes that got none, and approved in the admin like any entry. Schedule
|
|
179
|
+
# Shiplog::DraftAnnouncementJob early each month (it drafts the previous month), or use the inbox's
|
|
180
|
+
# "Draft announcement" buttons. Where it is published:
|
|
181
|
+
# :page (default) Shiplog renders /changelog/2026-10, links it from the month's heading and adds it to the feed
|
|
182
|
+
# :external your app publishes it (on_announcement_approved below); the month heading links to announcement_url
|
|
183
|
+
# false no announcements
|
|
184
|
+
# config.announcements = :page
|
|
185
|
+
# config.announcement_url = ->(announcement, locale) { main_app.news_path(announcement.slug) }
|
|
186
|
+
|
|
187
|
+
# Called with the announcement once it is approved and translated into every configured locale, once per approval
|
|
188
|
+
# (also sent as the `announcement_approved.shiplog` notification). The announcement offers display_title(locale),
|
|
189
|
+
# to_markdown(locale), to_html(locale), to_text(locale) and public_url, to publish it elsewhere or email it.
|
|
190
|
+
# config.on_announcement_approved = ->(announcement) { NewsletterJob.perform_later(announcement) }
|
|
191
|
+
|
|
192
|
+
# Newsletter: the approved announcement emailed, in each recipient's language, to your users who opted in and to
|
|
193
|
+
# the visitors who subscribed through Shiplog's form (double opt-in, stored in shiplog_newsletter_subscribers).
|
|
194
|
+
# Sending is an explicit admin action on the announcement's page, once per recipient. Needs newsletter_from,
|
|
195
|
+
# newsletter_address and canonical_url. Schedule Shiplog::PurgeNewsletterJob weekly (unconfirmed addresses).
|
|
196
|
+
# config.newsletter = false
|
|
197
|
+
|
|
198
|
+
# Your users who opted in: a callable returning objects (or a relation, read in batches) responding to #email,
|
|
199
|
+
# #locale and #id. Shiplog never stores them. Resolve the locale yourself (the user's, then a default).
|
|
200
|
+
# config.newsletter_recipients = -> { User.where(newsletter: true) }
|
|
201
|
+
|
|
202
|
+
# Your own unsubscribe or settings page for those users, linked in the footer as "Preferences". The one-click
|
|
203
|
+
# unsubscribe of mail providers always comes to Shiplog, which calls on_newsletter_unsubscribe below.
|
|
204
|
+
# config.newsletter_unsubscribe_url = ->(user) { Rails.application.routes.url_helpers.settings_url(host: "example.com") }
|
|
205
|
+
|
|
206
|
+
# Called with the lowercased email on every unsubscribe (one-click, the footer link, the preferences page), so your
|
|
207
|
+
# own setting follows: Shiplog suppresses the address itself either way, and skips it at send time whatever your
|
|
208
|
+
# list says. When a user opts in again in your app, call Shiplog::Newsletter.resubscribe(user.email) to lift the
|
|
209
|
+
# suppression. Errors are logged, never shown to the visitor.
|
|
210
|
+
# config.on_newsletter_unsubscribe = ->(email) { User.where("lower(email) = ?", email).update_all(newsletter: false) }
|
|
211
|
+
|
|
212
|
+
# The sender (also of the confirmation email) and the postal address in the footer (CAN-SPAM requires one).
|
|
213
|
+
# config.newsletter_from = "Acme <updates@example.com>"
|
|
214
|
+
# config.newsletter_address = "Acme Inc., 123 Main Street, Springfield"
|
|
215
|
+
|
|
216
|
+
# Branding: the logo (an http(s) URL, shown 140px wide) and the accent colour of buttons and links.
|
|
217
|
+
# config.newsletter_logo_url = "https://example.com/logo.png"
|
|
218
|
+
# config.newsletter_accent = "#0969da"
|
|
219
|
+
|
|
220
|
+
# Merged into every newsletter message (not the confirmation email): ActionMailer options or headers your
|
|
221
|
+
# delivery gem reads. With Postmark, a broadcast stream keeps complaints away from your transactional email.
|
|
222
|
+
# config.newsletter_mailer_options = { message_stream: "broadcast" }
|
|
223
|
+
|
|
224
|
+
# How many recipients a run claims and emails at a time (Postmark's batch limit by default).
|
|
225
|
+
# config.newsletter_batch_size = 500
|
|
226
|
+
|
|
177
227
|
# Callables above cannot be part of the public pages' ETag. Bump this when the output of a callable you
|
|
178
228
|
# configured changes (a new label, another path) so cached pages are refreshed.
|
|
179
229
|
# config.cache_version = nil
|
data/lib/shiplog/classes.rb
CHANGED
|
@@ -58,7 +58,20 @@ module Shiplog
|
|
|
58
58
|
area_link: "shiplog-areas__link",
|
|
59
59
|
entry_area: "shiplog-entry__area",
|
|
60
60
|
entry_images: "shiplog-entry__images",
|
|
61
|
-
entry_image: "shiplog-entry__image"
|
|
61
|
+
entry_image: "shiplog-entry__image",
|
|
62
|
+
announcement: "shiplog-announcement",
|
|
63
|
+
announcement_title: "shiplog-announcement__title",
|
|
64
|
+
announcement_body: "shiplog-announcement__body",
|
|
65
|
+
month_announcement: "shiplog-month__announcement",
|
|
66
|
+
month_announcement_link: "shiplog-month__announcement-link",
|
|
67
|
+
newsletter: "shiplog-newsletter",
|
|
68
|
+
newsletter_title: "shiplog-newsletter__title",
|
|
69
|
+
newsletter_hint: "shiplog-newsletter__hint",
|
|
70
|
+
newsletter_form: "shiplog-newsletter__form",
|
|
71
|
+
newsletter_label: "shiplog-newsletter__label",
|
|
72
|
+
newsletter_input: "shiplog-newsletter__input",
|
|
73
|
+
newsletter_button: "shiplog-newsletter__button",
|
|
74
|
+
newsletter_notice: "shiplog-newsletter__notice"
|
|
62
75
|
}.freeze
|
|
63
76
|
|
|
64
77
|
KEYS = BEM.keys.freeze
|