feat(settings): add pagination to imports and exports pages (#598)

* feat(settings): split imports and exports

* feat(security): sanitize pagination params to prevent abuse

* fix(settings): fix syntax in settings nav

* feat(settings): internationalize family_exports and imports UI strings

* fix(settings): fix coderabbit review

* fix(settings): fix coderabbit review

* fix(settings): fix coderabbit review

* Change default per_page value from 20 to 10

Signed-off-by: Juan José Mata <jjmata@jjmata.com>

* Add `/family_export` to navigation

* Consistency with old defaults

* Align `safe_per_page` even if not DRY

---------

Signed-off-by: Julien Orain <julien.orain@gmail.com>
Signed-off-by: Juan José Mata <jjmata@jjmata.com>
Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: JulienOrain <your-github-email@example.com>
Co-authored-by: Juan José Mata <jjmata@jjmata.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
This commit is contained in:
Julien Orain
2026-01-20 00:11:22 +01:00
committed by GitHub
parent 3d91e60a8a
commit 777fbdc4ca
51 changed files with 353 additions and 105 deletions

View File

@@ -38,7 +38,7 @@ class AccountsController < ApplicationController
@q = params.fetch(:q, {}).permit(:search, status: []) @q = params.fetch(:q, {}).permit(:search, status: [])
entries = @account.entries.where(excluded: false).search(@q).reverse_chronological entries = @account.entries.where(excluded: false).search(@q).reverse_chronological
@pagy, @entries = pagy(entries, limit: params[:per_page] || "10") @pagy, @entries = pagy(entries, limit: safe_per_page)
@activity_feed_data = Account::ActivityFeedData.new(@account, @entries) @activity_feed_data = Account::ActivityFeedData.new(@account, @entries)
end end

View File

@@ -1,7 +1,7 @@
class ApplicationController < ActionController::Base class ApplicationController < ActionController::Base
include RestoreLayoutPreferences, Onboardable, Localize, AutoSync, Authentication, Invitable, include RestoreLayoutPreferences, Onboardable, Localize, AutoSync, Authentication, Invitable,
SelfHostable, StoreLocation, Impersonatable, Breadcrumbable, SelfHostable, StoreLocation, Impersonatable, Breadcrumbable,
FeatureGuardable, Notifiable FeatureGuardable, Notifiable, SafePagination
include Pundit::Authorization include Pundit::Authorization
include Pagy::Backend include Pagy::Backend

View File

@@ -27,7 +27,7 @@ module AccountableResource
@q = params.fetch(:q, {}).permit(:search) @q = params.fetch(:q, {}).permit(:search)
entries = @account.entries.search(@q).reverse_chronological entries = @account.entries.search(@q).reverse_chronological
@pagy, @entries = pagy(entries, limit: params[:per_page] || "10") @pagy, @entries = pagy(entries, limit: safe_per_page(10))
end end
def edit def edit

View File

@@ -0,0 +1,15 @@
# frozen_string_literal: true
module SafePagination
extend ActiveSupport::Concern
private
def safe_per_page(default = 10)
allowed_values = [ 10, 20, 30, 50, 100 ]
per_page = params[:per_page].to_i
return default if per_page <= 0
allowed_values.include?(per_page) ? per_page : allowed_values.min_by { |v| (v - per_page).abs }
end
end

View File

@@ -13,29 +13,33 @@ class FamilyExportsController < ApplicationController
FamilyDataExportJob.perform_later(@export) FamilyDataExportJob.perform_later(@export)
respond_to do |format| respond_to do |format|
format.html { redirect_to imports_path, notice: "Export started. You'll be able to download it shortly." } format.html { redirect_to family_exports_path, notice: t("family_exports.create.success") }
format.turbo_stream { format.turbo_stream {
stream_redirect_to imports_path, notice: "Export started. You'll be able to download it shortly." stream_redirect_to family_exports_path, notice: t("family_exports.create.success")
} }
end end
end end
def index def index
@exports = Current.family.family_exports.ordered.limit(10) @pagy, @exports = pagy(Current.family.family_exports.ordered, limit: safe_per_page)
render layout: false # For turbo frame @breadcrumbs = [
[ t("breadcrumbs.home"), root_path ],
[ t("breadcrumbs.exports"), family_exports_path ]
]
render layout: "settings"
end end
def download def download
if @export.downloadable? if @export.downloadable?
redirect_to @export.export_file, allow_other_host: true redirect_to @export.export_file, allow_other_host: true
else else
redirect_to imports_path, alert: "Export not ready for download" redirect_to family_exports_path, alert: t("family_exports.export_not_ready")
end end
end end
def destroy def destroy
@export.destroy @export.destroy
redirect_to imports_path, notice: "Export deleted successfully" redirect_to family_exports_path, notice: t("family_exports.destroy.success")
end end
private private
@@ -46,7 +50,7 @@ class FamilyExportsController < ApplicationController
def require_admin def require_admin
unless Current.user.admin? unless Current.user.admin?
redirect_to root_path, alert: "Access denied" redirect_to root_path, alert: t("family_exports.access_denied")
end end
end end
end end

View File

@@ -12,11 +12,10 @@ class ImportsController < ApplicationController
end end
def index def index
@imports = Current.family.imports @pagy, @imports = pagy(Current.family.imports.where(type: Import::TYPES).ordered, limit: safe_per_page)
@exports = Current.user.admin? ? Current.family.family_exports.ordered.limit(10) : nil
@breadcrumbs = [ @breadcrumbs = [
[ "Home", root_path ], [ t("breadcrumbs.home"), root_path ],
[ "Import/Export", imports_path ] [ t("breadcrumbs.imports"), imports_path ]
] ]
render layout: "settings" render layout: "settings"
end end
@@ -90,7 +89,7 @@ class ImportsController < ApplicationController
private private
def set_import def set_import
@import = Current.family.imports.find(params[:id]) @import = Current.family.imports.includes(:account).find(params[:id])
end end
def import_params def import_params

View File

@@ -20,7 +20,7 @@ class RulesController < ApplicationController
.recent .recent
.includes(:rule) .includes(:rule)
@pagy, @recent_runs = pagy(recent_runs_scope, limit: params[:per_page] || 20, page_param: :runs_page) @pagy, @recent_runs = pagy(recent_runs_scope, limit: safe_per_page, page_param: :runs_page)
render layout: "settings" render layout: "settings"
end end

View File

@@ -21,7 +21,7 @@ class TransactionsController < ApplicationController
:transfer_as_inflow, :transfer_as_outflow :transfer_as_inflow, :transfer_as_outflow
) )
@pagy, @transactions = pagy(base_scope, limit: per_page) @pagy, @transactions = pagy(base_scope, limit: safe_per_page)
# Load projected recurring transactions for next month # Load projected recurring transactions for next month
@projected_recurring = Current.family.recurring_transactions @projected_recurring = Current.family.recurring_transactions
@@ -281,10 +281,6 @@ class TransactionsController < ApplicationController
end end
private private
def per_page
params[:per_page].to_i.positive? ? params[:per_page].to_i : 20
end
def needs_rule_notification?(transaction) def needs_rule_notification?(transaction)
return false if Current.user.rule_prompts_disabled return false if Current.user.rule_prompts_disabled

View File

@@ -20,6 +20,7 @@ module SettingsHelper
{ name: "Self-Hosting", path: :settings_hosting_path, condition: :self_hosted_and_admin? }, { name: "Self-Hosting", path: :settings_hosting_path, condition: :self_hosted_and_admin? },
{ name: "Providers", path: :settings_providers_path, condition: :admin_user? }, { name: "Providers", path: :settings_providers_path, condition: :admin_user? },
{ name: "Imports", path: :imports_path, condition: :admin_user? }, { name: "Imports", path: :imports_path, condition: :admin_user? },
{ name: "Exports", path: :family_exports_path, condition: :admin_user? },
# More section # More section
{ name: "Guides", path: :settings_guides_path }, { name: "Guides", path: :settings_guides_path },
{ name: "What's new", path: :changelog_path }, { name: "What's new", path: :changelog_path },

View File

@@ -9,7 +9,7 @@
<div class="flex items-center justify-between mx-4 py-4"> <div class="flex items-center justify-between mx-4 py-4">
<div class="flex items-center gap-2 mb-1"> <div class="flex items-center gap-2 mb-1">
<div> <div>
<p class="text-sm font-medium text-primary">Export from <%= export.created_at.strftime("%B %d, %Y at %I:%M %p") %></p> <p class="text-sm font-medium text-primary"><%= t("family_exports.list.export_from", date: l(export.created_at, format: :long)) %></p>
<p class="text-xs text-secondary"><%= export.filename %></p> <p class="text-xs text-secondary"><%= export.filename %></p>
</div> </div>
@@ -31,7 +31,7 @@
<% if export.processing? || export.pending? %> <% if export.processing? || export.pending? %>
<div class="flex items-center gap-2 text-secondary"> <div class="flex items-center gap-2 text-secondary">
<div class="animate-spin h-4 w-4 border-2 border-secondary border-t-transparent rounded-full"></div> <div class="animate-spin h-4 w-4 border-2 border-secondary border-t-transparent rounded-full"></div>
<span class="text-sm">Exporting...</span> <span class="text-sm"><%= t("family_exports.exporting") %></span>
</div> </div>
<% elsif export.completed? %> <% elsif export.completed? %>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
@@ -39,7 +39,7 @@
method: :delete, method: :delete,
class: "flex items-center gap-2 text-destructive hover:text-destructive-hover", class: "flex items-center gap-2 text-destructive hover:text-destructive-hover",
data: { data: {
turbo_confirm: "Are you sure you want to delete this export? This action cannot be undone.", turbo_confirm: t("family_exports.delete_confirmation"),
turbo_frame: "_top" turbo_frame: "_top"
} do %> } do %>
<%= icon "trash-2", class: "w-5 h-5 text-destructive" %> <%= icon "trash-2", class: "w-5 h-5 text-destructive" %>
@@ -61,7 +61,7 @@
method: :delete, method: :delete,
class: "flex items-center gap-2 text-destructive hover:text-destructive-hover", class: "flex items-center gap-2 text-destructive hover:text-destructive-hover",
data: { data: {
turbo_confirm: "Are you sure you want to delete this failed export?", turbo_confirm: t("family_exports.delete_failed_confirmation"),
turbo_frame: "_top" turbo_frame: "_top"
} do %> } do %>
<%= icon "trash-2", class: "w-5 h-5 text-destructive" %> <%= icon "trash-2", class: "w-5 h-5 text-destructive" %>
@@ -71,7 +71,7 @@
</div> </div>
<% end %> <% end %>
<% else %> <% else %>
<p class="text-sm text-primary text-center py-4 mb-1 font-medium">No exports yet.</p> <p class="text-sm text-primary text-center py-4 mb-1 font-medium"><%= t("family_exports.index.no_exports") %></p>
<% end %> <% end %>
</div> </div>
<% end %> <% end %>

View File

@@ -1 +1,85 @@
<%= render "list", exports: @exports %> <%= settings_section title: t(".title") do %>
<div class="space-y-4">
<% if @exports.empty? %>
<p class="text-sm text-primary text-center py-4 mb-1 font-medium"><%= t(".no_exports") %></p>
<% else %>
<div class="bg-container rounded-lg shadow-border-xs">
<% @exports.each do |export| %>
<div class="flex items-center justify-between mx-4 py-4">
<div class="flex items-center gap-2 mb-1">
<div>
<p class="text-sm font-medium text-primary"><%= t("family_exports.list.export_from", date: l(export.created_at, format: :long)) %></p>
<p class="text-xs text-secondary"><%= export.filename %></p>
</div>
<% if export.processing? || export.pending? %>
<span class="px-1 py text-xs rounded-full bg-gray-500/5 text-secondary border border-alpha-black-50">
<%= t("family_exports.list.in_progress") %>
</span>
<% elsif export.completed? %>
<span class="px-1 py text-xs rounded-full bg-green-500/5 text-green-500 border border-alpha-black-50">
<%= t("family_exports.list.complete") %>
</span>
<% elsif export.failed? %>
<span class="px-1 py text-xs rounded-full bg-red-500/5 text-red-500 border border-alpha-black-50">
<%= t("family_exports.list.failed") %>
</span>
<% end %>
</div>
<% if export.processing? || export.pending? %>
<div class="flex items-center gap-2 text-secondary">
<div class="animate-spin h-4 w-4 border-2 border-secondary border-t-transparent rounded-full"></div>
<span class="text-sm"><%= t("family_exports.exporting") %></span>
</div>
<% elsif export.completed? %>
<div class="flex items-center gap-2">
<%= button_to family_export_path(export),
method: :delete,
class: "flex items-center gap-2 text-destructive hover:text-destructive-hover",
data: {
turbo_confirm: t("family_exports.delete_confirmation")
} do %>
<%= icon "trash-2", class: "w-5 h-5 text-destructive" %>
<% end %>
<%= link_to download_family_export_path(export),
class: "flex items-center gap-2 text-primary hover:text-primary-hover" do %>
<%= icon "download", class: "w-5 h-5" %>
<% end %>
</div>
<% elsif export.failed? %>
<div class="flex items-center gap-2">
<div class="flex items-center gap-2 text-destructive">
<%= icon "alert-circle", class: "w-4 h-4" %>
</div>
<%= button_to family_export_path(export),
method: :delete,
class: "flex items-center gap-2 text-destructive hover:text-destructive-hover",
data: {
turbo_confirm: t("family_exports.delete_failed_confirmation")
} do %>
<%= icon "trash-2", class: "w-5 h-5 text-destructive" %>
<% end %>
</div>
<% end %>
</div>
<% end %>
</div>
<% if @pagy.pages > 1 %>
<div class="mt-4">
<%= render "shared/pagination", pagy: @pagy %>
</div>
<% end %>
<% end %>
<%= link_to new_family_export_path,
class: "bg-container-inset flex items-center justify-center gap-2 text-secondary mt-1 hover:bg-container-inset-hover rounded-lg px-4 py-2 w-full text-center",
data: { turbo_frame: :modal } do %>
<%= icon("plus") %>
<%= t(".new") %>
<% end %>
</div>
<% end %>

View File

@@ -1,11 +1,17 @@
<%= settings_section title: t(".imports") do %> <%= settings_section title: t(".title") do %>
<div class="space-y-4"> <div class="space-y-4">
<% if @imports.empty? %> <% if @imports.empty? %>
<%= render partial: "imports/empty" %> <%= render partial: "imports/empty" %>
<% else %> <% else %>
<div class="bg-container rounded-lg shadow-border-xs"> <div class="bg-container rounded-lg shadow-border-xs">
<%= render partial: "imports/import", collection: @imports.ordered %> <%= render partial: "imports/import", collection: @imports %>
</div> </div>
<% if @pagy.pages > 1 %>
<div class="mt-4">
<%= render "shared/pagination", pagy: @pagy %>
</div>
<% end %>
<% end %> <% end %>
<%= link_to new_import_path, <%= link_to new_import_path,
@@ -16,24 +22,3 @@
<% end %> <% end %>
</div> </div>
<% end %> <% end %>
<% if Current.user.admin? %>
<%= settings_section title: t(".exports") do %>
<div class="space-y-4">
<div class="bg-container rounded-lg shadow-border-xs">
<%= turbo_frame_tag "family_exports", src: family_exports_path, loading: :lazy do %>
<div class="mt-4 text-center text-secondary py-8">
<div class="animate-spin inline-block h-4 w-4 border-2 border-secondary border-t-transparent rounded-full"></div>
</div>
<% end %>
</div>
<%= link_to new_family_export_path,
class: "bg-container-inset flex items-center justify-center gap-2 text-secondary mt-1 hover:bg-container-inset-hover rounded-lg px-4 py-2 w-full text-center",
data: { turbo_frame: :modal } do %>
<%= icon("plus") %>
<%= t(".new_export") %>
<% end %>
</div>
<% end %>
<% end %>

View File

@@ -31,6 +31,7 @@ nav_sections = [
{ label: t(".self_hosting_label"), path: settings_hosting_path, icon: "database", if: self_hosted? }, { label: t(".self_hosting_label"), path: settings_hosting_path, icon: "database", if: self_hosted? },
{ label: "Providers", path: settings_providers_path, icon: "plug" }, { label: "Providers", path: settings_providers_path, icon: "plug" },
{ label: t(".imports_label"), path: imports_path, icon: "download" }, { label: t(".imports_label"), path: imports_path, icon: "download" },
{ label: t(".exports_label"), path: family_exports_path, icon: "upload" },
{ label: "SSO Providers", path: admin_sso_providers_path, icon: "key-round", if: Current.user&.super_admin? }, { label: "SSO Providers", path: admin_sso_providers_path, icon: "key-round", if: Current.user&.super_admin? },
{ label: "Users", path: admin_users_path, icon: "users", if: Current.user&.super_admin? } { label: "Users", path: admin_users_path, icon: "users", if: Current.user&.super_admin? }
] ]

View File

@@ -50,7 +50,7 @@
</div> </div>
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<%= select_tag :per_page, <%= select_tag :per_page,
options_for_select(["10", "20", "30", "50"], pagy.limit), options_for_select(["10", "20", "30", "50", "100"], pagy.limit),
data: { controller: "selectable-link" }, data: { controller: "selectable-link" },
class: "py-1.5 pr-8 text-sm text-primary font-medium bg-container-inset border border-secondary rounded-lg focus:border-secondary focus:ring-secondary focus-visible:ring-secondary" %> class: "py-1.5 pr-8 text-sm text-primary font-medium bg-container-inset border border-secondary rounded-lg focus:border-secondary focus:ring-secondary focus-visible:ring-secondary" %>
</div> </div>

View File

@@ -0,0 +1,6 @@
---
ca:
breadcrumbs:
exports: Exportacions
home: Inici
imports: Importacions

View File

@@ -0,0 +1,6 @@
---
de:
breadcrumbs:
exports: Exporte
home: Startseite
imports: Importe

View File

@@ -0,0 +1,6 @@
---
en:
breadcrumbs:
exports: Exports
home: Home
imports: Imports

View File

@@ -0,0 +1,6 @@
---
es:
breadcrumbs:
exports: Exportaciones
home: Inicio
imports: Importaciones

View File

@@ -0,0 +1,6 @@
---
nb:
breadcrumbs:
exports: Eksporter
home: Hjem
imports: Importer

View File

@@ -0,0 +1,6 @@
---
pt-BR:
breadcrumbs:
exports: Exportações
home: Início
imports: Importações

View File

@@ -0,0 +1,6 @@
---
ro:
breadcrumbs:
exports: Exporturi
home: Acasă
imports: Importuri

View File

@@ -0,0 +1,6 @@
---
tr:
breadcrumbs:
exports: Dışa Aktarmalar
home: Ana Sayfa
imports: İçe Aktarmalar

View File

@@ -0,0 +1,6 @@
---
zh-CN:
breadcrumbs:
exports: 导出
home: 主页
imports: 导入

View File

@@ -1,7 +1,22 @@
--- ---
ca: ca:
family_exports: family_exports:
access_denied: Accés denegat
create:
success: Exportació iniciada. Podràs descarregar-la aviat.
delete_confirmation: Estàs segur que vols eliminar aquesta exportació? Aquesta acció no es pot desfer.
delete_failed_confirmation: Estàs segur que vols eliminar aquesta exportació fallida?
destroy:
success: Exportació eliminada amb èxit
export_not_ready: L'exportació no està llesta per descarregar
exporting: Exportant...
index:
title: Exportacions
new: Nova exportació
no_exports: Encara no hi ha exportacions.
list: list:
export_from: "Exportació del %{date}"
in_progress: En curs
complete: Completat complete: Completat
failed: Error failed: Error
in_progress: En curs in_progress: En curs

View File

@@ -1,7 +1,21 @@
--- ---
de: de:
family_exports: family_exports:
access_denied: Zugriff verweigert
create:
success: Export gestartet. Sie können ihn in Kürze herunterladen.
delete_confirmation: Möchten Sie diesen Export wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.
delete_failed_confirmation: Möchten Sie diesen fehlgeschlagenen Export wirklich löschen?
destroy:
success: Export erfolgreich gelöscht
export_not_ready: Export noch nicht zum Download bereit
exporting: Wird exportiert...
index:
title: Exporte
new: Neuer Export
no_exports: Noch keine Exporte vorhanden.
list: list:
export_from: "Export vom %{date}"
in_progress: Wird ausgeführt in_progress: Wird ausgeführt
complete: Abgeschlossen complete: Abgeschlossen
failed: Fehlgeschlagen failed: Fehlgeschlagen

View File

@@ -1,7 +1,21 @@
--- ---
en: en:
family_exports: family_exports:
access_denied: Access denied
create:
success: Export started. You'll be able to download it shortly.
delete_confirmation: Are you sure you want to delete this export? This action cannot be undone.
delete_failed_confirmation: Are you sure you want to delete this failed export?
destroy:
success: Export deleted successfully
export_not_ready: Export not ready for download
exporting: Exporting...
index:
title: Exports
new: New Export
no_exports: No exports yet.
list: list:
export_from: "Export from %{date}"
in_progress: In progress in_progress: In progress
complete: Complete complete: Complete
failed: Failed failed: Failed

View File

@@ -1,7 +1,21 @@
--- ---
es: es:
family_exports: family_exports:
access_denied: Acceso denegado
create:
success: Exportación iniciada. Podrás descargarla en breve.
delete_confirmation: ¿Estás seguro de que quieres eliminar esta exportación? Esta acción no se puede deshacer.
delete_failed_confirmation: ¿Estás seguro de que quieres eliminar esta exportación fallida?
destroy:
success: Exportación eliminada con éxito
export_not_ready: La exportación no está lista para descargar
exporting: Exportando...
index:
title: Exportaciones
new: Nueva exportación
no_exports: Aún no hay exportaciones.
list: list:
export_from: "Exportación del %{date}"
in_progress: En progreso in_progress: En progreso
complete: Completo complete: Completo
failed: Fallido failed: Fallido

View File

@@ -1,7 +1,21 @@
--- ---
nb: nb:
family_exports: family_exports:
access_denied: Tilgang nektet
create:
success: Eksport startet. Du vil kunne laste den ned snart.
delete_confirmation: Er du sikker på at du vil slette denne eksporten? Denne handlingen kan ikke angres.
delete_failed_confirmation: Er du sikker på at du vil slette denne mislykkede eksporten?
destroy:
success: Eksport slettet
export_not_ready: Eksport ikke klar for nedlasting
exporting: Eksporterer...
index:
title: Eksporter
new: Ny Eksport
no_exports: Ingen eksporter ennå.
list: list:
export_from: "Eksport fra %{date}"
in_progress: Pågår in_progress: Pågår
complete: Fullført complete: Fullført
failed: Mislykket failed: Mislykket

View File

@@ -1,7 +1,21 @@
--- ---
pt-BR: pt-BR:
family_exports: family_exports:
access_denied: Acesso negado
create:
success: Exportação iniciada. Você poderá baixá-la em breve.
delete_confirmation: Tem certeza de que deseja excluir esta exportação? Esta ação não pode ser desfeita.
delete_failed_confirmation: Tem certeza de que deseja excluir esta exportação falhada?
destroy:
success: Exportação excluída com sucesso
export_not_ready: Exportação não está pronta para download
exporting: Exportando...
index:
title: Exportações
new: Nova Exportação
no_exports: Nenhuma exportação ainda.
list: list:
in_progress: Em andamento export_from: "Exportação de %{date}"
in_progress: Em andamento
complete: Concluído complete: Concluído
failed: Falhou failed: Falhou

View File

@@ -1,7 +1,21 @@
--- ---
ro: ro:
family_exports: family_exports:
access_denied: Acces interzis
create:
success: Export început. Veți putea să-l descărcați în curând.
delete_confirmation: Sigur doriți să ștergeți acest export? Această acțiune nu poate fi anulată.
delete_failed_confirmation: Sigur doriți să ștergeți acest export eșuat?
destroy:
success: Export șters cu succes
export_not_ready: Exportul nu este gata pentru descărcare
exporting: Se exportă...
index:
title: Exporturi
new: Export nou
no_exports: Nu există încă exporturi.
list: list:
export_from: "Export din %{date}"
in_progress: În curs de desfășurare in_progress: În curs de desfășurare
complete: Finalizat complete: Finalizat
failed: Eșuat failed: Eșuat

View File

@@ -1,7 +1,21 @@
--- ---
tr: tr:
family_exports: family_exports:
access_denied: Erişim reddedildi
create:
success: Dışa aktarma başladı. Kısa süre içinde indirebileceksiniz.
delete_confirmation: Bu dışa aktarma işlemini silmek istediğinizden emin misiniz? Bu işlem geri alınamaz.
delete_failed_confirmation: Bu başarısız dışa aktarma işlemini silmek istediğinizden emin misiniz?
destroy:
success: Dışa aktarma başarıyla silindi
export_not_ready: Dışa aktarma henüz indirmeye hazır değil
exporting: Dışa aktarılıyor...
index:
title: Dışa aktarmalar
new: Yeni Dışa Aktarma
no_exports: Henüz hiç dışa aktarma yok.
list: list:
export_from: "%{date} tarihli dışa aktarma"
in_progress: Devam ediyor in_progress: Devam ediyor
complete: Tamamlandı complete: Tamamlandı
failed: Başarısız failed: Başarısız

View File

@@ -1,7 +1,21 @@
--- ---
zh-CN: zh-CN:
family_exports: family_exports:
access_denied: 访问被拒绝
create:
success: 导出已开始。您很快就能下载它。
delete_confirmation: 确定要删除此导出吗?此操作无法撤销。
delete_failed_confirmation: 确定要删除此失败的导出吗?
destroy:
success: 导出已成功删除
export_not_ready: 导出尚未准备好下载
exporting: 正在导出...
index:
title: 导出
new: 新建导出
no_exports: 暂无导出记录
list: list:
export_from: "导出自 %{date}"
complete: 已完成 complete: 已完成
failed: 已失败 failed: 已失败
in_progress: 进行中 in_progress: 进行中

View File

@@ -1,6 +1,12 @@
--- ---
zh-TW: zh-TW:
family_exports: family_exports:
access_denied: 存取被拒絕
create:
success: 匯出已開始。您很快就能下載它。
destroy:
success: 匯出已成功刪除
export_not_ready: 匯出尚未準備好下載
list: list:
complete: 已完成 complete: 已完成
failed: 已失敗 failed: 已失敗

View File

@@ -84,12 +84,12 @@ ca:
uploading: Processant files uploading: Processant files
view: Veure view: Veure
index: index:
title: Importacions
new: Nova importació
exports: Exportacions exports: Exportacions
imports: Importacions imports: Importacions
new: Nova importació
new_export: Nova exportació new_export: Nova exportació
no_exports: Encara no hi ha exportacions. no_exports: Encara no hi ha exportacions.
title: Importa/Exporta
new: new:
description: Pots importar manualment diversos tipus de dades via CSV o utilitzar description: Pots importar manualment diversos tipus de dades via CSV o utilitzar
una de les nostres plantilles d'importació com Mint. una de les nostres plantilles d'importació com Mint.

View File

@@ -58,12 +58,8 @@ de:
uploading: Zeilen werden verarbeitet uploading: Zeilen werden verarbeitet
view: Anzeigen view: Anzeigen
index: index:
imports: Importe title: Importe
new: Neuer Import new: Neuer Import
title: Import/Export
exports: Exporte
new_export: Neuer Export
no_exports: Noch keine Exporte vorhanden.
new: new:
description: Du kannst verschiedene Datentypen manuell über CSV importieren oder eine unserer Importvorlagen wie Mint verwenden. description: Du kannst verschiedene Datentypen manuell über CSV importieren oder eine unserer Importvorlagen wie Mint verwenden.
import_accounts: Konten importieren import_accounts: Konten importieren

View File

@@ -83,12 +83,8 @@ en:
uploading: Processing rows uploading: Processing rows
view: View view: View
index: index:
imports: Imports title: Imports
new: New Import new: New Import
title: Import/Export
exports: Exports
new_export: New Export
no_exports: No exports yet.
new: new:
description: You can manually import various types of data via CSV or use one description: You can manually import various types of data via CSV or use one
of our import templates like Mint. of our import templates like Mint.

View File

@@ -62,12 +62,8 @@ es:
uploading: Procesando filas uploading: Procesando filas
view: Ver view: Ver
index: index:
imports: Importaciones title: Importaciones
new: Nueva importación new: Nueva importación
title: Importar/Exportar
exports: Exportaciones
new_export: Nueva exportación
no_exports: Aún no hay exportaciones.
new: new:
description: Puedes importar manualmente varios tipos de datos mediante CSV o usar una de nuestras plantillas de importación como Mint. description: Puedes importar manualmente varios tipos de datos mediante CSV o usar una de nuestras plantillas de importación como Mint.
import_accounts: Importar cuentas import_accounts: Importar cuentas

View File

@@ -71,12 +71,8 @@ nb:
uploading: Behandler rader uploading: Behandler rader
view: Vis view: Vis
index: index:
imports: Importer
new: Ny Import
title: Importer title: Importer
exports: Eksporter new: Ny Import
new_export: Ny Eksport
no_exports: Ingen eksporter ennå.
new: new:
description: Du kan manuelt importere ulike typer data via CSV eller bruke en av description: Du kan manuelt importere ulike typer data via CSV eller bruke en av
våre importmaler som Mint. våre importmaler som Mint.

View File

@@ -76,12 +76,8 @@ pt-BR:
uploading: Processando linhas uploading: Processando linhas
view: Visualizar view: Visualizar
index: index:
imports: Importações title: Importações
new: Nova Importação new: Nova Importação
title: Importar/Exportar
exports: Exportações
new_export: Nova Exportação
no_exports: Nenhuma exportação ainda.
new: new:
description: Você pode importar manualmente vários tipos de dados via CSV ou usar um description: Você pode importar manualmente vários tipos de dados via CSV ou usar um
de nossos modelos de importação, como o do Mint. de nossos modelos de importação, como o do Mint.

View File

@@ -58,12 +58,8 @@ ro:
uploading: Se procesează rândurile uploading: Se procesează rândurile
view: Vezi view: Vezi
index: index:
imports: Importuri title: Importuri
new: Import nou new: Import nou
title: Import/Export
exports: Exporturi
new_export: Export nou
no_exports: Nu există încă exporturi.
new: new:
description: Poți importa manual diverse tipuri de date prin CSV sau poți folosi unul dintre șabloanele noastre de import, cum ar fi Mint. description: Poți importa manual diverse tipuri de date prin CSV sau poți folosi unul dintre șabloanele noastre de import, cum ar fi Mint.
import_accounts: Importă conturi import_accounts: Importă conturi

View File

@@ -58,12 +58,8 @@ tr:
uploading: Satırlar işleniyor uploading: Satırlar işleniyor
view: Görüntüle view: Görüntüle
index: index:
imports: İçe aktarmalar
new: Yeni İçe Aktarma
title: İçe aktarmalar title: İçe aktarmalar
exports: Dışa aktarmalar new: Yeni İçe Aktarma
new_export: Yeni Dışa Aktarma
no_exports: Henüz hiç dışa aktarma yok.
new: new:
description: Farklı veri türlerini CSV ile manuel olarak içe aktarabilir veya Mint gibi içe aktarma şablonlarımızı kullanabilirsiniz. description: Farklı veri türlerini CSV ile manuel olarak içe aktarabilir veya Mint gibi içe aktarma şablonlarımızı kullanabilirsiniz.
import_accounts: Hesapları içe aktar import_accounts: Hesapları içe aktar

View File

@@ -66,12 +66,8 @@ zh-CN:
uploading: 处理行数据中 uploading: 处理行数据中
view: 查看 view: 查看
index: index:
exports: 记录 title: 记录
imports: 导入记录
new: 新建导入 new: 新建导入
new_export: 新建导出
no_exports: 暂无导出记录
title: 导入/导出管理
new: new:
description: 您可以通过 CSV 手动导入多种类型数据,或使用我们的导入模板(如 Mint 格式)。 description: 您可以通过 CSV 手动导入多种类型数据,或使用我们的导入模板(如 Mint 格式)。
import_accounts: 导入账户 import_accounts: 导入账户

View File

@@ -127,8 +127,9 @@ ca:
categories_label: Categories categories_label: Categories
feedback_label: Feedback feedback_label: Feedback
general_section_title: General general_section_title: General
imports_label: Importacions
exports_label: Exportacions
guides_label: Guies guides_label: Guies
imports_label: Importa/Exporta
logout: Tanca la sessió logout: Tanca la sessió
merchants_label: Comerços merchants_label: Comerços
other_section_title: Més other_section_title: Més

View File

@@ -94,7 +94,8 @@ de:
categories_label: Kategorien categories_label: Kategorien
feedback_label: Feedback feedback_label: Feedback
general_section_title: Allgemein general_section_title: Allgemein
imports_label: Import/Export imports_label: Importe
exports_label: Exporte
logout: Abmelden logout: Abmelden
merchants_label: Händler merchants_label: Händler
guides_label: Anleitungen guides_label: Anleitungen

View File

@@ -115,7 +115,8 @@ en:
categories_label: Categories categories_label: Categories
feedback_label: Feedback feedback_label: Feedback
general_section_title: General general_section_title: General
imports_label: Import/Export imports_label: Imports
exports_label: Exports
logout: Logout logout: Logout
merchants_label: Merchants merchants_label: Merchants
guides_label: Guides guides_label: Guides

View File

@@ -95,7 +95,8 @@ es:
categories_label: Categorías categories_label: Categorías
feedback_label: Comentarios feedback_label: Comentarios
general_section_title: General general_section_title: General
imports_label: Importar/Exportar imports_label: Importaciones
exports_label: Exportaciones
logout: Cerrar sesión logout: Cerrar sesión
merchants_label: Comerciantes merchants_label: Comerciantes
guides_label: Guías guides_label: Guías

View File

@@ -81,6 +81,7 @@ nb:
feedback_label: Tilbakemelding feedback_label: Tilbakemelding
general_section_title: Generelt general_section_title: Generelt
imports_label: Importer imports_label: Importer
exports_label: Eksporter
logout: Logg ut logout: Logg ut
merchants_label: Forhandlere merchants_label: Forhandlere
other_section_title: Mer other_section_title: Mer

View File

@@ -91,6 +91,7 @@ pt-BR:
feedback_label: Feedback feedback_label: Feedback
general_section_title: Geral general_section_title: Geral
imports_label: Importações imports_label: Importações
exports_label: Exportações
logout: Sair logout: Sair
merchants_label: Comerciantes merchants_label: Comerciantes
other_section_title: Mais other_section_title: Mais

View File

@@ -98,7 +98,8 @@ ro:
categories_label: Categorii categories_label: Categorii
feedback_label: Feedback feedback_label: Feedback
general_section_title: General general_section_title: General
imports_label: Import/Export imports_label: Importuri
exports_label: Exporturi
logout: Logout logout: Logout
merchants_label: Comercianți merchants_label: Comercianți
guides_label: Ghiduri guides_label: Ghiduri

View File

@@ -101,7 +101,8 @@ zh-CN:
feedback_label: 意见反馈 feedback_label: 意见反馈
general_section_title: 通用设置 general_section_title: 通用设置
guides_label: 使用指南 guides_label: 使用指南
imports_label: 导入/导出 imports_label: 导入记录
exports_label: 导出记录
logout: 退出登录 logout: 退出登录
merchants_label: 商户管理 merchants_label: 商户管理
other_section_title: 更多设置 other_section_title: 更多设置

View File

@@ -33,7 +33,7 @@ class FamilyExportsControllerTest < ActionDispatch::IntegrationTest
post family_exports_path post family_exports_path
end end
assert_redirected_to imports_path assert_redirected_to family_exports_path
assert_equal "Export started. You'll be able to download it shortly.", flash[:notice] assert_equal "Export started. You'll be able to download it shortly.", flash[:notice]
export = @family.family_exports.last export = @family.family_exports.last
@@ -67,7 +67,7 @@ class FamilyExportsControllerTest < ActionDispatch::IntegrationTest
export = @family.family_exports.create!(status: "processing") export = @family.family_exports.create!(status: "processing")
get download_family_export_path(export) get download_family_export_path(export)
assert_redirected_to imports_path assert_redirected_to family_exports_path
assert_equal "Export not ready for download", flash[:alert] assert_equal "Export not ready for download", flash[:alert]
end end
@@ -78,7 +78,7 @@ class FamilyExportsControllerTest < ActionDispatch::IntegrationTest
delete family_export_path(export) delete family_export_path(export)
end end
assert_redirected_to imports_path assert_redirected_to family_exports_path
assert_equal "Export deleted successfully", flash[:notice] assert_equal "Export deleted successfully", flash[:notice]
end end
@@ -95,7 +95,7 @@ class FamilyExportsControllerTest < ActionDispatch::IntegrationTest
delete family_export_path(export) delete family_export_path(export)
end end
assert_redirected_to imports_path assert_redirected_to family_exports_path
assert_equal "Export deleted successfully", flash[:notice] assert_equal "Export deleted successfully", flash[:notice]
end end
@@ -112,7 +112,7 @@ class FamilyExportsControllerTest < ActionDispatch::IntegrationTest
delete family_export_path(export) delete family_export_path(export)
end end
assert_redirected_to imports_path assert_redirected_to family_exports_path
assert_equal "Export deleted successfully", flash[:notice] assert_equal "Export deleted successfully", flash[:notice]
end end