plutonium 0.64.0 → 0.65.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 (278) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/plutonium/SKILL.md +4 -1
  3. data/.claude/skills/plutonium-app/SKILL.md +14 -0
  4. data/.claude/skills/plutonium-dashboard/SKILL.md +161 -0
  5. data/.claude/skills/plutonium-resource/SKILL.md +94 -14
  6. data/.claude/skills/plutonium-ui/SKILL.md +22 -1
  7. data/CHANGELOG.md +59 -0
  8. data/CLAUDE.md +282 -210
  9. data/SECURITY.md +1 -1
  10. data/app/assets/plutonium-charts.js +20604 -0
  11. data/app/assets/plutonium-charts.js.map +7 -0
  12. data/app/assets/plutonium-charts.min.js +41 -0
  13. data/app/assets/plutonium-charts.min.js.map +7 -0
  14. data/app/assets/plutonium.css +1 -1
  15. data/app/assets/plutonium.js +794 -629
  16. data/app/assets/plutonium.js.map +4 -4
  17. data/app/assets/plutonium.min.js +48 -48
  18. data/app/assets/plutonium.min.js.map +4 -4
  19. data/app/views/plutonium/_flash_alerts.html.erb +2 -2
  20. data/app/views/plutonium/_resource_header.html.erb +2 -2
  21. data/app/views/plutonium/_resource_sidebar.html.erb +11 -2
  22. data/app/views/plutonium/_toast.html.erb +2 -2
  23. data/app/views/rodauth/_login_form.html.erb +1 -1
  24. data/app/views/rodauth/_password_visibility.html.erb +1 -1
  25. data/app/views/rodauth/add_recovery_codes.html.erb +2 -2
  26. data/app/views/rodauth/change_login.html.erb +2 -2
  27. data/app/views/rodauth/create_account.html.erb +4 -4
  28. data/app/views/rodauth/reset_password_request.html.erb +1 -1
  29. data/app/views/rodauth/verify_account_resend.html.erb +1 -1
  30. data/app/views/rodauth/webauthn_remove.html.erb +1 -1
  31. data/config/brakeman.ignore +4 -4
  32. data/config/initializers/rabl.rb +0 -40
  33. data/config/locales/en/api_client.yml +22 -0
  34. data/config/locales/en/async.yml +34 -0
  35. data/config/locales/en/dashboard.yml +25 -0
  36. data/config/locales/en/invites.yml +37 -0
  37. data/config/locales/en/js.yml +40 -0
  38. data/config/locales/en/kanban.yml +28 -0
  39. data/config/locales/en/plutonium.yml +26 -0
  40. data/config/locales/en/positioning.yml +14 -0
  41. data/config/locales/en/profile.yml +29 -0
  42. data/config/locales/en/resource.yml +60 -0
  43. data/config/locales/en/ui.yml +93 -0
  44. data/config/locales/en/wizard.yml +46 -0
  45. data/docs/.vitepress/config.ts +34 -1
  46. data/docs/.vitepress/theme/components/HomeHero.vue +1 -1
  47. data/docs/.vitepress/theme/components/HomeInTheBox.vue +14 -0
  48. data/docs/.vitepress/theme/custom.css +34 -377
  49. data/docs/blog/introducing-plutonium-dashboards.md +175 -0
  50. data/docs/blog/introducing-plutonium-i18n.md +91 -0
  51. data/docs/blog/introducing-plutonium.md +4 -5
  52. data/docs/blog/whats-new-async-kanban-wizards.md +1 -1
  53. data/docs/guides/dashboards.md +300 -0
  54. data/docs/guides/index.md +1 -0
  55. data/docs/guides/kanban.md +2 -2
  56. data/docs/public/images/blog/dashboards-card-error.png +0 -0
  57. data/docs/public/images/blog/dashboards-loading.png +0 -0
  58. data/docs/public/images/blog/dashboards-overview.png +0 -0
  59. data/docs/public/images/blog/dashboards-sidebar.png +0 -0
  60. data/docs/public/images/blog/i18n-es-admin.png +0 -0
  61. data/docs/public/images/guides/dashboard-card-error.png +0 -0
  62. data/docs/public/images/guides/dashboard-loading.png +0 -0
  63. data/docs/public/images/guides/dashboard-overview.png +0 -0
  64. data/docs/public/images/guides/dashboard-sidebar.png +0 -0
  65. data/docs/reference/app/generators.md +2 -2
  66. data/docs/reference/app/portals.md +2 -0
  67. data/docs/reference/configuration.md +84 -27
  68. data/docs/reference/dashboard/dsl.md +123 -0
  69. data/docs/reference/dashboard/index.md +35 -0
  70. data/docs/reference/dashboard/registration.md +76 -0
  71. data/docs/reference/i18n.md +239 -0
  72. data/docs/reference/index.md +10 -1
  73. data/docs/reference/kanban/dsl.md +1 -1
  74. data/docs/reference/kanban/positioning.md +2 -2
  75. data/docs/reference/resource/actions.md +1 -1
  76. data/docs/reference/resource/definition.md +96 -18
  77. data/docs/reference/resource/export.md +5 -1
  78. data/docs/reference/resource/index.md +2 -0
  79. data/docs/reference/{positioning.md → resource/positioning.md} +2 -1
  80. data/docs/reference/resource/query.md +2 -0
  81. data/docs/reference/ui/assets.md +8 -0
  82. data/docs/reference/ui/displays.md +5 -8
  83. data/docs/reference/ui/index.md +1 -1
  84. data/docs/reference/wizard/dsl.md +2 -2
  85. data/esbuild.config.js +5 -2
  86. data/gemfiles/rails_7.gemfile.lock +2 -2
  87. data/gemfiles/rails_8.1.gemfile.lock +1 -1
  88. data/lib/active_model/validations/array_validator.rb +1 -1
  89. data/lib/active_model/validations/attached_validator.rb +1 -1
  90. data/lib/active_model/validations/url_validator.rb +2 -2
  91. data/lib/generators/pu/core/assets/assets_generator.rb +36 -21
  92. data/lib/generators/pu/core/assets/templates/postcss.config.js +1 -1
  93. data/lib/generators/pu/core/update/update_generator.rb +3 -2
  94. data/lib/generators/pu/dashboard/dashboard_generator.rb +96 -0
  95. data/lib/generators/pu/dashboard/templates/dashboard.rb.tt +25 -0
  96. data/lib/generators/pu/docker/install/install_generator.rb +58 -0
  97. data/lib/generators/pu/docker/install/templates/Dockerfile.dev.tt +18 -3
  98. data/lib/generators/pu/docker/install/templates/Dockerfile.tt +20 -5
  99. data/lib/generators/pu/docker/install/templates/{docker-compose.yml → docker-compose.yml.tt} +1 -1
  100. data/lib/generators/pu/gem/dotenv/dotenv_generator.rb +1 -1
  101. data/lib/generators/pu/invites/invitable_generator.rb +18 -1
  102. data/lib/generators/pu/lib/plutonium_generators/concerns/actions.rb +0 -95
  103. data/lib/generators/pu/lib/plutonium_generators/concerns/js_package_manager.rb +146 -0
  104. data/lib/generators/pu/lib/plutonium_generators/generator.rb +1 -17
  105. data/lib/generators/pu/pkg/package/package_generator.rb +1 -0
  106. data/lib/generators/pu/pkg/package/templates/config/locales/en.yml.tt +28 -0
  107. data/lib/generators/pu/pkg/portal/portal_generator.rb +1 -0
  108. data/lib/generators/pu/pkg/portal/templates/config/locales/en.yml.tt +30 -0
  109. data/lib/generators/pu/res/scaffold/scaffold_generator.rb +0 -8
  110. data/lib/generators/pu/rodauth/concerns/feature_selector.rb +0 -18
  111. data/lib/plutonium/action/base.rb +27 -3
  112. data/lib/plutonium/action/interactive.rb +17 -4
  113. data/lib/plutonium/api_client/concerns/create_api_client.rb +12 -11
  114. data/lib/plutonium/api_client/concerns/disable_api_client.rb +3 -3
  115. data/lib/plutonium/auth/sequel_adapter.rb +1 -1
  116. data/lib/plutonium/configuration.rb +5 -0
  117. data/lib/plutonium/core/controller.rb +25 -1
  118. data/lib/plutonium/core/controllers/authorizable.rb +1 -1
  119. data/lib/plutonium/dashboard/base.rb +114 -0
  120. data/lib/plutonium/dashboard/card.rb +200 -0
  121. data/lib/plutonium/dashboard/controller.rb +101 -0
  122. data/lib/plutonium/dashboard/dsl.rb +104 -0
  123. data/lib/plutonium/dashboard/register.rb +40 -0
  124. data/lib/plutonium/dashboard/route_resolution.rb +30 -0
  125. data/lib/plutonium/dashboard.rb +42 -0
  126. data/lib/plutonium/definition/actions.rb +6 -1
  127. data/lib/plutonium/definition/base.rb +41 -3
  128. data/lib/plutonium/definition/index_views.rb +1 -1
  129. data/lib/plutonium/definition/positioning.rb +1 -1
  130. data/lib/plutonium/definition/presentable.rb +2 -2
  131. data/lib/plutonium/helpers/assets_helper.rb +15 -2
  132. data/lib/plutonium/helpers/display_helper.rb +10 -2
  133. data/lib/plutonium/interaction/async/context.rb +6 -8
  134. data/lib/plutonium/interaction/async/executor.rb +6 -7
  135. data/lib/plutonium/interaction/async/run.rb +2 -2
  136. data/lib/plutonium/interaction/async/run_definition.rb +1 -1
  137. data/lib/plutonium/interaction/base.rb +2 -0
  138. data/lib/plutonium/interaction/concerns/dispatchable.rb +15 -3
  139. data/lib/plutonium/invites/concerns/cancel_invite.rb +3 -3
  140. data/lib/plutonium/invites/concerns/invite_token.rb +25 -7
  141. data/lib/plutonium/invites/concerns/invite_user.rb +10 -5
  142. data/lib/plutonium/invites/concerns/resend_invite.rb +4 -4
  143. data/lib/plutonium/invites/controller.rb +11 -11
  144. data/lib/plutonium/kanban/column.rb +13 -3
  145. data/lib/plutonium/kanban/dsl.rb +7 -4
  146. data/lib/plutonium/models/has_cents.rb +1 -1
  147. data/lib/plutonium/profile/security_section.rb +10 -18
  148. data/lib/plutonium/query/filter.rb +10 -1
  149. data/lib/plutonium/query/filters/association.rb +27 -2
  150. data/lib/plutonium/query/filters/boolean.rb +4 -4
  151. data/lib/plutonium/query/filters/date.rb +4 -17
  152. data/lib/plutonium/query/filters/date_range.rb +2 -2
  153. data/lib/plutonium/query/filters/select.rb +10 -1
  154. data/lib/plutonium/query/filters/text.rb +9 -12
  155. data/lib/plutonium/railtie.rb +17 -0
  156. data/lib/plutonium/resource/controller.rb +10 -5
  157. data/lib/plutonium/resource/controllers/crud_actions.rb +17 -20
  158. data/lib/plutonium/resource/controllers/export_csv.rb +19 -9
  159. data/lib/plutonium/resource/controllers/kanban_actions.rb +16 -16
  160. data/lib/plutonium/resource/controllers/position_actions.rb +5 -5
  161. data/lib/plutonium/resource/controllers/typeahead.rb +24 -1
  162. data/lib/plutonium/resource/query_object.rb +30 -3
  163. data/lib/plutonium/routing/dashboard_registration.rb +137 -0
  164. data/lib/plutonium/routing/resource_registration.rb +5 -0
  165. data/lib/plutonium/routing/route_set_extensions.rb +2 -1
  166. data/lib/plutonium/translation.rb +227 -0
  167. data/lib/plutonium/ui/actions_dropdown.rb +2 -2
  168. data/lib/plutonium/ui/breadcrumbs.rb +3 -3
  169. data/lib/plutonium/ui/color_mode_selector.rb +1 -1
  170. data/lib/plutonium/ui/component/behaviour.rb +46 -0
  171. data/lib/plutonium/ui/component/methods.rb +4 -0
  172. data/lib/plutonium/ui/dashboard/board.rb +80 -0
  173. data/lib/plutonium/ui/dashboard/card.rb +127 -0
  174. data/lib/plutonium/ui/dashboard/chart.rb +43 -0
  175. data/lib/plutonium/ui/dashboard/custom.rb +32 -0
  176. data/lib/plutonium/ui/dashboard/frame.rb +25 -0
  177. data/lib/plutonium/ui/dashboard/metric.rb +137 -0
  178. data/lib/plutonium/ui/dashboard/skeleton.rb +40 -0
  179. data/lib/plutonium/ui/display/components/attachment.rb +1 -1
  180. data/lib/plutonium/ui/display/components/badge.rb +7 -1
  181. data/lib/plutonium/ui/display/components/boolean.rb +2 -2
  182. data/lib/plutonium/ui/display/resource.rb +23 -16
  183. data/lib/plutonium/ui/export_button.rb +3 -3
  184. data/lib/plutonium/ui/form/components/key_value_store.rb +5 -5
  185. data/lib/plutonium/ui/form/components/password.rb +2 -2
  186. data/lib/plutonium/ui/form/components/resource_select.rb +27 -0
  187. data/lib/plutonium/ui/form/components/secure_association.rb +2 -0
  188. data/lib/plutonium/ui/form/components/uppy.rb +11 -3
  189. data/lib/plutonium/ui/form/concerns/renders_nested_resource_fields.rb +1 -1
  190. data/lib/plutonium/ui/form/concerns/renders_repeater_row_controls.rb +5 -4
  191. data/lib/plutonium/ui/form/concerns/renders_structured_inputs.rb +1 -1
  192. data/lib/plutonium/ui/form/query.rb +8 -8
  193. data/lib/plutonium/ui/form/resource.rb +54 -26
  194. data/lib/plutonium/ui/frame_navigator_panel.rb +4 -4
  195. data/lib/plutonium/ui/grid/card.rb +3 -3
  196. data/lib/plutonium/ui/grid/resource.rb +3 -3
  197. data/lib/plutonium/ui/interaction/async/run_progress.rb +6 -6
  198. data/lib/plutonium/ui/interaction/async/running_banner.rb +2 -2
  199. data/lib/plutonium/ui/kanban/column.rb +6 -6
  200. data/lib/plutonium/ui/layout/base.rb +19 -1
  201. data/lib/plutonium/ui/layout/header.rb +1 -1
  202. data/lib/plutonium/ui/layout/icon_rail.rb +4 -4
  203. data/lib/plutonium/ui/layout/rodauth_layout.rb +1 -1
  204. data/lib/plutonium/ui/layout/sidebar.rb +1 -1
  205. data/lib/plutonium/ui/layout/topbar.rb +2 -2
  206. data/lib/plutonium/ui/modal/base.rb +4 -4
  207. data/lib/plutonium/ui/nav_grid_menu.rb +1 -1
  208. data/lib/plutonium/ui/nav_user.rb +2 -2
  209. data/lib/plutonium/ui/page/base.rb +10 -2
  210. data/lib/plutonium/ui/page/dashboard.rb +54 -0
  211. data/lib/plutonium/ui/page/edit.rb +3 -3
  212. data/lib/plutonium/ui/page/interactive_action.rb +2 -2
  213. data/lib/plutonium/ui/page/new.rb +3 -3
  214. data/lib/plutonium/ui/page/show.rb +2 -2
  215. data/lib/plutonium/ui/page/wizard.rb +22 -16
  216. data/lib/plutonium/ui/page/wizard_chooser.rb +10 -10
  217. data/lib/plutonium/ui/page/wizard_completed.rb +2 -2
  218. data/lib/plutonium/ui/skeleton_table.rb +1 -1
  219. data/lib/plutonium/ui/table/base.rb +2 -2
  220. data/lib/plutonium/ui/table/components/attachment.rb +1 -1
  221. data/lib/plutonium/ui/table/components/bulk_actions_toolbar.rb +19 -3
  222. data/lib/plutonium/ui/table/components/drag_handle.rb +3 -3
  223. data/lib/plutonium/ui/table/components/filter_form.rb +11 -10
  224. data/lib/plutonium/ui/table/components/filter_pills.rb +4 -4
  225. data/lib/plutonium/ui/table/components/pagy_info.rb +61 -17
  226. data/lib/plutonium/ui/table/components/pagy_pagination.rb +9 -5
  227. data/lib/plutonium/ui/table/components/row_actions_dropdown.rb +1 -1
  228. data/lib/plutonium/ui/table/components/scopes_bar.rb +6 -2
  229. data/lib/plutonium/ui/table/components/scopes_pills.rb +3 -3
  230. data/lib/plutonium/ui/table/components/selection_column.rb +1 -1
  231. data/lib/plutonium/ui/table/components/toolbar.rb +2 -2
  232. data/lib/plutonium/ui/table/components/view_switcher.rb +12 -8
  233. data/lib/plutonium/ui/table/resource.rb +53 -17
  234. data/lib/plutonium/ui/wizard/review.rb +7 -7
  235. data/lib/plutonium/ui/wizard/stepper.rb +1 -1
  236. data/lib/plutonium/ui/wizard/summary_display.rb +6 -0
  237. data/lib/plutonium/version.rb +1 -1
  238. data/lib/plutonium/wizard/base.rb +2 -0
  239. data/lib/plutonium/wizard/driving.rb +8 -2
  240. data/lib/plutonium/wizard/dsl.rb +4 -2
  241. data/lib/plutonium/wizard/field_importer.rb +1 -8
  242. data/lib/plutonium/wizard/review_step.rb +8 -1
  243. data/lib/plutonium/wizard/step.rb +14 -3
  244. data/package.json +4 -2
  245. data/plutonium.gemspec +4 -1
  246. data/postcss-gem-import.cjs +34 -0
  247. data/postcss-gem-import.js +3 -28
  248. data/src/css/components.css +91 -0
  249. data/src/css/slim_select.css +0 -38
  250. data/src/css/tokens.css +24 -0
  251. data/src/js/controllers/attachment_input_controller.js +15 -6
  252. data/src/js/controllers/chart_controller.js +162 -0
  253. data/src/js/controllers/clipboard_controller.js +3 -2
  254. data/src/js/controllers/dirty_form_guard_controller.js +4 -3
  255. data/src/js/controllers/easymde_controller.js +3 -0
  256. data/src/js/controllers/filter_panel_controller.js +15 -6
  257. data/src/js/controllers/flatpickr_controller.js +8 -0
  258. data/src/js/controllers/frame_refresh_controller.js +59 -0
  259. data/src/js/controllers/intl_tel_input_controller.js +5 -0
  260. data/src/js/controllers/register_controllers.js +4 -0
  261. data/src/js/controllers/slim_select_controller.js +79 -176
  262. data/src/js/core.js +9 -1
  263. data/src/js/i18n.js +69 -0
  264. data/src/js/plutonium-charts.js +15 -0
  265. data/src/js/turbo/turbo_confirm.js +5 -3
  266. metadata +64 -16
  267. data/.yarnrc.yml +0 -8
  268. data/config/initializers/hotwire_turbo_monkey_patches.rb +0 -12
  269. data/lib/generators/pu/field/input/input_generator.rb +0 -32
  270. data/lib/generators/pu/field/input/templates/input.rb.tt +0 -15
  271. data/lib/generators/pu/lib/plutonium_generators/concerns/config.rb +0 -39
  272. data/lib/generators/pu/lib/plutonium_generators/concerns/serializer.rb +0 -39
  273. data/lib/generators/pu/lib/plutonium_generators/installer.rb +0 -205
  274. data/lib/generators/pu/res/scaffold/templates/presenter.rb.tt +0 -13
  275. data/lib/generators/pu/res/scaffold/templates/query_object.rb.tt +0 -17
  276. data/public/plutonium-assets/application.js +0 -31419
  277. data/public/plutonium-assets/plutonium.ico +0 -0
  278. data/yarn.lock +0 -6858
@@ -60,8 +60,8 @@ module Plutonium
60
60
  # regains the metadata rail that the modal hides.
61
61
  def render_modal_details
62
62
  render Plutonium::UI::Modal::Centered.new(
63
- title: page_title,
64
- description: page_description,
63
+ title: resolved_page_title,
64
+ description: resolved_page_description,
65
65
  size: :lg,
66
66
  open_full_url: open_full_page_url
67
67
  ) do
@@ -37,8 +37,13 @@ module Plutonium
37
37
  private
38
38
 
39
39
  def wizard_title
40
- step = @runner.current_step
41
- [@runner.wizard.class.label, step&.label].compact.join(" — ").presence || "Wizard"
40
+ wizard = @runner.wizard.class.label.presence
41
+ step = @runner.current_step&.label.presence
42
+ if wizard && step
43
+ t("plutonium.wizard.page.title", wizard:, step:)
44
+ else
45
+ wizard || step || t("plutonium.wizard.page.fallback_title")
46
+ end
42
47
  end
43
48
 
44
49
  # Centered wizard header: the title and the wizard-level description
@@ -103,7 +108,7 @@ module Plutonium
103
108
  div(class: "mb-6") do
104
109
  unless step.review?
105
110
  span(class: "text-xs font-bold uppercase tracking-wide text-primary-600 dark:text-primary-400") do
106
- "Step #{step_position(step)} of #{visible_step_count}"
111
+ t("plutonium.wizard.page.step_position", position: step_position(step), total: visible_step_count)
107
112
  end
108
113
  end
109
114
  h2(class: "mt-1 text-xl font-semibold tracking-tight text-[var(--pu-text)]") { step.label.to_s }
@@ -112,7 +117,7 @@ module Plutonium
112
117
  # shown — when summary is off (the ready panel / custom-only body), that
113
118
  # prompt would contradict the body, so we omit it.
114
119
  desc = step.description.presence
115
- desc ||= "Check everything over before you finish." if step.review? && step.summary?
120
+ desc ||= t("plutonium.wizard.page.review_prompt") if step.review? && step.summary?
116
121
  if desc
117
122
  p(class: "mt-1.5 text-sm text-[var(--pu-text-muted)]") { desc }
118
123
  end
@@ -182,12 +187,12 @@ module Plutonium
182
187
 
183
188
  div(class: "pu-wizard-nav flex items-center justify-between gap-3 border-t border-[var(--pu-border)] bg-[var(--pu-surface-alt)] px-6 py-4 sm:px-8") do
184
189
  div(class: "flex items-center gap-2") do
185
- nav_button("Back", direction: "back", style: "pu-btn-outline", embedded:) if show_back?
186
- nav_button("Cancel", direction: "cancel", style: "pu-btn-ghost", embedded:)
190
+ nav_button(t("plutonium.wizard.nav.back"), direction: "back", style: "pu-btn-outline", embedded:) if show_back?
191
+ nav_button(t("plutonium.wizard.cancel"), direction: "cancel", style: "pu-btn-ghost", embedded:)
187
192
  end
188
193
  div(class: "flex items-center gap-2") do
189
194
  if finish
190
- nav_button("Finish", direction: "next", style: "pu-btn-primary", embedded:, disabled: finish_disabled, name: "finish")
195
+ nav_button(t("plutonium.wizard.nav.finish"), direction: "next", style: "pu-btn-primary", embedded:, disabled: finish_disabled, name: "finish")
191
196
  else
192
197
  render_forward_buttons(embedded:)
193
198
  end
@@ -209,11 +214,11 @@ module Plutonium
209
214
  # nothing was persisted, so this isn't a re-edit. Keep "Next" there; the
210
215
  # presence of `@errors` is the error-render signal.
211
216
  revisiting = @runner.submitted?(step) && @errors.blank?
212
- continue_label = revisiting ? "Save & continue" : "Next"
217
+ continue_label = revisiting ? t("plutonium.wizard.nav.save_and_continue") : t("plutonium.wizard.nav.next")
213
218
 
214
219
  if review_shortcut?(step)
215
220
  nav_button(continue_label, direction: "next", style: "pu-btn-outline", embedded:, name: "next")
216
- nav_button("Save & review", direction: "next", style: "pu-btn-primary", embedded:, name: "save_review", goto: "review")
221
+ nav_button(t("plutonium.wizard.nav.save_and_review"), direction: "next", style: "pu-btn-primary", embedded:, name: "save_review", goto: "review")
217
222
  else
218
223
  nav_button(continue_label, direction: "next", style: "pu-btn-primary", embedded:, name: "next")
219
224
  end
@@ -305,8 +310,8 @@ module Plutonium
305
310
  # unsaved edits and the user clicks a control that abandons them.
306
311
  def leave_warning(direction)
307
312
  case direction
308
- when "back" then "You have unsaved changes on this step. Go back and lose them?"
309
- when "cancel" then "You have unsaved changes. Cancel the wizard and lose them?"
313
+ when "back" then t("plutonium.wizard.nav.unsaved_back")
314
+ when "cancel" then t("plutonium.wizard.nav.unsaved_cancel")
310
315
  end
311
316
  end
312
317
 
@@ -350,14 +355,15 @@ module Plutonium
350
355
  end
351
356
  end
352
357
 
353
- # Every finalize error as a full sentence: a `:base` error renders verbatim;
354
- # a field error is prefixed with its humanized attribute, mirroring Rails'
355
- # `full_messages` — so {name: ["has already been taken"]} → "Name has
358
+ # Every finalize error as a full sentence, built by the wizard's own
359
+ # `errors.full_message` so it follows Rails' `errors.format` and attribute
360
+ # translations: a `:base` error renders verbatim; a field error is prefixed
361
+ # with its attribute name — {name: ["has already been taken"]} → "Name has
356
362
  # already been taken".
357
363
  def review_error_messages
364
+ errors = @runner.wizard.errors
358
365
  @errors.flat_map do |attr, msgs|
359
- base = attr.to_s == "base"
360
- Array(msgs).map { |m| base ? m : "#{attr.to_s.humanize} #{m}" }
366
+ Array(msgs).map { |m| errors.full_message(attr.to_sym, m) }
361
367
  end
362
368
  end
363
369
 
@@ -38,7 +38,7 @@ module Plutonium
38
38
  div(class: "pu-wizard-header mb-7 text-center") do
39
39
  h1(class: "text-2xl font-bold tracking-tight text-[var(--pu-text)]") { @wizard_class.label }
40
40
  p(class: "mx-auto mt-1.5 max-w-prose text-[var(--pu-text-muted)]") do
41
- "Pick up where you left off, or start a new one."
41
+ t("plutonium.wizard.chooser.prompt")
42
42
  end
43
43
  end
44
44
  end
@@ -64,15 +64,15 @@ module Plutonium
64
64
  li(class: "flex items-center justify-between gap-4 px-5 py-4", data: {wizard_chooser_entry: entry.current_step}) do
65
65
  div(class: "min-w-0") do
66
66
  p(class: "truncate text-sm font-semibold text-[var(--pu-text)]") do
67
- entry.current_step_label.presence || "In progress"
67
+ entry.current_step_label.presence || t("plutonium.wizard.chooser.in_progress")
68
68
  end
69
- p(class: "mt-0.5 text-xs text-[var(--pu-text-muted)]") { "Updated #{updated_ago(entry)} ago" }
69
+ p(class: "mt-0.5 text-xs text-[var(--pu-text-muted)]") { t("plutonium.wizard.chooser.updated_ago", time: updated_ago(entry)) }
70
70
  end
71
71
  div(class: "flex items-center gap-2 shrink-0") do
72
72
  if entry.resume_url.present?
73
- a(href: entry.resume_url, class: "pu-btn pu-btn-sm pu-btn-outline", data: {wizard_chooser_resume: true}) { "Resume" }
73
+ a(href: entry.resume_url, class: "pu-btn pu-btn-sm pu-btn-outline", data: {wizard_chooser_resume: true}) { t("plutonium.wizard.chooser.resume") }
74
74
  else
75
- span(class: "pu-btn pu-btn-sm pu-btn-outline opacity-50 cursor-not-allowed") { "Resume" }
75
+ span(class: "pu-btn pu-btn-sm pu-btn-outline opacity-50 cursor-not-allowed") { t("plutonium.wizard.chooser.resume") }
76
76
  end
77
77
 
78
78
  render_cancel_form(entry) if entry.cancel_url.present?
@@ -92,19 +92,19 @@ module Plutonium
92
92
  button(
93
93
  type: "submit",
94
94
  class: "pu-btn pu-btn-sm pu-btn-soft-danger",
95
- aria_label: "Cancel this #{@wizard_class.label} draft",
95
+ aria_label: t("plutonium.wizard.chooser.cancel_draft", wizard: @wizard_class.label),
96
96
  data: {
97
97
  wizard_chooser_cancel: true,
98
- turbo_confirm: "Discard this in-progress #{@wizard_class.label}? This can't be undone."
98
+ turbo_confirm: t("plutonium.wizard.chooser.discard_confirm", wizard: @wizard_class.label)
99
99
  }
100
- ) { "Cancel" }
100
+ ) { t("plutonium.wizard.cancel") }
101
101
  end
102
102
  end
103
103
 
104
104
  def render_start_new
105
105
  div(class: "border-t border-[var(--pu-border)] bg-[var(--pu-surface-alt)] px-5 py-4") do
106
106
  a(href: @start_new_url, class: "pu-btn pu-btn-md pu-btn-primary w-full justify-center", data: {wizard_chooser_start_new: true}) do
107
- "Start new"
107
+ t("plutonium.wizard.chooser.start_new")
108
108
  end
109
109
  end
110
110
  end
@@ -112,7 +112,7 @@ module Plutonium
112
112
  def updated_ago(entry)
113
113
  helpers.time_ago_in_words(entry.updated_at)
114
114
  rescue
115
- "a while"
115
+ t("plutonium.wizard.chooser.a_while")
116
116
  end
117
117
 
118
118
  def page_type = :wizard_chooser_page
@@ -53,12 +53,12 @@ module Plutonium
53
53
  @wizard.class.label
54
54
  end
55
55
  p(class: "mx-auto mt-2 max-w-prose text-[var(--pu-text-muted)]") do
56
- "You've already completed this — there's nothing more to do here."
56
+ t("plutonium.wizard.completed.message")
57
57
  end
58
58
  div(class: "mt-7") do
59
59
  # Exits the wizard to a different page; opt out of Turbo morph (which
60
60
  # would otherwise nest the destination into this page — see Page::Wizard).
61
- a(href: @exit_url, class: "pu-btn pu-btn-md pu-btn-primary", data: {wizard_completed: "exit", turbo: "false"}) { "Continue" }
61
+ a(href: @exit_url, class: "pu-btn pu-btn-md pu-btn-primary", data: {wizard_completed: "exit", turbo: "false"}) { t("plutonium.wizard.completed.continue") }
62
62
  end
63
63
  end
64
64
 
@@ -20,7 +20,7 @@ module Plutonium
20
20
  end
21
21
  div(class: "h-3 bg-[var(--pu-border)] rounded-full w-12")
22
22
  end
23
- span(class: "sr-only") { "Loading..." }
23
+ span(class: "sr-only") { t("plutonium.ui.loading") }
24
24
  end
25
25
  end
26
26
  end
@@ -134,7 +134,7 @@ module Plutonium
134
134
  type: "button",
135
135
  class: themed(:column_menu_trigger),
136
136
  data: {action: "click->table-column-menu#toggle"},
137
- aria: {label: "Column options"}
137
+ aria: {label: Plutonium::Translation.t("plutonium.ui.table.column_options")}
138
138
  ) do
139
139
  render Phlex::TablerIcons::Dots.new(class: "w-3 h-3")
140
140
  end
@@ -143,7 +143,7 @@ module Plutonium
143
143
  class: themed(:column_menu_panel),
144
144
  data: {"table-column-menu-target": "panel"}
145
145
  ) do
146
- render_menu_item("Clear sort", sort_params[:reset_url], icon: Phlex::TablerIcons::X)
146
+ render_menu_item(Plutonium::Translation.t("plutonium.ui.table.clear_sort"), sort_params[:reset_url], icon: Phlex::TablerIcons::X)
147
147
  end
148
148
  end
149
149
  end
@@ -59,7 +59,7 @@ module Plutonium
59
59
  end
60
60
 
61
61
  def attachment_extension(attachment)
62
- attachment.try(:extension) || File.extname(attachment.filename.to_s)
62
+ attachment.try(:extension).presence || File.extname(attachment.filename.to_s).delete(".")
63
63
  end
64
64
 
65
65
  def normalize_value(value)
@@ -37,11 +37,27 @@ module Plutonium
37
37
 
38
38
  def render_selected_count
39
39
  div(class: "text-sm font-medium text-primary-700 dark:text-primary-300") do
40
- span(data: {bulk_actions_target: "selectedCount"}) { "0" }
41
- plain " selected"
40
+ # The count is live-updated by the Stimulus controller, so the
41
+ # sentence is translated with its placeholder left in and split
42
+ # around it (same pattern as PagyInfo#render_template).
43
+ selected_count_template.split(/(%\{count\})/).each do |part|
44
+ if part == "%{count}"
45
+ span(data: {bulk_actions_target: "selectedCount"}) { "0" }
46
+ else
47
+ plain part
48
+ end
49
+ end
42
50
  end
43
51
  end
44
52
 
53
+ # The raw template, placeholder intact. Fetched without `count:` so
54
+ # no pluralisation rule runs on a placeholder; a locale that defines
55
+ # one/other forms contributes its `other` form.
56
+ def selected_count_template
57
+ template = t("plutonium.ui.table.selected_count")
58
+ template.is_a?(Hash) ? template[:other] : template
59
+ end
60
+
45
61
  def render_action_buttons
46
62
  div(class: "flex items-center gap-1.5") do
47
63
  @bulk_actions.each do |action|
@@ -55,7 +71,7 @@ module Plutonium
55
71
  type: "button",
56
72
  data: {action: "click->bulk-actions#clearSelection"},
57
73
  class: "ml-auto text-xs text-primary-700 dark:text-primary-300 hover:underline"
58
- ) { "Clear selection" }
74
+ ) { t("plutonium.ui.table.clear_selection") }
59
75
  end
60
76
 
61
77
  def render_action_button(action)
@@ -70,8 +70,8 @@ module Plutonium
70
70
  type: "button",
71
71
  draggable: "true",
72
72
  class: themed(@variant[:grip]),
73
- title: "Drag to reorder",
74
- aria: {label: "Drag to reorder. Use the up and down arrow keys to move this row."},
73
+ title: Plutonium::Translation.t("plutonium.positioning.drag_handle.title"),
74
+ aria: {label: Plutonium::Translation.t("plutonium.positioning.drag_handle.aria_label")},
75
75
  data: {positioned_grip: ""}
76
76
  ) { icon }
77
77
  end
@@ -80,7 +80,7 @@ module Plutonium
80
80
  a(
81
81
  href: @sort_url,
82
82
  class: themed(@variant[:disabled]),
83
- title: "Sort by position to reorder"
83
+ title: Plutonium::Translation.t("plutonium.positioning.drag_handle.disabled_title")
84
84
  ) { icon }
85
85
  end
86
86
 
@@ -49,18 +49,18 @@ module Plutonium
49
49
  def render_header
50
50
  div(class: "shrink-0 flex items-center justify-between gap-4 px-6 pt-5 pb-4 " \
51
51
  "border-b border-[var(--pu-border)]") do
52
- h2(class: "text-lg font-semibold text-[var(--pu-text)]") { "Filters" }
52
+ h2(class: "text-lg font-semibold text-[var(--pu-text)]") { t("plutonium.ui.table.filters") }
53
53
  div(class: "flex items-center gap-1") do
54
54
  button(
55
55
  type: "button",
56
56
  class: "text-sm text-[var(--pu-text-muted)] hover:text-[var(--pu-text)] px-2 py-1 rounded transition-colors",
57
57
  data: {action: "filter-panel#clear"}
58
- ) { "Clear" }
58
+ ) { t("plutonium.ui.table.clear") }
59
59
  button(
60
60
  type: "button",
61
61
  class: "p-1.5 -m-1.5 text-[var(--pu-text-muted)] hover:text-[var(--pu-text)] hover:bg-[var(--pu-surface-alt)] rounded-md transition-colors",
62
62
  data: {action: "filter-panel#close"},
63
- "aria-label": "Close filters"
63
+ "aria-label": t("plutonium.ui.table.close_filters")
64
64
  ) do
65
65
  render Phlex::TablerIcons::X.new(class: "w-5 h-5")
66
66
  end
@@ -76,9 +76,9 @@ module Plutonium
76
76
  has_multiple_inputs = inputs.size > 1
77
77
  inputs.each do |input_name, _|
78
78
  label = if has_multiple_inputs
79
- "#{filter_name.to_s.humanize} (#{input_name.to_s.humanize.downcase})"
79
+ t("plutonium.query.filter_input_label", label: query_object.filter_label(filter_name), input: input_name.to_s.humanize.downcase)
80
80
  else
81
- filter_name.to_s.humanize
81
+ query_object.filter_label(filter_name)
82
82
  end
83
83
  render_filter_field(nested, definition, input_name, filter_label: label)
84
84
  end
@@ -93,7 +93,7 @@ module Plutonium
93
93
  render field(:submit).submit_button_tag(
94
94
  name: nil,
95
95
  class!: "pu-btn pu-btn-md pu-btn-primary"
96
- ) { "Apply" }
96
+ ) { t("plutonium.ui.table.apply") }
97
97
  end
98
98
  end
99
99
 
@@ -115,14 +115,15 @@ module Plutonium
115
115
  end
116
116
 
117
117
  def render_sort_fields
118
- field :sort_fields do |name|
118
+ field :sort_fields, value: helpers.params.dig(search_param, :sort_fields) do |name|
119
119
  render name.input_array_tag do |array|
120
120
  render array.input_tag(type: :hidden, hidden: true)
121
121
  end
122
122
  end
123
123
  nest_one :sort_directions do |nested|
124
124
  query_object.sort_definitions.each do |filter_name, _|
125
- nested.field(filter_name) do |f|
125
+ direction_value = helpers.params.dig(search_param, :sort_directions, filter_name)
126
+ nested.field(filter_name, value: direction_value) do |f|
126
127
  render f.input_tag(type: :hidden, hidden: true)
127
128
  end
128
129
  end
@@ -131,7 +132,7 @@ module Plutonium
131
132
 
132
133
  def render_scope_fields
133
134
  return if query_object.scope_definitions.blank?
134
- render field(:scope).input_tag(type: :hidden, hidden: true)
135
+ render field(:scope, value: helpers.params.dig(search_param, :scope)).input_tag(type: :hidden, hidden: true)
135
136
  end
136
137
 
137
138
  def render_filter_field(nested, resource_definition, name, filter_label: nil)
@@ -156,7 +157,7 @@ module Plutonium
156
157
  div(class: "space-y-1.5") do
157
158
  label(class: "text-sm font-medium text-[var(--pu-text)]") { filter_label }
158
159
  nested.field(name, value: current_value, **field_options) do |f|
159
- f.placeholder(input_options[:include_blank] || "All") if input_options[:include_blank]
160
+ f.placeholder(input_options[:include_blank] || t("plutonium.ui.table.all")) if input_options[:include_blank]
160
161
  render instance_exec(f, &tag_block)
161
162
  end
162
163
  end
@@ -32,7 +32,7 @@ module Plutonium
32
32
  span(class: "inline-flex items-center gap-1.5 h-6 px-2 rounded-full bg-primary-50 border border-primary-200 text-xs text-primary-700 dark:bg-primary-950/40 dark:border-primary-900/60 dark:text-primary-300") do
33
33
  span { plain "#{filter[:label]}: #{filter[:value_label]}" }
34
34
  a(href: filter[:clear_url],
35
- aria: {label: "Remove #{filter[:label]} filter"},
35
+ aria: {label: t("plutonium.ui.table.remove_filter", filter: filter[:label])},
36
36
  class: "ml-0.5 inline-flex items-center justify-center w-4 h-4 rounded hover:bg-primary-200 dark:hover:bg-primary-900/60") do
37
37
  render Phlex::TablerIcons::X.new(class: "w-3 h-3")
38
38
  end
@@ -44,14 +44,14 @@ module Plutonium
44
44
  data: {action: "click->filter-panel#toggle"},
45
45
  class: "inline-flex items-center gap-1 h-6 px-2 rounded-full border border-dashed border-[var(--pu-border)] text-xs text-[var(--pu-text-muted)] hover:text-[var(--pu-text)] hover:border-[var(--pu-border-strong)]") do
46
46
  render Phlex::TablerIcons::Plus.new(class: "w-3 h-3")
47
- plain "Filter"
47
+ plain t("plutonium.ui.table.filter")
48
48
  end
49
49
  end
50
50
 
51
51
  def render_clear_all_pill
52
52
  a(href: clear_all_url,
53
53
  class: "inline-flex items-center gap-1 h-6 px-2 rounded-full text-xs text-[var(--pu-text-muted)] hover:text-[var(--pu-text)] underline-offset-2 hover:underline") do
54
- plain "Clear all"
54
+ plain t("plutonium.ui.table.clear_all")
55
55
  end
56
56
  end
57
57
 
@@ -79,7 +79,7 @@ module Plutonium
79
79
 
80
80
  def render_result_count
81
81
  div(class: "ml-auto text-xs text-[var(--pu-text-muted)]") do
82
- plain "#{@total_count} #{(@total_count == 1) ? "result" : "results"}"
82
+ plain t("plutonium.ui.table.result_count", count: @total_count)
83
83
  end
84
84
  end
85
85
  end
@@ -4,10 +4,24 @@ module Plutonium
4
4
  module UI
5
5
  module Table
6
6
  module Components
7
+ # "Displaying articles 11-20 of 100 in total" plus a per-page selector.
8
+ #
9
+ # The sentences come from Pagy's own dictionaries (`pagy.info_tag.*`,
10
+ # `pagy.limit_tag_js`), translated in the current I18n locale, so a host
11
+ # that ships a Pagy locale file gets this component for free. Pagy
12
+ # leaves any placeholder it isn't handed untouched, which is what lets
13
+ # the counts render in bold and the per-page select sit inside its
14
+ # sentence: the template is translated with those placeholders still in
15
+ # place, then split around them.
7
16
  class PagyInfo < Plutonium::UI::Component::Base
8
- def initialize(pagy, per_page_options: [5, 10, 20, 50, 100])
17
+ PLACEHOLDER = /(%\{(?:from|to|count|limit_input)\})/
18
+
19
+ # @param item_name [String, nil] the noun for the paginated records
20
+ # ("articles"); defaults to Pagy's generic "items".
21
+ def initialize(pagy, per_page_options: [5, 10, 20, 50, 100], item_name: nil)
9
22
  @pagy = pagy
10
23
  @per_page_options = (per_page_options + [@pagy.limit]).uniq.sort
24
+ @item_name = item_name
11
25
  end
12
26
 
13
27
  def view_template
@@ -20,36 +34,66 @@ module Plutonium
20
34
  private
21
35
 
22
36
  def results_info
37
+ key = if @pagy.count.zero?
38
+ "pagy.info_tag.no_items"
39
+ elsif @pagy.in == @pagy.count
40
+ "pagy.info_tag.single_page"
41
+ else
42
+ "pagy.info_tag.multiple_pages"
43
+ end
44
+
23
45
  div do
24
- plain "Showing "
25
- b { @pagy.from.to_s }
26
- plain " to "
27
- b { @pagy.to.to_s }
28
- plain " of "
29
- b { @pagy.count }
30
- plain " results"
46
+ render_template(pagy_template(key, @pagy.count)) do |placeholder|
47
+ b { @pagy.public_send(placeholder).to_s }
48
+ end
31
49
  end
32
50
  end
33
51
 
34
52
  def per_page_selector
53
+ id = "perPage#{SecureRandom.hex}"
54
+
35
55
  div(
36
56
  class: "flex items-center space-x-2 mt-2 md:mt-0",
37
57
  data_controller: "select-navigator"
38
58
  ) do
39
- id = "perPage#{SecureRandom.hex}"
40
- label(for: id, class: "mr-2") { "Per page" }
41
- select(
42
- id: id, name: "items", class: select_classes,
43
- data_action: "change->select-navigator#navigate",
44
- data_select_navigator_target: "select"
45
- ) do
46
- @per_page_options.each do |option|
47
- option(value: page_url(option), selected: option == @pagy.limit) { option.to_s }
59
+ label(for: id, class: "flex items-center gap-2") do
60
+ render_template(pagy_template("pagy.limit_tag_js", @pagy.limit)) do
61
+ select(
62
+ id: id, name: "items", class: select_classes,
63
+ data_action: "change->select-navigator#navigate",
64
+ data_select_navigator_target: "select"
65
+ ) do
66
+ @per_page_options.each do |option|
67
+ option(value: page_url(option), selected: option == @pagy.limit) { option.to_s }
68
+ end
69
+ end
48
70
  end
49
71
  end
50
72
  end
51
73
  end
52
74
 
75
+ # Translate `key` with the item name filled in and every other
76
+ # placeholder left literal, for {#render_template} to split on.
77
+ def pagy_template(key, count)
78
+ Plutonium::Translation.pagy(key, item_name: item_name(count))
79
+ end
80
+
81
+ def item_name(count)
82
+ @item_name || Plutonium::Translation.pagy("pagy.item_name", count:)
83
+ end
84
+
85
+ # Emit `template` as text, yielding each remaining placeholder's name
86
+ # (:from, :to, :count, :limit_input) for the caller to render inline.
87
+ def render_template(template)
88
+ template.split(PLACEHOLDER).each do |part|
89
+ if (match = part.match(/\A%\{(\w+)\}\z/))
90
+ yield match[1].to_sym
91
+ else
92
+ plain part
93
+ end
94
+ end
95
+ end
96
+
53
97
  def select_classes
54
98
  "bg-[var(--pu-surface)] border border-[var(--pu-border)] text-[var(--pu-text)] text-sm rounded-[var(--pu-radius-md)] focus:ring-2 focus:ring-primary-500 focus:border-primary-500 block p-2.5 min-w-[5em]"
55
99
  end
@@ -10,7 +10,7 @@ module Plutonium
10
10
  end
11
11
 
12
12
  def view_template
13
- nav(aria_label: "Page navigation", class: "flex justify-center mt-6") do
13
+ nav(aria_label: Plutonium::Translation.pagy("pagy.aria_label.nav", count: @pagy.last), class: "flex justify-center mt-6") do
14
14
  ul(class: "inline-flex items-center gap-1 text-sm") do
15
15
  prev_link
16
16
  page_links
@@ -24,11 +24,11 @@ module Plutonium
24
24
  def prev_link
25
25
  li do
26
26
  if @pagy.previous
27
- a(href: page_url(@pagy.previous), class: link_classes(true)) {
27
+ a(href: page_url(@pagy.previous), class: link_classes(true), aria_label: previous_label) {
28
28
  render Phlex::TablerIcons::ChevronLeft.new
29
29
  }
30
30
  else
31
- button(class: disabled_link_classes(true), aria_disabled: "true") {
31
+ button(class: disabled_link_classes(true), aria_disabled: "true", aria_label: previous_label) {
32
32
  render Phlex::TablerIcons::ChevronLeft.new
33
33
  }
34
34
  end
@@ -38,17 +38,21 @@ module Plutonium
38
38
  def next_link
39
39
  li do
40
40
  if @pagy.next
41
- a(href: page_url(@pagy.next), class: link_classes(false, true)) {
41
+ a(href: page_url(@pagy.next), class: link_classes(false, true), aria_label: next_label) {
42
42
  render Phlex::TablerIcons::ChevronRight.new
43
43
  }
44
44
  else
45
- button(class: disabled_link_classes(false, true), aria_disabled: "true") {
45
+ button(class: disabled_link_classes(false, true), aria_disabled: "true", aria_label: next_label) {
46
46
  render Phlex::TablerIcons::ChevronRight.new
47
47
  }
48
48
  end
49
49
  end
50
50
  end
51
51
 
52
+ def previous_label = Plutonium::Translation.pagy("pagy.aria_label.previous")
53
+
54
+ def next_label = Plutonium::Translation.pagy("pagy.aria_label.next")
55
+
52
56
  def page_links
53
57
  @pagy.send(:series).each do |item|
54
58
  li do
@@ -25,7 +25,7 @@ module Plutonium
25
25
  button(
26
26
  type: "button",
27
27
  class: "p-1.5 rounded-[var(--pu-radius-md)] border border-[var(--pu-border)] bg-[var(--pu-surface)] text-[var(--pu-text-muted)] hover:text-[var(--pu-text)] hover:bg-[var(--pu-surface-alt)] hover:border-[var(--pu-border-strong)] transition-colors",
28
- aria: {expanded: "false", haspopup: "true", label: "More actions"},
28
+ aria: {expanded: "false", haspopup: "true", label: t("plutonium.ui.table.more_actions")},
29
29
  data: {resource_drop_down_target: "trigger"}
30
30
  ) do
31
31
  render Phlex::TablerIcons::DotsVertical.new(class: "w-4 h-4")
@@ -29,7 +29,7 @@ module Plutonium
29
29
  id: "all-scope",
30
30
  href: current_query_object.build_url(scope: nil),
31
31
  class: active ? active_scope_class : inactive_scope_class
32
- ) { "All" }
32
+ ) { t("plutonium.ui.table.all") }
33
33
  end
34
34
 
35
35
  def render_scope_button(name)
@@ -38,7 +38,11 @@ module Plutonium
38
38
  id: "#{name}-scope",
39
39
  href: current_query_object.build_url(scope: name),
40
40
  class: active ? active_scope_class : inactive_scope_class
41
- ) { name.to_s.humanize }
41
+ ) { scope_label(name) }
42
+ end
43
+
44
+ def scope_label(name)
45
+ Plutonium::Translation.label_for(:scope, current_query_object.resource_class, name) || name.to_s.humanize
42
46
  end
43
47
 
44
48
  def current_scope
@@ -9,7 +9,7 @@ module Plutonium
9
9
  return if scopes.empty?
10
10
 
11
11
  nav(role: "tablist",
12
- aria: {label: "Scope"},
12
+ aria: {label: t("plutonium.ui.table.scope")},
13
13
  class: "flex flex-nowrap items-center gap-1 px-4 py-2 border-b border-[var(--pu-border)] " \
14
14
  "overflow-x-auto whitespace-nowrap [scrollbar-width:none] [&::-webkit-scrollbar]:hidden") do
15
15
  render_all_pill
@@ -27,12 +27,12 @@ module Plutonium
27
27
  role: "tab",
28
28
  aria: {selected: active},
29
29
  class: pill_classes(active)
30
- ) { "All" }
30
+ ) { t("plutonium.ui.table.all") }
31
31
  end
32
32
 
33
33
  def render_pill(key)
34
34
  active = current_query_object.selected_scope.to_s == key.to_s
35
- label = key.to_s.humanize
35
+ label = Plutonium::Translation.label_for(:scope, current_query_object.resource_class, key) || key.to_s.humanize
36
36
 
37
37
  a(
38
38
  id: "#{key}-scope",
@@ -73,7 +73,7 @@ module Plutonium
73
73
  # Show X when no actions available for this record
74
74
  span(
75
75
  class: "inline-flex items-center justify-center size-4 text-[var(--pu-text-subtle)]",
76
- title: "No bulk actions available"
76
+ title: Plutonium::Translation.t("plutonium.ui.table.no_bulk_actions")
77
77
  ) { "✕" }
78
78
  else
79
79
  input(
@@ -69,7 +69,7 @@ module Plutonium
69
69
  data: {action: "click->filter-panel#toggle"}
70
70
  ) do
71
71
  render Phlex::TablerIcons::AdjustmentsHorizontal.new(class: "w-4 h-4 shrink-0")
72
- span { "Filter" }
72
+ span { t("plutonium.ui.table.filter") }
73
73
  if count > 0
74
74
  span(class: "ml-1 inline-flex items-center justify-center min-w-[18px] h-[18px] px-1 " \
75
75
  "rounded-full bg-primary-600 text-white text-[10px] font-semibold leading-none") do
@@ -90,7 +90,7 @@ module Plutonium
90
90
  type: "search",
91
91
  name: "#{@search_param}[search]",
92
92
  value: @search_value,
93
- placeholder: "Search...",
93
+ placeholder: t("plutonium.ui.table.search_placeholder"),
94
94
  class: "pu-input pu-input-toolbar pu-input-icon-left w-[220px]",
95
95
  # turbo-permanent + a stable id keep the DOM node
96
96
  # across Turbo morphs so focus / caret / IME state