shiplog 0.2.0 → 0.3.1

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 (86) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +74 -0
  3. data/README.md +172 -7
  4. data/app/controllers/shiplog/admin/area_detections_controller.rb +15 -0
  5. data/app/controllers/shiplog/admin/area_translations_controller.rb +19 -0
  6. data/app/controllers/shiplog/admin/areas/merges_controller.rb +19 -0
  7. data/app/controllers/shiplog/admin/areas/moves_controller.rb +13 -0
  8. data/app/controllers/shiplog/admin/areas_controller.rb +48 -0
  9. data/app/controllers/shiplog/admin/entries/images/moves_controller.rb +20 -0
  10. data/app/controllers/shiplog/admin/entries/images_controller.rb +76 -0
  11. data/app/controllers/shiplog/admin/entries/translations/retranslations_controller.rb +23 -0
  12. data/app/controllers/shiplog/admin/entries/translations_controller.rb +35 -0
  13. data/app/controllers/shiplog/admin/entries_controller.rb +9 -3
  14. data/app/controllers/shiplog/changelog_entries_controller.rb +26 -0
  15. data/app/controllers/shiplog/changelog_feeds_controller.rb +19 -0
  16. data/app/controllers/shiplog/changelogs_controller.rb +21 -5
  17. data/app/controllers/shiplog/public_controller.rb +57 -0
  18. data/app/controllers/shiplog/roadmaps_controller.rb +3 -1
  19. data/app/frontend/shiplog/admin.css +9 -0
  20. data/app/frontend/shiplog/shiplog.css +47 -2
  21. data/app/helpers/shiplog/admin/entries_helper.rb +15 -0
  22. data/app/helpers/shiplog/head_helper.rb +95 -0
  23. data/app/helpers/shiplog/head_tags_helper.rb +24 -0
  24. data/app/helpers/shiplog/permalink_helper.rb +149 -0
  25. data/app/helpers/shiplog/roadmap_helper.rb +9 -0
  26. data/app/helpers/shiplog/structured_data_helper.rb +74 -0
  27. data/app/helpers/shiplog/unseen_helper.rb +9 -0
  28. data/app/jobs/shiplog/detect_areas_job.rb +18 -0
  29. data/app/jobs/shiplog/rewrite_entry_job.rb +4 -1
  30. data/app/jobs/shiplog/translate_area_job.rb +25 -0
  31. data/app/jobs/shiplog/translate_entry_job.rb +25 -0
  32. data/app/models/shiplog/area.rb +156 -0
  33. data/app/models/shiplog/area_detector.rb +146 -0
  34. data/app/models/shiplog/area_translator.rb +77 -0
  35. data/app/models/shiplog/entry/images.rb +127 -0
  36. data/app/models/shiplog/entry.rb +204 -5
  37. data/app/models/shiplog/entry_translation.rb +15 -0
  38. data/app/models/shiplog/image_fetcher/downloader.rb +98 -0
  39. data/app/models/shiplog/image_fetcher/sources.rb +41 -0
  40. data/app/models/shiplog/image_fetcher.rb +72 -0
  41. data/app/models/shiplog/image_type.rb +111 -0
  42. data/app/models/shiplog/rewriter/prompt.rb +31 -7
  43. data/app/models/shiplog/rewriter.rb +41 -23
  44. data/app/models/shiplog/setting.rb +19 -0
  45. data/app/models/shiplog/translator/prompt.rb +40 -0
  46. data/app/models/shiplog/translator.rb +99 -0
  47. data/app/views/layouts/shiplog/application.html.erb +1 -1
  48. data/app/views/shiplog/_empty.html.erb +1 -1
  49. data/app/views/shiplog/_head_tags.html.erb +5 -0
  50. data/app/views/shiplog/_pagination.html.erb +3 -3
  51. data/app/views/shiplog/admin/_nav.html.erb +2 -1
  52. data/app/views/shiplog/admin/areas/index.html.erb +78 -0
  53. data/app/views/shiplog/admin/entries/_entry_row.html.erb +3 -0
  54. data/app/views/shiplog/admin/entries/_images.html.erb +41 -0
  55. data/app/views/shiplog/admin/entries/_translations.html.erb +42 -0
  56. data/app/views/shiplog/admin/entries/edit.html.erb +12 -0
  57. data/app/views/shiplog/changelog_entries/show.html.erb +23 -0
  58. data/app/views/shiplog/changelog_feeds/show.atom.builder +24 -0
  59. data/app/views/shiplog/changelogs/_areas.html.erb +8 -0
  60. data/app/views/shiplog/changelogs/_entry.html.erb +2 -5
  61. data/app/views/shiplog/changelogs/_entry_content.html.erb +10 -0
  62. data/app/views/shiplog/changelogs/_search.html.erb +4 -2
  63. data/app/views/shiplog/changelogs/show.html.erb +13 -7
  64. data/app/views/shiplog/head/_feed.html.erb +1 -0
  65. data/app/views/shiplog/head/_meta.html.erb +3 -0
  66. data/app/views/shiplog/head/_open_graph.html.erb +6 -0
  67. data/app/views/shiplog/head/_structured_data.html.erb +1 -0
  68. data/app/views/shiplog/roadmaps/_card.html.erb +2 -2
  69. data/app/views/shiplog/roadmaps/_column.html.erb +3 -3
  70. data/app/views/shiplog/roadmaps/_shipped.html.erb +1 -1
  71. data/app/views/shiplog/roadmaps/show.html.erb +5 -2
  72. data/config/admin_routes.rb +16 -0
  73. data/config/locales/en.yml +85 -0
  74. data/config/locales/es.yml +223 -0
  75. data/config/routes.rb +5 -0
  76. data/db/migrate/20261027000002_shiplog_0_3_0.rb +105 -0
  77. data/lib/generators/shiplog/install/install_generator.rb +10 -1
  78. data/lib/generators/shiplog/install/templates/initializer.rb.tt +58 -1
  79. data/lib/shiplog/classes.rb +10 -1
  80. data/lib/shiplog/configuration.rb +105 -3
  81. data/lib/shiplog/engine.rb +8 -0
  82. data/lib/shiplog/github/queries.rb +13 -1
  83. data/lib/shiplog/version.rb +1 -1
  84. data/lib/shiplog.rb +73 -0
  85. data/lib/tasks/shiplog.rake +29 -0
  86. metadata +47 -1
@@ -2,6 +2,9 @@
2
2
  <td>
3
3
  <%= link_to entry_heading(entry), edit_entry_path(entry, inbox_filter_params) %>
4
4
  <%= render "shiplog/admin/entries/badges", entry: %>
5
+ <% if entry.area %>
6
+ <span class="shiplog-badge shiplog-badge--area"><%= entry.area.name %></span>
7
+ <% end %>
5
8
  <% if entry.discarded? && entry.discard_reason.present? %>
6
9
  <div class="shiplog-admin__muted"><%= entry.discard_reason %></div>
7
10
  <% end %>
@@ -0,0 +1,41 @@
1
+ <%# locals: (entry:) %>
2
+ <section class="shiplog-admin-panel shiplog-admin-images">
3
+ <h2><%= t("shiplog.admin.entries.images.title", default: "Screenshots") %></h2>
4
+ <p class="shiplog-admin__muted"><%= t("shiplog.admin.entries.images.help", default: "Shown under the text on the changelog (the first one in the list, all of them on the entry's page). Taken from the GitHub issue or pull request when the draft is written; you can also upload your own.") %></p>
5
+ <% images = entry.ordered_images %>
6
+ <% if images.empty? %>
7
+ <p class="shiplog-admin__muted"><%= t("shiplog.admin.entries.images.empty", default: "No screenshots yet.") %></p>
8
+ <% else %>
9
+ <ol class="shiplog-admin-images__list">
10
+ <% images.each_with_index do |image, index| %>
11
+ <li class="shiplog-admin-images__item">
12
+ <% thumbnail = image_thumbnail_path(image) %>
13
+ <%= tag.img(src: thumbnail, alt: entry.image_alt(image), class: "shiplog-admin-images__thumb", loading: "lazy") if thumbnail %>
14
+ <div class="shiplog-admin-images__controls">
15
+ <%= form_with url: entry_image_path(entry, image.blob_id, inbox_filter_params), method: :patch, class: "shiplog-admin-inline-form" do |form| %>
16
+ <%= form.label :alt, t("shiplog.admin.entries.images.alt", default: "Alt text"), for: "alt_#{image.blob_id}" %>
17
+ <%= form.text_field :alt, id: "alt_#{image.blob_id}", value: entry.custom_image_alt(image), maxlength: Shiplog::Entry::Images::ALT_MAX_LENGTH, placeholder: t("shiplog.admin.entries.images.alt_placeholder", default: "Describe the image (defaults to the title)") %>
18
+ <%= form.submit t("shiplog.admin.entries.images.save_alt", default: "Save alt text"), class: "shiplog-button" %>
19
+ <% end %>
20
+ <div class="shiplog-admin-inline-form">
21
+ <% if index.positive? %>
22
+ <%= button_to t("shiplog.admin.entries.images.move_up", default: "Up"), entry_image_move_path(entry, image.blob_id, inbox_filter_params.merge(direction: "up")), class: "shiplog-button" %>
23
+ <% end %>
24
+ <% if index < images.size - 1 %>
25
+ <%= button_to t("shiplog.admin.entries.images.move_down", default: "Down"), entry_image_move_path(entry, image.blob_id, inbox_filter_params.merge(direction: "down")), class: "shiplog-button" %>
26
+ <% end %>
27
+ <%= button_to t("shiplog.admin.entries.images.remove", default: "Remove"), entry_image_path(entry, image.blob_id, inbox_filter_params), method: :delete, class: "shiplog-button" %>
28
+ </div>
29
+ </div>
30
+ </li>
31
+ <% end %>
32
+ </ol>
33
+ <% end %>
34
+ <%= form_with url: entry_images_path(entry, inbox_filter_params), multipart: true, class: "shiplog-admin-form" do |form| %>
35
+ <p>
36
+ <%= form.label :images, t("shiplog.admin.entries.images.upload_label", default: "Images (PNG, JPEG, GIF or WebP, up to 5 MB each)") %>
37
+ <%= form.file_field :images, multiple: true, name: "images[]", accept: Shiplog::ImageType::EXTENSIONS.keys.join(",") %>
38
+ </p>
39
+ <%= form.submit t("shiplog.admin.entries.images.upload", default: "Upload images"), class: "shiplog-button" %>
40
+ <% end %>
41
+ </section>
@@ -0,0 +1,42 @@
1
+ <%# locals: (entry:) %>
2
+ <%# one editable title and body per configured locale (config.locales), with its state: missing, stale (made from older English text), edited by an admin, or up to date. Needs Shiplog::Admin::EntriesHelper. %>
3
+ <% translations = entry.translations.index_by(&:locale) %>
4
+ <section class="shiplog-admin-panel shiplog-translations">
5
+ <h2><%= t("shiplog.admin.entries.translations.title", default: "Translations") %></h2>
6
+ <p class="shiplog-admin__muted"><%= t("shiplog.admin.entries.translations.help", default: "Approving an entry translates it automatically, and the translation is published with the English text. A translation you edit is kept until the English text changes; then it is translated again. A stale or missing translation is not shown: visitors see the English text.") %></p>
7
+ <% unless entry.approved? %>
8
+ <p class="shiplog-admin__muted"><%= t("shiplog.admin.entries.translations.not_approved", default: "Translations are written when the entry is approved.") %></p>
9
+ <% end %>
10
+ <% Shiplog.config.locales.each do |locale| %>
11
+ <% translation = translations[locale.to_s] %>
12
+ <div class="shiplog-translation" data-locale="<%= locale %>">
13
+ <h3>
14
+ <%= translation_language_label(locale) %>
15
+ <% if translation.nil? %>
16
+ <span class="shiplog-badge"><%= t("shiplog.admin.entries.translations.missing", default: "Missing") %></span>
17
+ <% elsif translation.stale? %>
18
+ <span class="shiplog-badge"><%= t("shiplog.admin.entries.translations.stale", default: "Stale") %></span>
19
+ <% else %>
20
+ <span class="shiplog-badge"><%= t("shiplog.admin.entries.translations.current", default: "Up to date") %></span>
21
+ <% end %>
22
+ <% if translation&.edited_by_admin %>
23
+ <span class="shiplog-badge"><%= t("shiplog.admin.entries.translations.edited", default: "Edited by an admin") %></span>
24
+ <% end %>
25
+ </h3>
26
+ <%= form_with url: entry_translation_path(entry, inbox_filter_params.merge(translation_locale: locale)), scope: :translation, method: :patch, class: "shiplog-admin-form" do |form| %>
27
+ <p>
28
+ <%= form.label :title, t("shiplog.admin.entries.fields.title"), for: "translation_#{locale}_title" %>
29
+ <%= form.text_field :title, value: translation&.title, id: "translation_#{locale}_title", lang: locale %>
30
+ </p>
31
+ <p>
32
+ <%= form.label :body, t("shiplog.admin.entries.fields.body"), for: "translation_#{locale}_body" %>
33
+ <%= form.text_area :body, value: translation&.body, rows: 5, id: "translation_#{locale}_body", lang: locale %>
34
+ </p>
35
+ <%= form.submit t("shiplog.admin.entries.save"), class: "shiplog-button shiplog-button--primary" %>
36
+ <% end %>
37
+ <% if entry.approved? %>
38
+ <%= button_to t("shiplog.admin.entries.translations.retranslate", default: "Retranslate"), entry_translation_retranslation_path(entry, inbox_filter_params.merge(translation_locale: locale)), class: "shiplog-button" %>
39
+ <% end %>
40
+ </div>
41
+ <% end %>
42
+ </section>
@@ -28,6 +28,10 @@
28
28
  <%= form.select :changelog_kind, Shiplog::Entry::CHANGELOG_KINDS.map { |kind| [ t("shiplog.admin.entries.category_options.#{kind}"), kind ] } %>
29
29
  </p>
30
30
  <% end %>
31
+ <p>
32
+ <%= form.label :area_id, t("shiplog.admin.entries.fields.area", default: "Area") %>
33
+ <%= form.collection_select :area_id, Shiplog::Area.ordered, :id, :name, include_blank: t("shiplog.admin.entries.no_area", default: "None") %>
34
+ </p>
31
35
  <p>
32
36
  <%= form.label :released_on, t("shiplog.admin.entries.fields.released_on") %>
33
37
  <%= form.date_field :released_on %>
@@ -63,5 +67,13 @@
63
67
  </aside>
64
68
  </div>
65
69
 
70
+ <% if Shiplog.config.locales.any? %>
71
+ <%= render "shiplog/admin/entries/translations", entry: @entry %>
72
+ <% end %>
73
+
74
+ <% if Shiplog.images? %>
75
+ <%= render "shiplog/admin/entries/images", entry: @entry %>
76
+ <% end %>
77
+
66
78
  <p><%= link_to t("shiplog.admin.entries.back"), entries_filter_path(**inbox_filter_params) %></p>
67
79
  <% end %>
@@ -0,0 +1,23 @@
1
+ <% content_for :title, t("shiplog.permalink.title", title: @entry.display_title, changelog: t("shiplog.changelog.title", default: "Changelog"), default: "%{title} · %{changelog}") %>
2
+ <% shiplog_head_page :permalink, title: @entry.display_title, entry: @entry, url: shiplog_entry_url(@entry), json_ld: shiplog_article_json_ld(@entry) %>
3
+ <%= render layout: "shiplog/page", locals: { name: "permalink" } do %>
4
+ <%= render "shiplog/page_header", title: t("shiplog.changelog.title", default: "Changelog"), lede: nil %>
5
+ <article class="<%= shiplog_class(:permalink, @entry.kind) %>">
6
+ <%= render "shiplog/changelogs/entry_content", entry: @entry, heading: "h2" %>
7
+ </article>
8
+ <%= link_to t("shiplog.permalink.back", default: "← Back to the changelog"), changelog_path(anchor: shiplog_month_anchor(@entry.released_on)), class: shiplog_class(:permalink_back) %>
9
+ <% if @newer || @older %>
10
+ <nav class="<%= shiplog_class(:permalink_nav) %>" aria-label="<%= t("shiplog.permalink.nav_label", default: "More changelog entries") %>">
11
+ <% if @newer %>
12
+ <%= shiplog_entry_link @newer, class: shiplog_class(:permalink_nav_link, :newer), rel: "prev" do %>
13
+ <%= t("shiplog.permalink.newer", title: @newer.display_title, default: "← Newer: %{title}") %>
14
+ <% end %>
15
+ <% end %>
16
+ <% if @older %>
17
+ <%= shiplog_entry_link @older, class: shiplog_class(:permalink_nav_link, :older), rel: "next" do %>
18
+ <%= t("shiplog.permalink.older", title: @older.display_title, default: "Older: %{title} →") %>
19
+ <% end %>
20
+ <% end %>
21
+ </nav>
22
+ <% end %>
23
+ <% end %>
@@ -0,0 +1,24 @@
1
+ # The latest published features and changes. Entry ids are tag URIs built from the permalink path, so they
2
+ # stay the same when a title changes.
3
+ title = Shiplog.config.product_name.present? ? t("shiplog.feed.title_with_product", product: Shiplog.config.product_name, default: "%{product} changelog") : t("shiplog.changelog.title", default: "Changelog")
4
+
5
+ atom_feed(language: I18n.locale.to_s, url: shiplog_absolute_url(:changelog_feed), root_url: shiplog_absolute_url(:changelog),
6
+ id: "tag:#{shiplog_tag_host},2005:#{changelog_path}") do |feed|
7
+ feed.title(title)
8
+ feed.author { |author| author.name(Shiplog.config.product_name.presence || "Changelog") } # RFC 4287 4.1.1: the feed or each entry needs one
9
+ # An entry is updated when its text or the translation shown in this feed changes.
10
+ updated_at = ->(entry) { [ entry.updated_at, entry.translation&.updated_at ].compact.max }
11
+ feed.updated(@entries.map(&updated_at).max || Time.current)
12
+
13
+ # Under a translated locale, an entry without a current translation is in English (the default locale): say so.
14
+ english = ->(entry) { Shiplog::Entry.translated_locale?(I18n.locale) && entry.translation.nil? ? { "xml:lang" => I18n.default_locale.to_s } : {} }
15
+
16
+ @entries.each do |entry|
17
+ feed.entry(entry, id: "tag:#{shiplog_tag_host},2005:#{shiplog_entry_path(entry)}", url: shiplog_entry_url(entry),
18
+ published: entry.released_on.in_time_zone, updated: updated_at.(entry)) do |item|
19
+ item.title(entry.display_title.to_s, english.(entry))
20
+ item.category(term: entry.area.slug, label: entry.area.display_name) if entry.area
21
+ item.content(shiplog_feed_content(entry), { type: "html" }.merge(english.(entry)))
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,8 @@
1
+ <%# locals: (areas:, current: nil, query: nil) %>
2
+ <%# the area filter: an "All" link and one link per area (GET ?area=slug, works without JavaScript); the search query is kept %>
3
+ <nav class="<%= shiplog_class(:areas) %>" aria-label="<%= t("shiplog.changelog.areas.label", default: "Filter by area") %>">
4
+ <%= link_to t("shiplog.changelog.areas.all", default: "All"), changelog_path(q: query), class: shiplog_class(:area_link, ("current" unless current)), "aria-current": ("page" unless current) %>
5
+ <% areas.each do |area| %>
6
+ <%= link_to area.display_name, changelog_path(area: area.slug, q: query), class: shiplog_class(:area_link, ("current" if area == current)), "aria-current": ("page" if area == current) %>
7
+ <% end %>
8
+ </nav>
@@ -1,8 +1,5 @@
1
1
  <%# locals: (entry:) %>
2
- <%# one changelog entry: category pill ("New" for features, "Improved" for changes), date, title and body %>
2
+ <%# one changelog entry in the list; the entry itself is the entry_content partial, shared with the permalink page %>
3
3
  <li class="<%= token_list(shiplog_class(:entry, entry.kind), (shiplog_class(:entry_feature) if entry.feature?)) %>">
4
- <span class="<%= shiplog_class(:entry_category) %>"><%= t("shiplog.categories.#{entry.kind}") %></span>
5
- <time class="<%= shiplog_class(:entry_date) %>" datetime="<%= entry.released_on.iso8601 %>"><%= l(entry.released_on, format: t("shiplog.date_format", default: "%-d %B %Y")) %></time>
6
- <h3 class="<%= shiplog_class(:entry_title) %>"><%= entry.title %></h3>
7
- <div class="<%= shiplog_class(:entry_body) %>"><%= simple_format(h(entry.body)) %></div>
4
+ <%= render "shiplog/changelogs/entry_content", entry:, heading: "h3", linked: true %>
8
5
  </li>
@@ -0,0 +1,10 @@
1
+ <%# locals: (entry:, heading: "h3", linked: false, all_images: !linked) %>
2
+ <%# what an entry shows: category pill ("New" for features, "Improved" for changes), date, title (a permalink link in the list) and body. The area tag (features and changes only) follows the category. The images follow the body: the first one in the list, all of them on the permalink page. %>
3
+ <span class="<%= shiplog_class(:entry_category) %>"><%= t("shiplog.categories.#{entry.kind}", default: (entry.feature? ? "New" : "Improved")) %></span>
4
+ <% if entry.area && Shiplog::Entry::CHANGELOG_KINDS.include?(entry.kind) %>
5
+ <span class="<%= shiplog_class(:entry_area) %>"><%= entry.area.display_name %></span>
6
+ <% end %>
7
+ <time class="<%= shiplog_class(:entry_date) %>" datetime="<%= entry.released_on.iso8601 %>"><%= l(entry.released_on, format: t("shiplog.date_format", default: "%-d %B %Y")) %></time>
8
+ <%= content_tag heading, (linked && shiplog_entry_path(entry) ? link_to(entry.display_title, shiplog_entry_path(entry)) : entry.display_title), class: shiplog_class(:entry_title) %>
9
+ <div class="<%= shiplog_class(:entry_body) %>"><%= simple_format(h(entry.display_body)) %></div>
10
+ <%= shiplog_entry_images(entry, limit: (all_images ? nil : 1)) %>
@@ -1,8 +1,10 @@
1
1
  <%# locals: (query: nil, count: nil) %>
2
- <%# the changelog search form (GET, works without JavaScript); with a query, also the result summary and a link back to the full changelog %>
2
+ <%# the selected area is read from @area, not a local: a copy of this partial from 0.2.0 declares only query and count %>
3
+ <%# the changelog search form (GET, works without JavaScript); with a query, also the result summary and a link back to the changelog (of the same area, when one is selected) %>
3
4
  <% label = t("shiplog.changelog.search.label", default: "Search the changelog") %>
4
5
  <form class="<%= shiplog_class(:search) %>" role="search" action="<%= changelog_path %>" method="get">
5
6
  <input class="<%= shiplog_class(:search_input) %>" type="search" name="q" value="<%= query %>" aria-label="<%= label %>" placeholder="<%= label %>" maxlength="<%= Shiplog::Entry::SEARCH_MAX_LENGTH %>">
7
+ <% if @area %><input type="hidden" name="area" value="<%= @area.slug %>"><% end %>
6
8
  <button class="<%= shiplog_class(:search_button) %>" type="submit"><%= t("shiplog.changelog.search.submit", default: "Search") %></button>
7
9
  </form>
8
10
  <% if query %>
@@ -11,6 +13,6 @@
11
13
  <% if count.to_i.positive? %>
12
14
  <%= t("shiplog.changelog.search.summary", count: count, query: query, default: { one: "%{count} result for “%{query}”", other: "%{count} results for “%{query}”" }) %>
13
15
  <% end %>
14
- <%= link_to t("shiplog.changelog.search.clear", default: "Clear search"), changelog_path, class: shiplog_class(:search_clear) %>
16
+ <%= link_to t("shiplog.changelog.search.clear", default: "Clear search"), changelog_path(area: @area&.slug), class: shiplog_class(:search_clear) %>
15
17
  </p>
16
18
  <% end %>
@@ -1,12 +1,15 @@
1
- <% content_for :title, t("shiplog.changelog.title") %>
1
+ <% changelog_title = t("shiplog.changelog.title", default: "Changelog") %>
2
+ <% list_url = shiplog_absolute_url(:changelog, area: @area&.slug, page: (@page if @page > 1)) %>
3
+ <% content_for :title, changelog_title %>
4
+ <% shiplog_head_page :changelog, title: changelog_title, url: list_url, query: @query, json_ld: shiplog_changelog_json_ld(@months.values.flatten, name: changelog_title, url: list_url) %>
2
5
  <%= render layout: "shiplog/page", locals: { name: "changelog" } do %>
3
- <%= render "shiplog/page_header", title: t("shiplog.changelog.title"), lede: t("shiplog.changelog.lede") %>
4
- <% if @query %>
5
- <% content_for :shiplog_robots, "noindex" %>
6
- <% end %>
6
+ <%= render "shiplog/page_header", title: t("shiplog.changelog.title", default: "Changelog"), lede: t("shiplog.changelog.lede", default: "New features and improvements, as they ship.") %>
7
7
  <% if Shiplog.config.changelog_search %>
8
8
  <%= render "shiplog/changelogs/search", query: @query, count: @result_count %>
9
9
  <% end %>
10
+ <% if @areas.any? %>
11
+ <%= render "shiplog/changelogs/areas", areas: @areas, current: @area, query: @query %>
12
+ <% end %>
10
13
  <% if @months.empty? && @query %>
11
14
  <p class="<%= shiplog_class(:search_empty) %>"><%= t("shiplog.changelog.search.empty", query: @query, default: "No entries match “%{query}”.") %></p>
12
15
  <% elsif @months.empty? %>
@@ -19,6 +22,9 @@
19
22
  </div>
20
23
  <% end %>
21
24
  <%= render "shiplog/pagination",
22
- newer_path: (changelog_path(page: (@page - 1 if @page > 2), q: @query) if @page > 1),
23
- older_path: (changelog_path(page: @page + 1, q: @query) if @older) %>
25
+ newer_path: (changelog_path(area: @area&.slug, page: (@page - 1 if @page > 2), q: @query) if @page > 1),
26
+ older_path: (changelog_path(area: @area&.slug, page: @page + 1, q: @query) if @older) %>
27
+ <p class="<%= shiplog_class(:footnote) %>">
28
+ <%= link_to t("shiplog.feed.subscribe", default: "Subscribe (Atom)"), changelog_feed_path, class: shiplog_class(:footnote_link, :feed) %>
29
+ </p>
24
30
  <% end %>
@@ -0,0 +1 @@
1
+ <%= auto_discovery_link_tag(:atom, feed_url, title: feed_title) if feed_url.present? %>
@@ -0,0 +1,3 @@
1
+ <%= tag.meta(name: "description", content: description) if description.present? %>
2
+ <%= tag.link(rel: "canonical", href: canonical) if canonical.present? %>
3
+ <%= tag.meta(name: "robots", content: robots) if robots.present? %>
@@ -0,0 +1,6 @@
1
+ <% open_graph.each do |property, content| %>
2
+ <%= tag.meta(property:, content:) %>
3
+ <% end %>
4
+ <% twitter.each do |name, content| %>
5
+ <%= tag.meta(name:, content:) %>
6
+ <% end %>
@@ -0,0 +1 @@
1
+ <%= json_ld if json_ld.present? %>
@@ -6,8 +6,8 @@
6
6
  parts = issue.children.size
7
7
  %>
8
8
  <li class="<%= shiplog_class(:card) %>">
9
- <h3 class="<%= shiplog_class(:card_title) %>"><%= entry.title %></h3>
10
- <div class="<%= shiplog_class(:card_body) %>"><%= simple_format(h(entry.body)) %></div>
9
+ <h3 class="<%= shiplog_class(:card_title) %>"><%= entry.display_title %></h3>
10
+ <div class="<%= shiplog_class(:card_body) %>"><%= simple_format(h(entry.display_body)) %></div>
11
11
  <% if tag_label || parts > 1 %>
12
12
  <p class="<%= shiplog_class(:card_meta) %>">
13
13
  <% if tag_label %><span class="<%= shiplog_class(:card_tag) %>"><%= tag_label %></span><% end %>
@@ -3,12 +3,12 @@
3
3
  <% heading_id = "shiplog-column-#{column.dasherize}" %>
4
4
  <section class="<%= shiplog_class(:column, column) %>" aria-labelledby="<%= heading_id %>">
5
5
  <header class="<%= shiplog_class(:column_header) %>">
6
- <h2 class="<%= shiplog_class(:column_title) %>" id="<%= heading_id %>"><span class="<%= shiplog_class(:column_marker, column) %>" aria-hidden="true"></span><%= t("shiplog.columns.#{column}") %></h2>
6
+ <h2 class="<%= shiplog_class(:column_title) %>" id="<%= heading_id %>"><span class="<%= shiplog_class(:column_marker, column) %>" aria-hidden="true"></span><%= t("shiplog.columns.#{column}", default: shiplog_column_defaults.fetch(column.to_sym, {})[:title] || column.humanize) %></h2>
7
7
  <span class="<%= shiplog_class(:column_count) %>"><%= entries.size %></span>
8
- <p class="<%= shiplog_class(:column_description) %>"><%= t("shiplog.columns.#{column}_description") %></p>
8
+ <p class="<%= shiplog_class(:column_description) %>"><%= t("shiplog.columns.#{column}_description", default: shiplog_column_defaults.fetch(column.to_sym, {})[:description].to_s) %></p>
9
9
  </header>
10
10
  <% if entries.empty? %>
11
- <p class="<%= shiplog_class(:column_empty) %>"><%= t("shiplog.empty") %></p>
11
+ <p class="<%= shiplog_class(:column_empty) %>"><%= t("shiplog.empty", default: "Nothing here yet") %></p>
12
12
  <% else %>
13
13
  <ol class="<%= shiplog_class(:cards) %>">
14
14
  <%= render partial: "shiplog/roadmaps/card", collection: entries, as: :entry %>
@@ -8,7 +8,7 @@
8
8
  <% entries.each do |entry| %>
9
9
  <li class="<%= shiplog_class(:shipped_item) %>">
10
10
  <time class="<%= shiplog_class(:shipped_date) %>" datetime="<%= entry.released_on.iso8601 %>"><%= l(entry.released_on, format: t("shiplog.roadmap.shipped_date_format", default: "%-d %B")) %></time>
11
- <%= link_to entry.title, changelog_path(anchor: "shiplog-month-#{entry.released_on.strftime("%Y-%m")}"), class: shiplog_class(:shipped_link) %>
11
+ <%= link_to entry.display_title, changelog_path(anchor: "shiplog-month-#{entry.released_on.strftime("%Y-%m")}"), class: shiplog_class(:shipped_link) %>
12
12
  </li>
13
13
  <% end %>
14
14
  </ul>
@@ -1,6 +1,9 @@
1
- <% content_for :title, t("shiplog.roadmap.title") %>
1
+ <% roadmap_title = t("shiplog.roadmap.title", default: "Roadmap") %>
2
+ <% roadmap_url = shiplog_absolute_url(:roadmap) %>
3
+ <% content_for :title, roadmap_title %>
4
+ <% shiplog_head_page :roadmap, title: roadmap_title, url: roadmap_url, feed: false, json_ld: shiplog_roadmap_json_ld(@columns.values.flatten.map(&:entry), name: roadmap_title, url: roadmap_url) %>
2
5
  <%= render layout: "shiplog/page", locals: { name: "roadmap" } do %>
3
- <%= render "shiplog/page_header", title: t("shiplog.roadmap.title"), lede: t("shiplog.roadmap.lede") %>
6
+ <%= render "shiplog/page_header", title: t("shiplog.roadmap.title", default: "Roadmap"), lede: t("shiplog.roadmap.lede", default: "What we're working on and what comes next.") %>
4
7
  <div class="<%= shiplog_class(:columns) %>">
5
8
  <% @columns.each do |column, items| %>
6
9
  <%= render "shiplog/roadmaps/column", column:, entries: items.map(&:entry) %>
@@ -5,6 +5,22 @@ 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
+ # The locale segment is :translation_locale, not :locale: a host's around_action that switches the locale
9
+ # from params[:locale] must not read it (a configured translation locale may not be an available one).
10
+ locale = { translation_locale: /[A-Za-z][A-Za-z0-9_-]*/ }
11
+ patch "translations/:translation_locale", to: "translations#update", as: :translation, constraints: locale
12
+ post "translations/:translation_locale/retranslation", to: "translations/retranslations#create", as: :translation_retranslation, constraints: locale
13
+ resources :images, only: %i[create update destroy] do
14
+ resource :move, only: :create, module: :images
15
+ end
16
+ end
17
+ end
18
+ resource :area_detection, only: :create, path: "areas/detection"
19
+ resource :area_translation, only: :create, path: "areas/translation"
20
+ resources :areas, only: %i[index update destroy] do
21
+ scope module: :areas do
22
+ resource :merge, only: :create
23
+ resource :move, only: :create
8
24
  end
9
25
  end
10
26
  resource :board, only: %i[show update]
@@ -46,12 +46,61 @@ en:
46
46
  other: "%{count} results for “%{query}”"
47
47
  clear: Clear search
48
48
  empty: "No entries match “%{query}”."
49
+ areas:
50
+ label: Filter by area
51
+ all: All
52
+ permalink:
53
+ title: "%{title} · %{changelog}"
54
+ back: "← Back to the changelog"
55
+ nav_label: More changelog entries
56
+ newer: "← Newer: %{title}"
57
+ older: "Older: %{title} →"
58
+ feed:
59
+ title_with_product: "%{product} changelog"
60
+ link_title: Changelog (Atom)
61
+ subscribe: Subscribe (Atom)
49
62
  admin:
50
63
  title: Shiplog admin
51
64
  nav:
52
65
  drafts: Drafts
53
66
  board: Roadmap board
54
67
  sync: Sync
68
+ areas: Areas
69
+ areas:
70
+ title: Product areas
71
+ 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.
72
+ detect: Detect areas
73
+ recheck: Re-check unplaced entries
74
+ recheck_help: Ask the AI again about entries it left without an area, e.g. after renaming, merging or adding areas.
75
+ no_rewriter: Detection needs config.rewriter, which is not set.
76
+ last_detection: "Last detection: %{time}, assigned %{count}"
77
+ last_error: "Last detection failed (%{time}): %{error}"
78
+ detection_queued: Detection queued. Refresh in a moment.
79
+ translate_names: Translate names
80
+ translate_names_help: Ask the AI for the names that are missing in some language. Names that are already there, including ones you edited, are kept.
81
+ no_locales: Translating names needs config.locales, which is empty.
82
+ no_rewriter_translate: Translating names needs config.rewriter, which is not set.
83
+ translation_queued:
84
+ one: Translation queued for %{count} area. Refresh in a moment.
85
+ other: Translation queued for %{count} areas. Refresh in a moment.
86
+ empty: No areas yet. Detect areas to propose some from your entries.
87
+ columns:
88
+ name: Name
89
+ entries: Entries
90
+ order: Order
91
+ merge: Merge into
92
+ name_label: Name
93
+ rename: Rename
94
+ renamed: Area renamed.
95
+ move_up: Move up
96
+ move_down: Move down
97
+ merge: Merge
98
+ merge_into: Merge into
99
+ merged: "%{from} merged into %{into}."
100
+ merge_target: Choose another area to merge into.
101
+ merge_help: Merging moves the entries to the other area and deletes this one. Deleting an area leaves its entries without one.
102
+ delete: Delete
103
+ deleted: Area deleted. Its entries have no area now.
55
104
  entries:
56
105
  title: Entries
57
106
  filters: Filter entries
@@ -73,6 +122,8 @@ en:
73
122
  restore: Restore
74
123
  save: Save
75
124
  regenerate: Regenerate
125
+ no_area: None
126
+ area_gone: no longer exists
76
127
  regenerate_warning: Regenerating unpublishes this entry until re-approved.
77
128
  source_changed: GitHub text changed since this draft
78
129
  rewrite_error: Rewrite failed
@@ -80,6 +131,39 @@ en:
80
131
  public_text: Public text
81
132
  source: GitHub source
82
133
  back: Back to drafts
134
+ images:
135
+ title: Screenshots
136
+ help: Shown under the text on the changelog (the first one in the list, all of them on the entry's page). Taken from the GitHub issue or pull request when the draft is written; you can also upload your own.
137
+ empty: No screenshots yet.
138
+ alt: Alt text
139
+ alt_placeholder: Describe the image (defaults to the title)
140
+ save_alt: Save alt text
141
+ remove: Remove
142
+ move_up: Up
143
+ move_down: Down
144
+ upload: Upload images
145
+ upload_label: Images (PNG, JPEG, GIF or WebP, up to 5 MB each)
146
+ none_selected: Choose at least one image.
147
+ rejected: "Not added, only PNG, JPEG, GIF and WebP images up to 5 MB and 10,000 pixels a side are accepted: %{names}"
148
+ added:
149
+ one: Image added.
150
+ other: "%{count} images added."
151
+ alt_saved: Alt text saved.
152
+ removed: Image removed.
153
+ moved: Image moved.
154
+ translations:
155
+ title: Translations
156
+ help: "Approving an entry translates it automatically, and the translation is published with the English text. A translation you edit is kept until the English text changes; then it is translated again. A stale or missing translation is not shown: visitors see the English text."
157
+ not_approved: Translations are written when the entry is approved.
158
+ missing: Missing
159
+ stale: Stale
160
+ current: Up to date
161
+ edited: Edited by an admin
162
+ retranslate: Retranslate
163
+ saved: Translation saved.
164
+ queued: Translation queued. Refresh in a moment.
165
+ only_approved: Only approved entries are translated.
166
+ no_rewriter: Translating needs config.rewriter, which is not set.
83
167
  category_options:
84
168
  feature: New feature
85
169
  change: Improvement
@@ -101,6 +185,7 @@ en:
101
185
  body: Text
102
186
  released_on: Released on
103
187
  category: Category
188
+ area: Area
104
189
  reason: Reason
105
190
  board:
106
191
  title: Roadmap board