diff --git a/Gemfile b/Gemfile index 0e373cc1..921484e4 100644 --- a/Gemfile +++ b/Gemfile @@ -34,6 +34,7 @@ gem 'omniauth-oauth', '~> 1.2' # OAuth 1.0 gem 'omniauth-rails_csrf_protection', '~> 2.0' # OAuth CSRF compatibility gem 'pundit', '~> 2.5.2' # For roles managements gem 'rack-attack', '~> 6.8.0' # For throttling incoming request in authentication +gem 'rails-i18n', '~> 8.0' # For internationalization [https://github.com/svenfuchs/rails-i18n] gem 'rondo_form', '~> 1.0' # For nested forms gem 'rqrcode', '~> 3.0' # For generating QR codes gem 'tzinfo-data', '~> 1', platforms: %i[windows jruby] # Windows does not include zoneinfo files, so bundle the tzinfo-data gem diff --git a/Gemfile.lock b/Gemfile.lock index 30b129f6..ab484aba 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -321,6 +321,9 @@ GEM rails-html-sanitizer (1.7.1) loofah (~> 2.25, >= 2.25.2) nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) + rails-i18n (8.1.0) + i18n (>= 0.7, < 2) + railties (>= 8.0.0, < 9) railties (8.1.3.1) actionpack (= 8.1.3.1) activesupport (= 8.1.3.1) @@ -478,6 +481,7 @@ DEPENDENCIES pundit (~> 2.5.2) rack-attack (~> 6.8.0) rails (~> 8.1.3.1) + rails-i18n (~> 8.0) rondo_form (~> 1.0) rqrcode (~> 3.0) rubocop (~> 1.84) @@ -615,6 +619,7 @@ CHECKSUMS rails (8.1.3.1) sha256=ccd11a36bfc171bf9c66d585d14c0ece91c0c9dde840aae60c0118d6f5c9c52a rails-dom-testing (2.3.0) sha256=8acc7953a7b911ca44588bf08737bc16719f431a1cc3091a292bca7317925c1d rails-html-sanitizer (1.7.1) sha256=e797a7c9b01e567307e317c576b49ab4168017e63eea4dba9ce3cb587e2f22c2 + rails-i18n (8.1.0) sha256=52d5fd6c0abef28d84223cc05647f6ae0fd552637a1ede92deee9545755b6cf3 railties (8.1.3.1) sha256=2388a232579a00cefea4487de66c8553c3408c1300abdc6cf1799d86ffb04487 rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a rake (13.3.1) sha256=8c9e89d09f66a26a01264e7e3480ec0607f0c497a861ef16063604b1b08eb19c diff --git a/app/components/shared/header_component.html.haml b/app/components/shared/header_component.html.haml index e75f5788..9fe54d2f 100644 --- a/app/components/shared/header_component.html.haml +++ b/app/components/shared/header_component.html.haml @@ -1,12 +1,12 @@ %header.gh-topbar - = link_to home_path, class: 'gh-topbar-logo', 'aria-label': 'Gamification Hub, start' do + = link_to home_path, class: 'gh-topbar-logo', 'aria-label': t('layouts.header.home') do = image_tag 'logo.png', alt: '' %span.gh-topbar-logo-word Gamification Hub - if group_chrome %button.gh-group-switcher{ type: 'button', data: { action: 'menu#open', menu_id_param: 'gh-group-switcher' }, - 'aria-label': "Grupa #{group_chrome.name}, zmień grupę" } + 'aria-label': t('layouts.header.switch_group', name: group_chrome.name) } = render 'layouts/group_thumb', story_group: group_chrome.story_group, size: nil %span.gh-group-switcher-name= group_chrome.name %i.fa-solid.fa-chevron-down{ 'aria-hidden': 'true' } @@ -17,21 +17,21 @@ - purse = group_chrome.student_membership = link_to story_group_student_currency_transactions_path(group_chrome.story_group, purse), class: 'gh-balance-button', - title: "#{purse.current_currency} do wydania, #{purse.total_currency} zebrane łącznie", - 'aria-label': "Masz #{purse.current_currency} do wydania, pokaż historię waluty" do + title: t('layouts.header.balance_title', balance: purse.current_currency, total: purse.total_currency), + 'aria-label': t('layouts.header.balance_label', balance: purse.current_currency) do = render 'layouts/currency_token', story_group: group_chrome.story_group, size: 32 %span %b= purse.current_currency - %small do wydania + %small= t('layouts.header.to_spend') = link_to new_join_path, class: 'gh-btn gh-btn--secondary gh-btn--small gh-topbar-join-link', data: { turbo_frame: 'modal' } do %i.fa-solid.fa-plus - %span Dołącz do grupy + %span= t('layouts.header.join') %button.gh-icon-button.gh-topbar-theme-toggle{ type: 'button', data: { action: 'theme#toggle' }, - 'aria-label': 'Przełącz motyw' } + 'aria-label': t('layouts.theme.toggle') } %i.fa-solid.fa-moon.gh-theme-icon-moon %i.fa-solid.fa-sun.gh-theme-icon-sun @@ -44,6 +44,6 @@ %button.gh-avatar-button{ type: 'button', data: { action: 'menu#open', menu_id_param: 'gh-account-menu' }, - 'aria-label': "Konto: #{user.full_name}" } + 'aria-label': t('layouts.header.account', name: user.full_name) } %span.gh-avatar.gh-octagon-clip{ 'data-gh-h': helpers.gh_avatar_hue(user.full_name), 'aria-hidden': 'true' } = helpers.gh_initials(user.full_name) diff --git a/app/components/shared/header_component.rb b/app/components/shared/header_component.rb index c25d2d9b..69345a59 100644 --- a/app/components/shared/header_component.rb +++ b/app/components/shared/header_component.rb @@ -20,8 +20,8 @@ def unread_count end def bell_label - return 'Powiadomienia' if unread_count.zero? + return t('layouts.header.notifications') if unread_count.zero? - "Powiadomienia, #{unread_count} #{helpers.gh_plural(unread_count, 'nowe', 'nowe', 'nowych')}" + t('layouts.header.notifications_unread', count: unread_count) end end diff --git a/app/components/shared/sidebar_component.html.haml b/app/components/shared/sidebar_component.html.haml index 3b9fd3f7..08ac4d12 100644 --- a/app/components/shared/sidebar_component.html.haml +++ b/app/components/shared/sidebar_component.html.haml @@ -1,6 +1,6 @@ %aside.gh-sidebar{ class: ('gh-sidebar--collapsed' if collapsed), data: { sidebar_target: 'sidebar' } } - %nav.gh-sidebar-nav.gh-sidebar-nav--primary{ 'aria-label': 'Główna' } + %nav.gh-sidebar-nav.gh-sidebar-nav--primary{ 'aria-label': t('layouts.sidebar.main') } - nav_items.each do |item| = link_to item.path, class: "gh-sidebar-link #{'gh-sidebar-link--active' if current?(item.path)}", title: item.label, 'aria-current': ('page' if current?(item.path)) do @@ -10,7 +10,7 @@ - if group_chrome = render 'layouts/deck', chrome: group_chrome, current_path: current_path - elsif groups.any? - %p.gh-sidebar-section-label Twoje grupy + %p.gh-sidebar-section-label= t('layouts.sidebar.your_groups') .gh-sidebar-group-list - groups.each do |story_group| = link_to story_group_path(story_group), class: 'gh-sidebar-group-list-item', title: story_group.name do diff --git a/app/components/shared/sidebar_component.rb b/app/components/shared/sidebar_component.rb index 189cd664..52885048 100644 --- a/app/components/shared/sidebar_component.rb +++ b/app/components/shared/sidebar_component.rb @@ -25,7 +25,7 @@ def groups end def role_label(story_group) - StoryGroupsListing::ROLE_LABELS[role_for(story_group)] + t("story_groups.listing.roles.#{role_for(story_group)}") end def role_for(story_group) @@ -44,7 +44,7 @@ def current?(path) end def toggle_label - collapsed ? 'Rozwiń' : 'Zwiń panel' + collapsed ? t('layouts.sidebar.expand') : t('layouts.sidebar.collapse') end def toggle_icon diff --git a/app/components/shared/tab_bar_component.html.haml b/app/components/shared/tab_bar_component.html.haml index 7fb3437a..cfae9664 100644 --- a/app/components/shared/tab_bar_component.html.haml +++ b/app/components/shared/tab_bar_component.html.haml @@ -1,4 +1,4 @@ -%nav.gh-mobile-tabbar{ 'aria-label': 'Nawigacja' } +%nav.gh-mobile-tabbar{ 'aria-label': t('layouts.tab_bar.label') } - items.each do |item| - on = item.current?(current_path) = link_to item.path, class: "gh-mobile-tab #{'gh-mobile-tab--active' if on}", @@ -10,4 +10,4 @@ %button.gh-mobile-tab{ type: 'button', data: { action: 'menu#open', menu_id_param: 'gh-more-sheet' } } %i.fa-solid.fa-ellipsis - %span Więcej + %span= t('shared.actions.more') diff --git a/app/controllers/account_setups_controller.rb b/app/controllers/account_setups_controller.rb index 3f5d9677..3d0fffd1 100644 --- a/app/controllers/account_setups_controller.rb +++ b/app/controllers/account_setups_controller.rb @@ -12,7 +12,7 @@ def update @current_user.first_login = false if @current_user.save(context: :account_setup) - redirect_to home_path, notice: 'Pomyślnie skonfigurowano konto.' + redirect_to home_path, notice: t('account_setups.flash.done') else @current_user.first_login = true render :edit, status: :unprocessable_content diff --git a/app/controllers/activity_group_templates_controller.rb b/app/controllers/activity_group_templates_controller.rb index c426c45c..c58e6997 100644 --- a/app/controllers/activity_group_templates_controller.rb +++ b/app/controllers/activity_group_templates_controller.rb @@ -11,7 +11,9 @@ class ActivityGroupTemplatesController < ApplicationController def new @activity_group_template = @story_group.activity_group_templates.build - @activity_group_template.categories.build(didactic_description: 'Obecność', reward: 2, position: 0) + @activity_group_template.categories.build(didactic_description: t('activity_group_templates.default_category'), + reward: 2, + position: 0,) @activity_group_template.categories.build(reward: 1, position: 1) end @@ -28,7 +30,8 @@ def create if @activity_group_template.save(context: :settings) redirect_to story_group_activity_groups_path(@story_group), - notice: "Utworzono szablon „#{@activity_group_template.base_name}”.", status: :see_other + notice: t('activity_group_templates.flash.created', name: @activity_group_template.base_name), + status: :see_other else render :new, status: :unprocessable_content end @@ -39,7 +42,7 @@ def update if @activity_group_template.save(context: :settings) redirect_to story_group_activity_groups_path(@story_group), - notice: 'Zapisano szablon. Nowe arkusze dostaną te kategorie.', status: :see_other + notice: t('activity_group_templates.flash.updated'), status: :see_other else @sheet_names = sheet_names_for(@activity_group_template) render :edit, status: :unprocessable_content @@ -51,7 +54,7 @@ def destroy @activity_group_template.soft_delete! redirect_outside_turbo_frame story_group_activity_groups_path(@story_group), - notice: "Usunięto szablon „#{name}”. Utworzone z niego arkusze zostają." + notice: t('activity_group_templates.flash.destroyed', name: name) end private diff --git a/app/controllers/activity_groups_controller.rb b/app/controllers/activity_groups_controller.rb index 5d6f34f2..91f642cb 100644 --- a/app/controllers/activity_groups_controller.rb +++ b/app/controllers/activity_groups_controller.rb @@ -43,7 +43,7 @@ def update if @activity_group.save(context: :settings) redirect_to story_group_activity_groups_path(@story_group), - notice: "Zapisano ustawienia arkusza #{@activity_group.name}.", status: :see_other + notice: t('activity_groups.flash.updated', name: @activity_group.name), status: :see_other else render :edit, status: :unprocessable_content end @@ -54,8 +54,7 @@ def destroy @activity_group.soft_delete! redirect_outside_turbo_frame story_group_activity_groups_path(@story_group), - notice: "Usunięto arkusz #{name}. " \ - 'Przyznane nagrody zostają w historii studentów.' + notice: t('activity_groups.flash.destroyed', name: name) end private @@ -87,9 +86,9 @@ def bulk_count end def created_notice(sheets) - return "Utworzono: #{sheets.first.name}." if sheets.one? + return t('activity_groups.flash.created_one', name: sheets.first.name) if sheets.one? - "Utworzono: #{sheets.first.name} – #{sheets.last.name}." + t('activity_groups.flash.created_many', first: sheets.first.name, last: sheets.last.name) end def columns_changed? diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 0e24dbc7..6406ac23 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -2,6 +2,7 @@ class ApplicationController < ActionController::Base include Pundit::Authorization + include Localization include Authentication include Sidebar include StatusRedirects diff --git a/app/controllers/auth/passwordless_controller.rb b/app/controllers/auth/passwordless_controller.rb index e29ed773..482e48ea 100644 --- a/app/controllers/auth/passwordless_controller.rb +++ b/app/controllers/auth/passwordless_controller.rb @@ -30,7 +30,7 @@ def verify token = params.expect(:token) login_token = LoginToken.find_by_token(token) - return redirect_to login_path, alert: 'Nieprawidłowy token' if login_token.nil? || login_token.expired? + return redirect_to login_path, alert: t('auth.flash.invalid_token') if login_token.nil? || login_token.expired? user = login_token.user user.consume_login_token! @@ -61,7 +61,7 @@ def create session[:pending_login_email] = email session[:pending_login_sent_at] = Time.current.to_i - notice = 'Wysłaliśmy nowy link. Poprzedni już nie działa.' if resend + notice = t('auth.flash.resent') if resend redirect_to auth_passwordless_inbox_path, notice: notice end diff --git a/app/controllers/auth/usos_controller.rb b/app/controllers/auth/usos_controller.rb index 474d8ac9..6536820b 100644 --- a/app/controllers/auth/usos_controller.rb +++ b/app/controllers/auth/usos_controller.rb @@ -16,9 +16,9 @@ def create sign_in_and_redirect(user) rescue InvalidProviderError - redirect_to root_path, alert: 'Nieprawidłowy dostawca logowania.' + redirect_to root_path, alert: t('auth.flash.invalid_provider') rescue Providers::InvalidAuthError - redirect_to root_path, alert: 'Nieprawidłowa próba autoryzacji.' + redirect_to root_path, alert: t('auth.flash.invalid_auth') end private diff --git a/app/controllers/badges_controller.rb b/app/controllers/badges_controller.rb index f02ab869..6ecd0760 100644 --- a/app/controllers/badges_controller.rb +++ b/app/controllers/badges_controller.rb @@ -32,7 +32,7 @@ def create @badge = @story_group.badges.build(badge_params) if @badge.save - redirect_to story_group_badges_path(@story_group), notice: "Dodano odznakę „#{@badge.name}”." + redirect_to story_group_badges_path(@story_group), notice: t('badges.flash.created', name: @badge.name) else set_shelf render :new, status: :unprocessable_content @@ -41,7 +41,7 @@ def create def update if @badge.update(badge_params) - redirect_to story_group_badges_path(@story_group), notice: "Zapisano odznakę „#{@badge.name}”." + redirect_to story_group_badges_path(@story_group), notice: t('badges.flash.updated', name: @badge.name) else set_shelf render :edit, status: :unprocessable_content @@ -53,8 +53,7 @@ def destroy @badge.soft_delete! redirect_outside_turbo_frame story_group_badges_path(@story_group), - notice: "Usunięto odznakę „#{name}”. Studenci, " \ - 'którzy ją mają, zachowują ją w historii.' + notice: t('badges.flash.destroyed', name: name) end private diff --git a/app/controllers/concerns/auth/sign_in.rb b/app/controllers/concerns/auth/sign_in.rb index 907f2b0e..4b8164a3 100644 --- a/app/controllers/concerns/auth/sign_in.rb +++ b/app/controllers/concerns/auth/sign_in.rb @@ -10,7 +10,7 @@ def sign_in_and_redirect(user) reset_session session[:user_id] = user.id - redirect_to after_login_path(return_to), notice: 'Zalogowano pomyślnie.' + redirect_to after_login_path(return_to), notice: t('auth.flash.signed_in') end def after_login_path(path) diff --git a/app/controllers/concerns/authentication.rb b/app/controllers/concerns/authentication.rb index d41faabe..e35fb53b 100644 --- a/app/controllers/concerns/authentication.rb +++ b/app/controllers/concerns/authentication.rb @@ -21,7 +21,7 @@ def authenticate! return if @current_user store_return_path - redirect_to login_path, alert: 'Zaloguj się, aby kontynuować.' + redirect_to login_path, alert: t('application.flash.log_in_first') end def store_return_path diff --git a/app/controllers/concerns/localization.rb b/app/controllers/concerns/localization.rb new file mode 100644 index 00000000..8d3d38f0 --- /dev/null +++ b/app/controllers/concerns/localization.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Localization + extend ActiveSupport::Concern + + LOCALE_COOKIE = :locale + + included do + around_action :switch_locale + end + + protected + + # A block keeps the locale from leaking into the next request served by the same thread. + def switch_locale(&) + I18n.with_locale(requested_locale, &) + end + + def requested_locale + [cookies[LOCALE_COOKIE], browser_locale].find { |locale| available_locale?(locale) } || I18n.default_locale + end + + # "en-US,en;q=0.9,pl;q=0.8" -> "en". Browsers list languages in order of preference. + def browser_locale + request.headers['Accept-Language'].to_s.split(',') + .map { |entry| entry.strip.first(2).downcase } + .find { |locale| available_locale?(locale) } + end + + def available_locale?(locale) + I18n.available_locales.map(&:to_s).include?(locale.to_s) + end +end diff --git a/app/controllers/concerns/logged_user_redirector.rb b/app/controllers/concerns/logged_user_redirector.rb index 434acf22..9dca80a1 100644 --- a/app/controllers/concerns/logged_user_redirector.rb +++ b/app/controllers/concerns/logged_user_redirector.rb @@ -2,6 +2,6 @@ module LoggedUserRedirector def redirect_logged_user - redirect_to root_path, alert: 'Jesteś już zalogowany.' if @current_user + redirect_to root_path, alert: t('application.flash.already_signed_in') if @current_user end end diff --git a/app/controllers/concerns/status_redirects.rb b/app/controllers/concerns/status_redirects.rb index 69180f2c..a6543ffd 100644 --- a/app/controllers/concerns/status_redirects.rb +++ b/app/controllers/concerns/status_redirects.rb @@ -4,10 +4,10 @@ module StatusRedirects protected def record_not_found - redirect_to root_path, alert: 'Nie znaleziono.' + redirect_to root_path, alert: t('application.flash.not_found') end def not_authorized - redirect_to root_path, alert: 'Nie znaleziono.' + redirect_to root_path, alert: t('application.flash.not_found') end end diff --git a/app/controllers/currency_adjustments_controller.rb b/app/controllers/currency_adjustments_controller.rb index 69e60cfa..1fb30ee1 100644 --- a/app/controllers/currency_adjustments_controller.rb +++ b/app/controllers/currency_adjustments_controller.rb @@ -38,8 +38,8 @@ def set_presentation end def notice_for(form) - verb = form.positive? ? 'Dodano' : 'Odjęto' + key = form.positive? ? 'added' : 'taken' - "#{verb} #{form.value}. Saldo: #{@student.reload.current_currency}." + t("currency_adjustments.flash.#{key}", value: form.value, balance: @student.reload.current_currency) end end diff --git a/app/controllers/currency_transactions_controller.rb b/app/controllers/currency_transactions_controller.rb index 5ed2ac1c..f483a43e 100644 --- a/app/controllers/currency_transactions_controller.rb +++ b/app/controllers/currency_transactions_controller.rb @@ -31,7 +31,7 @@ def set_student end def ledger_kind - kinds = CurrencyLedger::KINDS.map(&:first).compact + kinds = CurrencyLedger::KINDS.compact kinds.include?(params[:kind]) ? params[:kind] : nil end end diff --git a/app/controllers/items_controller.rb b/app/controllers/items_controller.rb index 27a7f866..325a3e12 100644 --- a/app/controllers/items_controller.rb +++ b/app/controllers/items_controller.rb @@ -32,7 +32,7 @@ def create if @item.save redirect_to story_group_items_path(@story_group), - notice: "Dodano przedmiot „#{@item.name}” do sklepu." + notice: t('items.flash.created', name: @item.name) else set_shelf render :new, status: :unprocessable_content @@ -42,7 +42,7 @@ def create def update if @item.update(item_params) redirect_to story_group_items_path(@story_group), - notice: "Zapisano „#{@item.name}”. Zmiany dotyczą nowych zakupów." + notice: t('items.flash.updated', name: @item.name) else set_shelf render :edit, status: :unprocessable_content @@ -54,8 +54,7 @@ def destroy @item.soft_delete! redirect_outside_turbo_frame story_group_items_path(@story_group), - notice: "Usunięto „#{name}” z oferty. " \ - 'Kupione egzemplarze zostają u studentów.' + notice: t('items.flash.destroyed', name: name) end private diff --git a/app/controllers/locales_controller.rb b/app/controllers/locales_controller.rb new file mode 100644 index 00000000..f037647d --- /dev/null +++ b/app/controllers/locales_controller.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class LocalesController < ApplicationController + skip_before_action :authenticate! + + def update + locale = params[:locale].to_s + cookies.permanent[Localization::LOCALE_COOKIE] = locale if available_locale?(locale) + + redirect_back_or_to root_path, status: :see_other + end +end diff --git a/app/controllers/organizations/admins_controller.rb b/app/controllers/organizations/admins_controller.rb index 04265fa0..0eb48316 100644 --- a/app/controllers/organizations/admins_controller.rb +++ b/app/controllers/organizations/admins_controller.rb @@ -10,7 +10,7 @@ def confirm_destroy; end def destroy @admin.destroy! - redirect_after_destroy notice: "Usunięto administratora: #{display_name}." + redirect_after_destroy notice: t('organizations.flash.admin_removed', name: display_name) end private diff --git a/app/controllers/organizations/invitations_controller.rb b/app/controllers/organizations/invitations_controller.rb index de787bdd..5aaa0675 100644 --- a/app/controllers/organizations/invitations_controller.rb +++ b/app/controllers/organizations/invitations_controller.rb @@ -13,14 +13,14 @@ def create send_invitation(user, create_invitation_token(user)) redirect_to organization_path(organization), status: :see_other, - notice: "Wysłano ponownie zaproszenie: #{user.email}." + notice: t('organizations.flash.invitation_resent', email: user.email) end private def too_many_resends redirect_to organization_path(params[:organization_id]), - alert: 'Zaproszenie zostało już wysłane kilka razy. Spróbuj ponownie za kilka minut.', + alert: t('organizations.flash.too_many_resends'), status: :see_other end end diff --git a/app/controllers/organizations/users_controller.rb b/app/controllers/organizations/users_controller.rb index 6c4da1c5..e2d5edb9 100644 --- a/app/controllers/organizations/users_controller.rb +++ b/app/controllers/organizations/users_controller.rb @@ -13,7 +13,7 @@ def update @user.assign_attributes(user_params) if @user.save(context: :user_invitation) - redirect_after_save notice: "Zapisano zmiany: #{@user.full_name}." + redirect_after_save notice: t('organizations.flash.user_updated', name: @user.full_name) else render :edit, status: :unprocessable_content end @@ -25,7 +25,7 @@ def destroy name = @user.full_name @user.destroy! - redirect_after_save notice: "Usunięto użytkownika: #{name}." + redirect_after_save notice: t('organizations.flash.user_removed', name: name) end private diff --git a/app/controllers/organizations_controller.rb b/app/controllers/organizations_controller.rb index 09e57a53..be7c1698 100644 --- a/app/controllers/organizations_controller.rb +++ b/app/controllers/organizations_controller.rb @@ -37,7 +37,7 @@ def create authorize @organization if @organization.save - redirect_after_save organization_path(@organization), notice: 'Pomyślnie utworzono organizację.' + redirect_after_save organization_path(@organization), notice: t('organizations.flash.created') else render :new, status: :unprocessable_content end @@ -51,7 +51,7 @@ def update authorize @organization if @organization.update(organization_params) - redirect_after_save organization_path(@organization), notice: 'Pomyślnie zaktualizowano organizację.' + redirect_after_save organization_path(@organization), notice: t('organizations.flash.updated') else render :edit, status: :unprocessable_content end @@ -70,7 +70,7 @@ def destroy end @organization.destroy! - redirect_after_save organizations_path, notice: "Usunięto organizację #{@organization.name}." + redirect_after_save organizations_path, notice: t('organizations.flash.destroyed', name: @organization.name) end def new_admin @@ -96,7 +96,7 @@ def add_admin if token send_invitation(@org_admin, token) redirect_after_save organization_path(@organization), - notice: 'Pomyślnie dodano administratora organizacji. Wysłano zaproszenie na email.' + notice: t('organizations.flash.admin_added') else render :new_admin, status: :unprocessable_content end @@ -124,7 +124,7 @@ def add_user if token send_invitation(@new_user, token) redirect_after_save organization_path(@organization), - notice: 'Pomyślnie dodano użytkownika do organizacji. Wysłano zaproszenie na email.' + notice: t('organizations.flash.user_added') else render :new_user, status: :unprocessable_content end diff --git a/app/controllers/ranking_controller.rb b/app/controllers/ranking_controller.rb index cc43c075..85ad1297 100644 --- a/app/controllers/ranking_controller.rb +++ b/app/controllers/ranking_controller.rb @@ -46,10 +46,10 @@ def ranking_params end def notice_for_update - return 'Ranking ukryty przed studentami.' unless @story_group.ranking_enabled? - return 'Ranking widoczny w całości.' if @story_group.ranking_full? + return t('ranking.flash.hidden') unless @story_group.ranking_enabled? + return t('ranking.flash.full') if @story_group.ranking_full? - 'Ranking widoczny: podium i własne miejsce.' + t('ranking.flash.podium') end def gh_group_chrome diff --git a/app/controllers/ranks_controller.rb b/app/controllers/ranks_controller.rb index 3c50c5fe..1f898c2d 100644 --- a/app/controllers/ranks_controller.rb +++ b/app/controllers/ranks_controller.rb @@ -33,7 +33,7 @@ def create @rank = @story_group.ranks.build(rank_params) if @rank.save - redirect_to story_group_ranks_path(@story_group), notice: "Dodano rangę „#{@rank.name}”." + redirect_to story_group_ranks_path(@story_group), notice: t('ranks.flash.created', name: @rank.name) else set_ladder render :new, status: :unprocessable_content @@ -42,7 +42,7 @@ def create def update if @rank.update(rank_params) - redirect_to story_group_ranks_path(@story_group), notice: "Zapisano rangę „#{@rank.name}”." + redirect_to story_group_ranks_path(@story_group), notice: t('ranks.flash.updated', name: @rank.name) else set_ladder render :edit, status: :unprocessable_content @@ -55,13 +55,13 @@ def destroy if blockers.any? return redirect_outside_turbo_frame story_group_ranks_path(@story_group), - alert: "Nie można usunąć rangi „#{name}”: wymagają jej " \ - "przedmioty #{helpers.gh_and_list(blockers)}." + alert: t('ranks.flash.blocked', name: name, + items: helpers.gh_and_list(blockers),) end @rank.destroy redirect_outside_turbo_frame story_group_ranks_path(@story_group), - notice: "Usunięto rangę „#{name}”." + notice: t('ranks.flash.destroyed', name: name) end private diff --git a/app/controllers/shop_controller.rb b/app/controllers/shop_controller.rb index 47c51823..6373ba73 100644 --- a/app/controllers/shop_controller.rb +++ b/app/controllers/shop_controller.rb @@ -35,7 +35,7 @@ def confirm_buy return if shop.state_for(@item) == :afford redirect_to story_group_shop_index_path(@story_group), - alert: "Nie możesz teraz kupić „#{@item.name}”." + alert: t('shop.flash.unavailable', name: @item.name) end # POST /story_groups/:story_group_id/shop/:id/buy @@ -44,7 +44,7 @@ def buy if result.success? redirect_to story_group_shop_index_path(@story_group), - notice: "Kupione: „#{@item.name}”. Zostało Ci #{@student.current_currency}." + notice: t('shop.flash.bought', name: @item.name, balance: @student.current_currency) else redirect_to story_group_shop_index_path(@story_group), alert: result.errors.join(', ') end diff --git a/app/controllers/story_group_invites_controller.rb b/app/controllers/story_group_invites_controller.rb index 877486c9..3f3bb794 100644 --- a/app/controllers/story_group_invites_controller.rb +++ b/app/controllers/story_group_invites_controller.rb @@ -38,8 +38,7 @@ def create flash[:fresh_invite] = @form.invite.id redirect_outside_turbo_frame story_group_invites_path(@story_group), - notice: "Utworzono kod #{@form.invite.code}. " \ - 'Kliknij „Pokaż”, żeby wyświetlić go studentom.' + notice: t('story_group_invites.flash.created', code: @form.invite.code) end def update @@ -47,7 +46,7 @@ def update return render :edit, status: :unprocessable_content unless @form.save redirect_outside_turbo_frame story_group_invites_path(@story_group), - notice: "Zapisano zaproszenie #{@invite.code}." + notice: t('story_group_invites.flash.updated', code: @invite.code) end def destroy @@ -55,7 +54,7 @@ def destroy @invite.destroy redirect_outside_turbo_frame story_group_invites_path(@story_group), - notice: "Usunięto zaproszenie #{code}." + notice: t('story_group_invites.flash.destroyed', code: code) end private diff --git a/app/controllers/story_group_memberships_controller.rb b/app/controllers/story_group_memberships_controller.rb index fff3f22a..3a79c1aa 100644 --- a/app/controllers/story_group_memberships_controller.rb +++ b/app/controllers/story_group_memberships_controller.rb @@ -23,7 +23,7 @@ def update return render(from_dialog? ? :nickname : :edit, status: :unprocessable_content) end - notice = "Twój pseudonim w tej grupie: #{@membership.display_name}." + notice = t('story_group_memberships.flash.nickname', name: @membership.display_name) return redirect_to(edit_story_group_membership_path(@story_group), notice: notice) unless from_dialog? redirect_outside_turbo_frame story_group_ranking_path(@story_group), notice: notice @@ -39,7 +39,7 @@ def destroy name = @story_group.name @membership.destroy! - redirect_to story_groups_path, notice: "Opuszczono grupę #{name}.", status: :see_other + redirect_to story_groups_path, notice: t('story_group_memberships.flash.left', name: name), status: :see_other end private diff --git a/app/controllers/story_groups_controller.rb b/app/controllers/story_groups_controller.rb index 9531db41..662ba935 100644 --- a/app/controllers/story_groups_controller.rb +++ b/app/controllers/story_groups_controller.rb @@ -80,7 +80,7 @@ def update authorize @story_group if @story_group.update(story_group_params) - redirect_to edit_story_group_path(@story_group), notice: 'Zapisano ustawienia grupy.' + redirect_to edit_story_group_path(@story_group), notice: t('story_groups.flash.updated') else render :edit, status: :unprocessable_content end @@ -100,7 +100,7 @@ def destroy name = @story_group.name @story_group.destroy! - notice = "Usunięto grupę #{name}." + notice = t('story_groups.flash.destroyed', name: name) return redirect_outside_turbo_frame(story_groups_path, notice: notice) if @in_modal diff --git a/app/controllers/students_activity_group_categories_controller.rb b/app/controllers/students_activity_group_categories_controller.rb index 9c7876ac..e934a6e6 100644 --- a/app/controllers/students_activity_group_categories_controller.rb +++ b/app/controllers/students_activity_group_categories_controller.rb @@ -41,10 +41,11 @@ def set_activity_group end def award_notice(result) - return 'Nie zaznaczono żadnego pola.' if result.pairs.none? + return t('students_activity_group_categories.flash.nothing') if result.pairs.none? - students = helpers.gh_plural(result.students, 'studentowi', 'studentom', 'studentom') - "Przyznano #{result.total} #{@story_group.currency_name} #{result.students} #{students}." + t('students_activity_group_categories.flash.awarded', total: result.total, + currency: @story_group.currency_name, + count: result.students,) end # `completions[student_id][category_id]`. A missing key is not a revocation — diff --git a/app/controllers/students_badges_controller.rb b/app/controllers/students_badges_controller.rb index 49943b10..8733837e 100644 --- a/app/controllers/students_badges_controller.rb +++ b/app/controllers/students_badges_controller.rb @@ -25,7 +25,7 @@ def create if saved redirect_outside_turbo_frame story_group_student_path(@story_group, @student), - notice: "Przyznano odznakę „#{@students_badge.name}”." + notice: t('students_badges.flash.awarded', name: @students_badge.name) else set_badges render :new, status: :unprocessable_content @@ -37,7 +37,7 @@ def destroy @students_badge.destroy redirect_outside_turbo_frame story_group_student_path(@story_group, @student), - notice: "Odebrano odznakę „#{name}”." + notice: t('students_badges.flash.revoked', name: name) end private diff --git a/app/controllers/students_controller.rb b/app/controllers/students_controller.rb index 5f85c8b9..4ada507e 100644 --- a/app/controllers/students_controller.rb +++ b/app/controllers/students_controller.rb @@ -58,7 +58,7 @@ def update_lives redirect_to story_group_students_path(@story_group), notice: lives_notice else redirect_to story_group_students_path(@story_group), - alert: 'Nie można odebrać życia — student ma już 0.' + alert: t('students.flash.cannot_take_life') end end @@ -71,7 +71,7 @@ def destroy # "Usunięto Anna Kowalska" would be the wrong case and there is no way to # decline a name here. redirect_outside_turbo_frame story_group_students_path(@story_group), - notice: "#{name} nie należy już do grupy." + notice: t('students.flash.removed', name: name) end private @@ -97,7 +97,7 @@ def sheet # The ledger tab's filter, as a kind or nil for "Wszystkie". def ledger_kind - kinds = CurrencyLedger::KINDS.map(&:first).compact + kinds = CurrencyLedger::KINDS.compact kinds.include?(params[:kind]) ? params[:kind] : nil end @@ -105,9 +105,7 @@ def ledger_kind # this form and this stepper can do, so the toast says the result rather than # "zapisano". def lives_notice - lives = @student.lives.to_i - - "#{@student.display_name} ma teraz #{lives} #{helpers.gh_plural(lives, 'życie', 'życia', 'żyć')}." + t('students.flash.lives_notice', name: @student.display_name, count: @student.lives.to_i) end def update_student_params diff --git a/app/controllers/teachers_controller.rb b/app/controllers/teachers_controller.rb index c8d6b1bc..d9bb0e5f 100644 --- a/app/controllers/teachers_controller.rb +++ b/app/controllers/teachers_controller.rb @@ -26,7 +26,7 @@ def create if @teacher.save redirect_outside_turbo_frame story_group_teachers_path(@story_group), - notice: "Dodano: #{@teacher.full_name}." + notice: t('teachers.flash.added', name: @teacher.full_name) else set_pool render :new, status: :unprocessable_content @@ -38,7 +38,7 @@ def destroy @teacher.destroy redirect_outside_turbo_frame story_group_teachers_path(@story_group), - notice: "Usunięto z grupy: #{name}." + notice: t('teachers.flash.removed', name: name) end private diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 12fc768b..86299c40 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -8,15 +8,21 @@ def transaction_description(transaction, story_group) student_name = transaction.student.full_name case transaction.kind when 'purchase' - item_name = transaction.transactionable&.name || 'przedmiot' - "#{student_name} zakupił #{item_name} za #{transaction.amount.abs} #{story_group.currency_name}" + item_name = transaction.transactionable&.name || t('application.helpers.transaction.item_fallback') + t('application.helpers.transaction.purchase', student: student_name, + item: item_name, + amount: transaction.amount.abs, + currency: story_group.currency_name,) when 'reward' - category_name = transaction.transactionable&.didactic_description || 'aktywność' - "#{student_name} zdobył nagrodę za: #{category_name}" + category_name = transaction.transactionable&.didactic_description || + t('application.helpers.transaction.category_fallback') + t('application.helpers.transaction.reward', student: student_name, category: category_name) when 'adjustment' sign = transaction.amount >= 0 ? '+' : '' - adjuster = transaction.granted_by_user&.full_name || 'nauczyciel' - "#{adjuster} skorygował walutę #{student_name} (#{sign}#{transaction.amount})" + adjuster = transaction.granted_by_user&.full_name || t('application.helpers.transaction.adjuster_fallback') + t('application.helpers.transaction.adjustment', adjuster: adjuster, + student: student_name, + amount: "#{sign}#{transaction.amount}",) end end @@ -36,12 +42,8 @@ def gh_group_chrome @gh_group_chrome = GroupChrome.for(user: @current_user, story_group: @story_group) end - def gh_plural(count, one, few, many) - Plural.pick(count, one, few, many) - end - def gh_minutes(count) - "#{count} #{gh_plural(count, 'minuta', 'minuty', 'minut')}" + t('application.helpers.minutes', count: count) end def gh_initials(name) @@ -64,14 +66,15 @@ def gh_avatar_hue(name) def gh_when(time) case time.to_date - when Date.current then "dziś #{time.strftime('%H:%M')}" - when Date.yesterday then "wczoraj #{time.strftime('%H:%M')}" - else time.strftime('%d.%m, %H:%M') + when Date.current then t('application.helpers.today', time: l(time, format: :gh_time)) + when Date.yesterday then t('application.helpers.yesterday', time: l(time, format: :gh_time)) + else l(time, format: :gh_day_time) end end + # I18n.l rather than l: InviteForm calls this outside a view. def gh_stamp(time) - time.strftime('%d.%m, %H:%M') + I18n.l(time, format: :gh_day_time) end def gh_feed_title(transaction) @@ -79,11 +82,16 @@ def gh_feed_title(transaction) case transaction.kind.to_sym when :purchase - subject ? "Zakup: #{subject.name}" : 'Zakup' + if subject + t('application.helpers.feed.purchase', name: subject.name) + else + t('application.helpers.feed.purchase_fallback') + end when :reward - subject&.story_description.presence || subject&.didactic_description.presence || 'Nagroda za aktywność' + subject&.story_description.presence || subject&.didactic_description.presence || + t('application.helpers.feed.reward_fallback') else - 'Korekta waluty' + t('application.helpers.feed.adjustment') end end @@ -93,11 +101,22 @@ def gh_percent(value, max) ((value.to_f / max) * 100).round.clamp(0, 100) end - def gh_and_list(items) - list = Array(items) - return list.first.to_s if list.size < 2 + # Fills a translated template whose slots hold markup rather than text: + # gh_fill(t('items.sentences.max'), holder: ..., percent: tag.b('20%')) + # Text around the slots is escaped; html_safe values (tag.b, safe_join) pass + # through. item_form_controller.ts fills the same templates on the client. + def gh_fill(template, **vars) + parts = template.to_s.split(/(%\{\w+\})/).map do |part| + key = part[/\A%\{(\w+)\}\z/, 1] + key ? vars[key.to_sym] : part + end - "#{list[0..-2].join(', ')} i #{list.last}" + safe_join(parts) + end + + # "a, b i c" in Polish, "a, b and c" in English (connectors from rails-i18n). + def gh_and_list(items) + Array(items).map(&:to_s).to_sentence end def gh_glyph(key, css_class: 'gh-card-art-glyph') diff --git a/app/helpers/badges_helper.rb b/app/helpers/badges_helper.rb index fdebbe80..1fbaff23 100644 --- a/app/helpers/badges_helper.rb +++ b/app/helpers/badges_helper.rb @@ -2,23 +2,23 @@ module BadgesHelper def badge_holders_line(count) - return 'Nikt jej jeszcze nie ma' if count.zero? + return t('badges.helpers.nobody_holds') if count.zero? - "Ma ją #{count} #{gh_plural(count, 'student', 'studentów', 'studentów')}" + t('badges.helpers.holders', count: count) end def badge_discount_label(badge) return if badge.discount.to_i.zero? - "−#{badge.discount}% w sklepie" + t('badges.helpers.discount', discount: badge.discount) end def badge_rule(badge) - badge.didactic_description.presence || 'Jak zdobyć…' + badge.didactic_description.presence || t('badges.helpers.rule_placeholder') end def badge_name(badge) - badge.name.presence || 'Nazwa odznaki' + badge.name.presence || t('badges.helpers.name_placeholder') end def badge_thumb_subtitle(badge) diff --git a/app/helpers/currency_transactions_helper.rb b/app/helpers/currency_transactions_helper.rb index d573c159..14c76df9 100644 --- a/app/helpers/currency_transactions_helper.rb +++ b/app/helpers/currency_transactions_helper.rb @@ -11,21 +11,24 @@ module CurrencyTransactionsHelper # of its currency and Polish would want a different case here, so the sentence # simply does not name it. Same rule as ShopHelper#shop_balance_sentence. def ledger_balance_sentence(ledger) - safe_join(['Masz ', tag.b(ledger.balance), ' do wydania i ', tag.b(ledger.total), ' zebrane łącznie.']) + t('currency_transactions.helpers.balance_sentence_html', balance: tag.b(ledger.balance), + total: tag.b(ledger.total),) end # The teacher's version, which sits on one line beside the filters rather than # under a heading. def ledger_summary_sentence(ledger) - safe_join(['Saldo: ', tag.b(ledger.balance), ', zebrane łącznie: ', tag.b(ledger.total), '.']) + t('currency_transactions.helpers.summary_sentence_html', balance: tag.b(ledger.balance), + total: tag.b(ledger.total),) end # The filter chips. Links rather than buttons, because the app's .gh-purchase-filter-chip is # keyed on aria-current and because a filter that survives a refresh and can # be pasted to someone is worth a query parameter. def ledger_filter_chips(ledger, selected, path_for:) - safe_join(CurrencyLedger::KINDS.map do |kind, label| + safe_join(CurrencyLedger::KINDS.map do |kind| current = kind == selected + label = t("currency_transactions.filters.#{kind || 'all'}") link_to path_for.call(kind), class: 'gh-purchase-filter-chip', 'aria-current': current.to_s do safe_join([label, tag.span(ledger.count_for(kind), class: 'gh-count-label')], ' ') diff --git a/app/helpers/home_helper.rb b/app/helpers/home_helper.rb index e6889af8..f758f54d 100644 --- a/app/helpers/home_helper.rb +++ b/app/helpers/home_helper.rb @@ -3,9 +3,9 @@ module HomeHelper def home_greeting(user) first_name = user.full_name.to_s.split.first - base_greeting = 'Dzień dobry' - return base_greeting if first_name.blank? - "#{base_greeting}, #{first_name}" + return t('home.admin_greeting_anonymous') if first_name.blank? + + t('home.admin_greeting', name: first_name) end end diff --git a/app/helpers/items_helper.rb b/app/helpers/items_helper.rb index a8a0d381..552f5e41 100644 --- a/app/helpers/items_helper.rb +++ b/app/helpers/items_helper.rb @@ -2,75 +2,68 @@ module ItemsHelper def item_bought_line(count) - return 'Jeszcze nikt nie kupił' if count.zero? + return t('items.helpers.nobody_bought') if count.zero? - "Kupiono #{count} #{gh_plural(count, 'raz', 'razy', 'razy')}" + t('items.helpers.bought', count: count) end def item_name(item) - item.name.presence || 'Nazwa przedmiotu' + item.name.presence || t('items.helpers.name_placeholder') end def item_rules(item) - item.didactic_description.presence || 'Co daje studentowi…' + item.didactic_description.presence || t('items.helpers.rules_placeholder') end def item_unlock_sentence(item) rank = item.unlock_rank&.starting? ? nil : item.unlock_rank badges = item.unlock_badges.map(&:name).sort + lives = t(item.can_buy_at_0_lives ? 'items.sentences.lives_yes' : 'items.sentences.lives_no') - parts = if rank.nil? && badges.empty? - ['Każdy student może kupić ten przedmiot'] - else - ['Kupią tylko studenci', *unlock_rank_clause(rank), *unlock_badges_clause(badges)] - end + return gh_fill(t('items.sentences.unlock_everyone'), lives: lives) if rank.nil? && badges.empty? - safe_join(parts + [item_lives_clause(item)]) + gh_fill(t('items.sentences.unlock_restricted'), + rank: unlock_rank_clause(rank), + badges: unlock_badges_clause(badges), + lives: lives,) end def item_discount_sentence(item, card, example) price = item.price.to_i - return safe_join(['Bez zniżek. Każdy kupujący zapłaci ', tag.b(price), '.']) if card.max_discount.zero? + return gh_fill(t('items.sentences.no_discount'), price: tag.b(price)) if card.max_discount.zero? return if example.none? paid = PriceCalculatorService.new(price: price, discount: Discount.new(example.percent)).calculate - saving = tag.b("#{example.percent}%") - tail = [' zaoszczędzi ', saving, ' i zapłaci ', tag.b(paid), " zamiast #{price}."] - - safe_join(['Przykładowo: student', *example_holder_clause(example), *tail]) + gh_fill(t('items.sentences.example'), + holder: example_holder_clause(example), + percent: tag.b("#{example.percent}%"), + paid: tag.b(paid), + price: price,) end def item_discount_max_sentence(card) return if card.max_discount.zero? - saving = tag.b("#{card.max_discount}%") - - safe_join(['Maksymalnie: student', *max_holder_clause(card), ' uzyska zniżkę ', saving, '.']) + gh_fill(t('items.sentences.max'), holder: max_holder_clause(card), percent: tag.b("#{card.max_discount}%")) end def item_overlap_warnings(item, card = nil) warnings = [] - if card&.capped? - warnings << "Zniżki sumują się do #{card.raw_discount}%, a sklep odejmie najwyżej " \ - "#{Discount::CAP_VALUE}%. Nadwyżka przepada — rozważ niższe zniżki " \ - 'przy rangach i odznakach.' - end + warnings << t('items.sentences.warn_cap', raw: card.raw_discount, cap: Discount::CAP_VALUE) if card&.capped? if discount_rank_covers_everyone?(item) warnings << if item.unlock_rank.starting? - 'Zniżka dla rang obejmie każdego kupującego.' + t('items.sentences.warn_rank_everyone') else - "Kupić mogą tylko studenci od rangi #{item.unlock_rank.name}, " \ - 'więc zniżka dla rang obejmie każdego kupującego.' + t('items.sentences.warn_rank_gate', rank: item.unlock_rank.name) end end overlapping = (item.discount_badges & item.unlock_badges).sort_by { |badge| badge.name.to_s } overlapping.each do |badge| - warnings << "Odznaka #{badge.name} jest wymagana do zakupu, " \ - 'więc jej zniżka obejmie każdego kupującego.' + warnings << t('items.sentences.warn_badge_required', name: badge.name) end warnings @@ -79,10 +72,10 @@ def item_overlap_warnings(item, card = nil) def item_unlock_rank_options(ranks, selected) options = ranks.map do |rank| data = { gh_name: rank.name, gh_gates: rank.starting? ? 'false' : 'true' } - ["od rangi #{rank.name}", rank.id, { data: data }] + [t('items.helpers.unlock_option_rank', name: rank.name), rank.id, { data: data }] end - blank = ['każdy student', '', { data: { gh_name: '', gh_gates: 'false' } }] + blank = [t('items.helpers.unlock_option_everyone'), '', { data: { gh_name: '', gh_gates: 'false' } }] options_for_select([blank] + options, selected) end @@ -92,7 +85,7 @@ def item_discount_rank_options(ranks, selected) above = ranks.select { |other| other.required_currency_value.to_i >= rank.required_currency_value.to_i } best = above.max_by { |other| other.discount.to_i } - label = "od #{rank.name} (−#{rank.discount.to_i}% i więcej)" + label = t('items.helpers.discount_option_rank', name: rank.name, discount: rank.discount.to_i) data = { gh_discount: best&.discount.to_i, gh_name: best&.name.to_s, @@ -102,37 +95,42 @@ def item_discount_rank_options(ranks, selected) [label, rank.id, { data: data }] end - blank = ['brak', '', { data: { gh_discount: 0, gh_name: '', gh_own: 0, gh_own_name: '' } }] + blank_data = { data: { gh_discount: 0, gh_name: '', gh_own: 0, gh_own_name: '' } } + blank = [t('items.helpers.discount_option_none'), '', blank_data] options_for_select([blank] + options, selected) end + # The templates and plural forms item_form_controller.ts needs to rebuild + # the same sentences while the teacher types. + def item_form_wording + t('items.sentences').merge( + t('items.labels').slice(:discount_up_to, :seal_rank, :seal_badge), + t('items.helpers').slice(:name_placeholder, :rules_placeholder), + buy: t('items.card.buy'), + ) + end + private def unlock_rank_clause(rank) - return [] if rank.nil? + return '' if rank.nil? - [' z rangą ', tag.b(rank.name), ' lub wyższą'] + gh_fill(t('items.sentences.unlock_rank'), rank: tag.b(rank.name)) end def unlock_badges_clause(badges) - return [] if badges.empty? + return '' if badges.empty? - noun = badges.size > 1 ? 'wszystkie odznaki' : 'odznakę' - [", którzy mają #{noun} ", tag.b(gh_and_list(badges))] - end - - def item_lives_clause(item) - item.can_buy_at_0_lives ? '. Także przy 0 życiach.' : ', jeśli mają co najmniej 1 życie.' + gh_fill(plural_template('items.sentences.unlock_badges', badges.size), badges: tag.b(gh_and_list(badges))) end def example_holder_clause(example) names = example.badges.map(&:name) return holder_clause(example.rank&.name, nil) if names.empty? - noun = names.size > 1 ? 'odznakami ' : 'odznaką ' - - holder_clause(example.rank&.name, [noun, tag.b(gh_and_list(names))]) + phrase = gh_fill(plural_template('items.sentences.holder_badges', names.size), badges: tag.b(gh_and_list(names))) + holder_clause(example.rank&.name, phrase) end def max_holder_clause(card) @@ -140,19 +138,27 @@ def max_holder_clause(card) phrase = case badges.size when 0 then nil - when 1 then ['odznaką ', tag.b(badges.first.name)] - else ['wszystkimi odznakami'] + when 1 then gh_fill(plural_template('items.sentences.holder_badges', 1), badges: tag.b(badges.first.name)) + else t('items.sentences.holder_all_badges') end holder_clause(card.best_discount_rank&.name, phrase) end def holder_clause(rank_name, badge_phrase) - clause = [] - clause += [' z rangą ', tag.b(rank_name)] if rank_name - return clause if badge_phrase.nil? + rank = rank_name ? gh_fill(t('items.sentences.holder_rank'), rank: tag.b(rank_name)) : '' + return rank if badge_phrase.nil? + + join = rank_name ? 'items.sentences.holder_join_and' : 'items.sentences.holder_join_with' + safe_join([rank, gh_fill(t(join), phrase: badge_phrase)]) + end + + # The raw template of a pluralised key, with its other slots left in place + # for gh_fill. + def plural_template(key, count) + slots = t(key).values.join.scan(/%\{(\w+)\}/).flatten.uniq - ['count'] - clause + [rank_name ? ' oraz ' : ' z ', *badge_phrase] + t(key, count: count, **slots.to_h { |slot| [slot.to_sym, "%{#{slot}}"] }) end def discount_rank_covers_everyone?(item) diff --git a/app/helpers/organizations_helper.rb b/app/helpers/organizations_helper.rb index cd9ff246..c432f6a8 100644 --- a/app/helpers/organizations_helper.rb +++ b/app/helpers/organizations_helper.rb @@ -1,23 +1,22 @@ # frozen_string_literal: true module OrganizationsHelper - USER_ROLE_LABELS = { - 'student' => 'Student', - 'teacher' => 'Nauczyciel', - }.freeze - def user_role_label(role) str_role = role.to_s - USER_ROLE_LABELS.fetch(str_role, str_role.humanize) + t("organizations.roles.#{str_role}", default: str_role.humanize) end def organization_capacity_lead(organization, members) - return "#{members} #{gh_plural(members, 'konto', 'konta', 'kont')} · bez limitu miejsc" if organization.unlimited? + if organization.unlimited? + return t('organizations.helpers.capacity_unlimited', + accounts: t('organizations.helpers.capacity_accounts', count: members),) + end free = [organization.max_members - members, 0].max - free_text = gh_plural(free, 'wolne', 'wolne', 'wolnych') - "#{members} z #{organization.max_members} miejsc zajętych · #{free} #{free_text}" + t('organizations.helpers.capacity_limited', members: members, + max: organization.max_members, + free: t('organizations.helpers.capacity_free', count: free),) end def user_role_options diff --git a/app/helpers/ranks_helper.rb b/app/helpers/ranks_helper.rb index cfccd455..57478bb4 100644 --- a/app/helpers/ranks_helper.rb +++ b/app/helpers/ranks_helper.rb @@ -2,33 +2,37 @@ module RanksHelper def rank_summary(rank) - opening = rank.starting? ? 'Ranga startowa' : "Od #{rank.required_currency_value} zebranych" + opening = rank.starting? ? t('ranks.helpers.starting') : t('ranks.helpers.from', min: rank.required_currency_value) - return "#{opening}, bez zniżki" if rank.discount.to_i.zero? + return t('ranks.helpers.summary_no_discount', opening: opening) if rank.discount.to_i.zero? - "#{opening}, −#{rank.discount}% w sklepie" + t('ranks.helpers.summary_discount', opening: opening, discount: rank.discount) end def rank_summary_sentences(rank) - opening = rank.starting? ? 'Ranga startowa.' : "Od #{rank.required_currency_value} zebranych." + opening = if rank.starting? + t('ranks.helpers.starting_sentence') + else + t('ranks.helpers.from_sentence', min: rank.required_currency_value) + end return opening if rank.discount.to_i.zero? - "#{opening} Daje −#{rank.discount}% w sklepie." + "#{opening} #{t('ranks.helpers.gives_discount', discount: rank.discount)}" end def rank_ladder_terms(rank) - return "−#{rank.discount}% w sklepie" if rank.discount.to_i.positive? - return 'Ranga startowa' if rank.starting? + return t('ranks.helpers.discount', discount: rank.discount) if rank.discount.to_i.positive? + return t('ranks.helpers.starting') if rank.starting? - 'Bez zniżki' + t('ranks.helpers.no_discount') end def rank_reward_line(rank) - line = "Wymagane: #{rank.required_currency_value.to_i} zebranych." + line = t('ranks.helpers.required', min: rank.required_currency_value.to_i) return line if rank.discount.to_i.zero? - "#{line} Daje −#{rank.discount}% w sklepie." + "#{line} #{t('ranks.helpers.gives_discount', discount: rank.discount)}" end end diff --git a/app/helpers/shop_helper.rb b/app/helpers/shop_helper.rb index 6ea81d66..327637db 100644 --- a/app/helpers/shop_helper.rb +++ b/app/helpers/shop_helper.rb @@ -7,7 +7,7 @@ module ShopHelper # declined: a group holds one nominative form ("marchewki") and Polish wants # the genitive here. ranks/_teacher_ladder.html.haml:8 makes the same dodge. def shop_balance_sentence(balance) - safe_join(['Masz ', tag.b(balance), ' do wydania.']) + t('shop.helpers.balance_html', balance: tag.b(balance)) end # "Twoje odznaki Mechanik i Nawigator obniżają ceny niektórych przedmiotów." @@ -17,17 +17,18 @@ def shop_discount_sentence(badges) return if badges.empty? names = badges.map { |badge| tag.b(badge.name) } - lead = gh_plural(badges.size, 'Twoja odznaka ', 'Twoje odznaki ', 'Twoje odznaki ') - verb = gh_plural(badges.size, ' obniża ', ' obniżają ', ' obniżają ') - safe_join([lead, gh_and_list_html(names), verb, 'ceny niektórych przedmiotów.']) + t('shop.helpers.discount_badges_html', count: badges.size, badges: gh_and_list_html(names)) end # gh_and_list, but for parts that are already markup: Array#join would hand - # safe_join a plain String and every would come back escaped. + # safe_join a plain String and every would come back escaped. The + # connectors are rails-i18n's, the same ones Array#to_sentence uses. def gh_and_list_html(parts) return parts.first if parts.size < 2 + return safe_join(parts, t('support.array.two_words_connector')) if parts.size == 2 - safe_join([safe_join(parts[0..-2], ', '), parts.last], ' i ') + safe_join([safe_join(parts[0..-2], t('support.array.words_connector')), parts.last], + t('support.array.last_word_connector'),) end end diff --git a/app/helpers/story_group_invites_helper.rb b/app/helpers/story_group_invites_helper.rb index 2da94dac..e10c129b 100644 --- a/app/helpers/story_group_invites_helper.rb +++ b/app/helpers/story_group_invites_helper.rb @@ -21,35 +21,49 @@ def invite_qr_code(invite) end def invite_uses_label(invite) - return "#{invite.uses}, bez limitu" if invite.max_uses.nil? + return t('story_group_invites.helpers.uses_unlimited', uses: invite.uses) if invite.max_uses.nil? - "#{invite.uses} z #{invite.max_uses}" + t('story_group_invites.helpers.uses_limited', uses: invite.uses, max: invite.max_uses) end def invite_expiry_label(invite) - return 'Bez daty ważności' if invite.expires_at.nil? + return t('story_group_invites.helpers.no_expiry') if invite.expires_at.nil? - "#{invite.expire_time_condition ? 'Wygasa' : 'Wygasło'} #{gh_stamp(invite.expires_at)}" + key = invite.expire_time_condition ? 'expires' : 'expired' + t("story_group_invites.helpers.#{key}", date: gh_stamp(invite.expires_at)) end def invite_stat_sentence(invite) expiry = if invite.expires_at.nil? - 'Bez daty ważności.' + t('story_group_invites.helpers.no_expiry') else - "Wygasa #{gh_stamp(invite.expires_at)}." + t('story_group_invites.helpers.expires', date: gh_stamp(invite.expires_at)) end - "Użycia: #{invite_uses_label(invite)}. #{expiry}" + t('story_group_invites.helpers.stat_sentence', uses: invite_uses_label(invite), expiry: expiry) end def invite_code_aria_label(invite) - "Kod: #{invite.code.chars.join(' ')}" + t('story_group_invites.helpers.code_label', code: invite.code.chars.join(' ')) end def invite_option_label(invite) - uses = invite.max_uses.nil? ? 'bez limitu' : "#{invite.uses} z #{invite.max_uses}" - validity = invite.expires_at.nil? ? 'bezterminowo' : "ważne do #{gh_stamp(invite.expires_at)}" + uses = if invite.max_uses.nil? + t('story_group_invites.helpers.option_unlimited') + else + t('story_group_invites.helpers.uses_limited', uses: invite.uses, max: invite.max_uses) + end + validity = if invite.expires_at.nil? + t('story_group_invites.helpers.option_no_expiry') + else + t('story_group_invites.helpers.option_valid_until', date: gh_stamp(invite.expires_at)) + end - "#{invite.code} – #{uses}, #{validity}" + t('story_group_invites.helpers.option', code: invite.code, uses: uses, validity: validity) + end + + # What invite_form_controller.ts needs to rebuild the form's summary line. + def invite_form_wording + t('story_group_invites.summary').merge(month_names: t('date.abbr_month_names')) end end diff --git a/app/helpers/story_groups_helper.rb b/app/helpers/story_groups_helper.rb index be0251ef..41b00a1c 100644 --- a/app/helpers/story_groups_helper.rb +++ b/app/helpers/story_groups_helper.rb @@ -6,31 +6,29 @@ module StoryGroupsHelper def overview_teacher_lead(story_group, students_count) return story_group.description if story_group.description.present? - "#{students_count} #{gh_plural(students_count, 'student', 'studenci', 'studentów')} w tej grupie. " \ - 'Dodaj opis w ustawieniach, żeby powitać nowe osoby.' + t('story_groups.helpers.teacher_lead', count: students_count) end def overview_next_rank_line(missing, next_rank) - line = "Jeszcze #{missing} do rangi #{next_rank.name}." + line = t('story_groups.helpers.next_rank', missing: missing, rank: next_rank.name) return line unless next_rank.discount.to_i.positive? - "#{line} Ta ranga daje −#{next_rank.discount}% w sklepie." + "#{line} #{t('story_groups.helpers.next_rank_discount', discount: next_rank.discount)}" end def ranking_visibility_note(story_group) - return 'Ranking jest wyłączony — studenci nie widzą tego podium.' unless story_group.ranking_enabled? - return 'Studenci widzą pełny ranking, pod pseudonimami.' if story_group.ranking_full? + return t('story_groups.helpers.ranking_off') unless story_group.ranking_enabled? + return t('story_groups.helpers.ranking_full') if story_group.ranking_full? - 'Studenci widzą podium i własne miejsce, pod pseudonimami.' + t('story_groups.helpers.ranking_podium') end def overview_place_line(place) - "#{place}. miejsce w rankingu grupy" + t('story_groups.helpers.place', place: place) end def overview_hand_line(affordable) - "Dobierz coś w sklepie. Stać cię teraz na #{affordable} " \ - "#{gh_plural(affordable, 'przedmiot', 'przedmioty', 'przedmiotów')}." + t('story_groups.helpers.hand', count: affordable) end def overview_hand_rotation(index, count) diff --git a/app/helpers/students_badges_helper.rb b/app/helpers/students_badges_helper.rb index 46ebfb7c..d862cd06 100644 --- a/app/helpers/students_badges_helper.rb +++ b/app/helpers/students_badges_helper.rb @@ -4,13 +4,13 @@ module StudentsBadgesHelper # "Sebastian ma 2 z 8 odznak." — the assign dialog's sub-line. def badge_assign_lead(student, held, total) - "#{student.display_name} ma #{held} z #{total} #{gh_plural(total, 'odznaki', 'odznak', 'odznak')}." + t('students_badges.helpers.assign_lead', name: student.display_name, held: held, count: total) end # The right-hand label on a row of the picker: what the badge is worth, or # that this student already has it. def badge_pick_status(badge, held) - return 'Ma już' if held + return t('students_badges.helpers.owned') if held return '−0%' if badge.discount.to_i.zero? "−#{badge.discount.to_i}%" @@ -19,34 +19,35 @@ def badge_pick_status(badge, held) # What awarding this badge will actually do. Rendered once per badge, hidden, # so badge_picker_controller reveals one rather than building a sentence. def badge_effect_sentence(badge) - parts = [ - 'Po przyznaniu: ', - tag.b("−#{badge.discount.to_i}%"), - ' na przedmioty, które uwzględniają tę odznakę', - ] - unlocking = badge.unlocking_items.order(:name).pluck(:name) discounting = badge.dependent_items.order(:name).pluck(:name) - unlocking - parts << " (#{gh_and_list(discounting)})" if discounting.any? - parts << '.' - parts += [' Odblokuje zakup: ', tag.b(gh_and_list(unlocking)), '.'] if unlocking.any? + t('students_badges.helpers.effect_html', + discount: tag.b("−#{badge.discount.to_i}%"), + discounting: badge_effect_discounting(discounting), + unlocks: badge_effect_unlocks(unlocking),) + end - parts << ' Zobaczy ją w swojej talii.' + def badge_effect_discounting(names) + return '' if names.empty? - safe_join(parts) + t('students_badges.helpers.effect_discounting', items: gh_and_list(names)) + end + + def badge_effect_unlocks(names) + return '' if names.empty? + + t('students_badges.helpers.effect_unlocks_html', items: tag.b(gh_and_list(names))) end # What revoking it costs. The purchase clause only when the badge is a key to # something; items already bought are never taken back. def badge_revoke_sentence(student, badge) - parts = [student.display_name, ' straci zniżkę ', tag.b("−#{badge.discount.to_i}%")] - unlocking = badge.unlocking_items.order(:name).pluck(:name) - parts << " i możliwość zakupu: #{gh_and_list(unlocking)}" if unlocking.any? - - parts << '. Kupione wcześniej przedmioty zostają.' - safe_join(parts) + t('students_badges.helpers.revoke_html', + name: student.display_name, + discount: tag.b("−#{badge.discount.to_i}%"), + unlocking: unlocking.any? ? t('students_badges.helpers.revoke_unlocking', items: gh_and_list(unlocking)) : '',) end end diff --git a/app/helpers/students_helper.rb b/app/helpers/students_helper.rb index e545b0db..480b4338 100644 --- a/app/helpers/students_helper.rb +++ b/app/helpers/students_helper.rb @@ -3,19 +3,18 @@ # Screen-specific labels live in the screen's own helper, not in ApplicationHelper. module StudentsHelper def student_list_lead(list) - lead = ['Życie odbierasz za nieusprawiedliwioną nieobecność.'] + lead = [t('students.helpers.list_lead')] return safe_join(lead) if list.zero_lives.zero? - zero = list.zero_lives - safe_join(lead + [' ', - tag.b("#{zero} #{gh_plural(zero, 'student ma', 'studentów ma', 'studentów ma')} 0 żyć"), - ' i kupi tylko przedmioty dostępne przy 0 życiach.',]) + tag.b(t('students.helpers.zero_lives', count: list.zero_lives)), + ' ', + t('students.helpers.zero_lives_tail'),]) end def student_list_subtitle(student) parts = [] - parts << "„#{student.nickname}”" if student.nickname.present? + parts << t('students.helpers.nickname_quoted', nickname: student.nickname) if student.nickname.present? parts << student.email parts.join(' · ') @@ -27,9 +26,11 @@ def student_list_subtitle(student) # student who joined without an index number simply does not get that half. def student_sheet_subtitle(student) parts = [] - parts << "Pseudonim: #{student.nickname}" if student.nickname.present? + parts << t('students.helpers.nickname', nickname: student.nickname) if student.nickname.present? parts << student.email - parts << "indeks #{student.university_number}" if student.university_number.present? + if student.university_number.present? + parts << t('students.helpers.university_number', number: student.university_number) + end parts.join(', ') end @@ -39,7 +40,9 @@ def student_sheet_subtitle(student) def student_rank_progress_line(sheet) return if sheet.next_rank.nil? - "#{sheet.total} z #{sheet.next_rank.required_currency_value} do rangi #{sheet.next_rank.name}" + t('students.helpers.rank_progress', total: sheet.total, + required: sheet.next_rank.required_currency_value, + rank: sheet.next_rank.name,) end # What "Usuń z grupy" costs, spelled out in numbers. Nothing is soft-deleted @@ -48,18 +51,11 @@ def student_rank_progress_line(sheet) def student_removal_losses(sheet) losses = [] - if sheet.badges.any? - count = sheet.badges.size - losses << "#{count} #{gh_plural(count, 'odznakę', 'odznaki', 'odznak')}" - end - - if sheet.purchases.any? - count = sheet.purchases.size - losses << "#{count} #{gh_plural(count, 'przedmiot', 'przedmioty', 'przedmiotów')}" - end + losses << t('students.helpers.losses.badges', count: sheet.badges.size) if sheet.badges.any? + losses << t('students.helpers.losses.items', count: sheet.purchases.size) if sheet.purchases.any? entries = sheet.ledger.size - losses << "całą historię waluty (#{entries} #{gh_plural(entries, 'wpis', 'wpisy', 'wpisów')})" if entries.positive? + losses << t('students.helpers.losses.ledger', count: entries) if entries.positive? losses end diff --git a/app/helpers/students_items_helper.rb b/app/helpers/students_items_helper.rb index d3418fba..4622788f 100644 --- a/app/helpers/students_items_helper.rb +++ b/app/helpers/students_items_helper.rb @@ -4,30 +4,29 @@ module StudentsItemsHelper # "3 przedmioty. Wykorzystanie przedmiotu zgłaszasz prowadzącemu na zajęciach." def inventory_lead(count) - "#{count} #{gh_plural(count, 'przedmiot', 'przedmioty', 'przedmiotów')}. " \ - 'Wykorzystanie przedmiotu zgłaszasz prowadzącemu na zajęciach.' + t('students_items.helpers.inventory_lead', count: count) end # The student's own card foot: "Kupione 12.06, 10:21, za 15". def purchase_line(purchase) - "Kupione #{gh_when(purchase.created_at)}, za #{purchase.price_paid}" + t('students_items.helpers.purchase_line', when: gh_when(purchase.created_at), price: purchase.price_paid) end # The teacher's, on the sheet's Przedmioty tab. The price paid is in the cost # chip there, so this names the discount instead — what the item costs anyone # else is the part the teacher cannot see from the number alone. def purchase_teacher_line(purchase) - line = "Kupione #{gh_when(purchase.created_at)}" - list = purchase.item&.price.to_i + bought_at = gh_when(purchase.created_at) + list = purchase.item&.price.to_i - return line if purchase.price_paid.to_i >= list + return t('students_items.helpers.purchase_teacher_line', when: bought_at) if purchase.price_paid.to_i >= list - "#{line}, ze zniżką z #{list}" + t('students_items.helpers.purchase_teacher_discount', when: bought_at, list: list) end # "Stać cię teraz na 3 przedmioty w sklepie." — the dashed slot that closes # the grid, which is the whole reason this screen links to the shop at all. def inventory_shop_line(count) - "Stać cię teraz na #{count} #{gh_plural(count, 'przedmiot', 'przedmioty', 'przedmiotów')} w sklepie." + t('students_items.helpers.shop_line', count: count) end end diff --git a/app/helpers/teachers_helper.rb b/app/helpers/teachers_helper.rb index 163cb4e4..2a9e224b 100644 --- a/app/helpers/teachers_helper.rb +++ b/app/helpers/teachers_helper.rb @@ -2,29 +2,22 @@ module TeachersHelper def teacher_list_lead(manage:) - opening = 'Nauczyciele wspomagający pomagają prowadzić tę grupę.' + tail = manage ? t('teachers.helpers.lead_manage') : t('teachers.helpers.lead_read_only') - return "#{opening} Grupę może usunąć tylko właściciel." if manage - - "#{opening} Dodawać i usuwać nauczycieli może tylko właściciel grupy." + "#{t('teachers.helpers.lead')} #{tail}" end def teacher_added_on(time) - time.to_date.strftime('%d.%m.%Y') + l(time.to_date, format: :default) end def teacher_pool_hint(size) - count = gh_plural(size, - "jest #{size} nauczyciel", - "są #{size} nauczyciele", - "jest #{size} nauczycieli",) - - "Wpisz co najmniej 2 znaki imienia, nazwiska albo e-maila. W Twojej uczelni #{count}." + t('teachers.helpers.pool_hint', count: size) end def teacher_candidate_subtitle(candidate, cross_university:) return candidate.email unless cross_university && candidate.person.university_name.present? - "#{candidate.email}, #{candidate.person.university_name}" + t('teachers.helpers.candidate', email: candidate.email, university: candidate.person.university_name) end end diff --git a/app/javascript/controllers/badge_form_controller.ts b/app/javascript/controllers/badge_form_controller.ts index d9f49fed..b2e1bfc3 100644 --- a/app/javascript/controllers/badge_form_controller.ts +++ b/app/javascript/controllers/badge_form_controller.ts @@ -10,6 +10,16 @@ class BadgeFormController extends Controller { "flip", "side", ] + static values = { + namePlaceholder: String, + rulePlaceholder: String, + discountLabel: String, + } + + declare readonly namePlaceholderValue: string + declare readonly rulePlaceholderValue: string + declare readonly discountLabelValue: string + declare readonly nameTarget: HTMLInputElement declare readonly ruleTarget: HTMLTextAreaElement declare readonly storyTarget: HTMLTextAreaElement @@ -37,18 +47,18 @@ class BadgeFormController extends Controller { const story = this.storyTarget.value.trim() const discount = Math.max(0, parseInt(this.discountTarget.value, 10) || 0) - this.placeholder(this.frontNameTarget, name, "Nazwa odznaki") - this.placeholder(this.backNameTarget, name, "Nazwa odznaki") - this.placeholder(this.frontRulesTarget, rule, "Jak zdobyć…") - this.backHowTarget.textContent = rule || "Jak zdobyć…" + this.placeholder(this.frontNameTarget, name, this.namePlaceholderValue) + this.placeholder(this.backNameTarget, name, this.namePlaceholderValue) + this.placeholder(this.frontRulesTarget, rule, this.rulePlaceholderValue) + this.backHowTarget.textContent = rule || this.rulePlaceholderValue this.frontFlavorTarget.textContent = story this.frontFlavorTarget.hidden = story === "" - this.frontTagTarget.textContent = `−${discount}% w sklepie` + this.frontTagTarget.textContent = this.discountLabelValue.replace("%{discount}", String(discount)) this.frontTagTarget.hidden = discount === 0 - this.thumbNameTarget.textContent = name || "Nazwa odznaki" + this.thumbNameTarget.textContent = name || this.namePlaceholderValue this.thumbSubTarget.textContent = story || rule this.renderArt() diff --git a/app/javascript/controllers/grading_controller.ts b/app/javascript/controllers/grading_controller.ts index 6575c055..7b919cbf 100644 --- a/app/javascript/controllers/grading_controller.ts +++ b/app/javascript/controllers/grading_controller.ts @@ -1,6 +1,16 @@ import { application } from "@controllers/application" import { Controller, type ActionEvent } from "@hotwired/stimulus" import { TOAST_EVENT, type ToastDetail } from "@controllers/toast_controller" +import { interpolate, pluralize, type PluralForms } from "@utils/i18n" + +interface Wording { + cells: PluralForms + students: PluralForms + recipients: PluralForms + cell_empty: string + cell_marked: string + locked: string +} class GradingController extends Controller { static targets = [ @@ -11,11 +21,7 @@ class GradingController extends Controller { ] static values = { - cells: Array, - students: Array, - recipients: Array, - states: Array, - currency: String, + i18n: Object, } declare readonly searchTarget: HTMLInputElement @@ -34,11 +40,7 @@ class GradingController extends Controller { declare readonly reviewRowTargets: HTMLElement[] declare readonly reviewChipTargets: HTMLElement[] declare readonly reviewConfirmSumTarget: HTMLElement - declare readonly cellsValue: string[] - declare readonly studentsValue: string[] - declare readonly recipientsValue: string[] - declare readonly statesValue: string[] - declare readonly currencyValue: string + declare readonly i18nValue: Wording connect() { this.clearTarget.hidden = false @@ -90,7 +92,7 @@ class GradingController extends Controller { locked() { const detail: ToastDetail = { - message: "To pole jest już przyznane i nie można go cofnąć.", + message: this.i18nValue.locked, icon: "fa-lock", } @@ -150,10 +152,8 @@ class GradingController extends Controller { const students = new Set(marked.map((cell) => this.studentOf(cell))).size const sum = marked.reduce((total, cell) => total + rewardOf(cell), 0) - this.barCellsTarget.textContent = - `Zaznaczone teraz: ${count} ${plural(count, this.cellsValue)}` - this.barStudentsTarget.textContent = - `u ${students} ${plural(students, this.studentsValue)}, razem` + this.barCellsTarget.textContent = pluralize(this.i18nValue.cells, count) + this.barStudentsTarget.textContent = pluralize(this.i18nValue.students, students) this.barSumTarget.textContent = `+${sum}` } @@ -185,14 +185,14 @@ class GradingController extends Controller { const students = byStudent.size this.reviewSumTarget.textContent = String(sum) - this.reviewStudentsTarget.textContent = `${students} ${plural(students, this.recipientsValue)}` + this.reviewStudentsTarget.textContent = pluralize(this.i18nValue.recipients, students) this.reviewConfirmSumTarget.textContent = String(sum) } private relabel(box: HTMLInputElement) { - const [empty, marked] = this.statesValue + const template = box.checked ? this.i18nValue.cell_marked : this.i18nValue.cell_empty - box.setAttribute("aria-label", `${box.dataset.ghLabel ?? ""}: ${box.checked ? marked : empty}`) + box.setAttribute("aria-label", interpolate(template, { label: box.dataset.ghLabel ?? "" })) } private get visibleCells(): HTMLElement[] { @@ -234,15 +234,4 @@ function fold(text: string): string { .replace(/ł/g, "l") } -function plural(count: number, forms: string[]): string { - const [one, few, many] = forms - const last = count % 10 - const teens = count % 100 - - if (count === 1) return one - if (last >= 2 && last <= 4 && (teens < 12 || teens > 14)) return few - - return many -} - application.register("grading", GradingController) diff --git a/app/javascript/controllers/group_wizard_controller.ts b/app/javascript/controllers/group_wizard_controller.ts index 15adad7b..2b28dfb0 100644 --- a/app/javascript/controllers/group_wizard_controller.ts +++ b/app/javascript/controllers/group_wizard_controller.ts @@ -1,5 +1,20 @@ import { application } from "@controllers/application" import { Controller } from "@hotwired/stimulus" +import { interpolate } from "@utils/i18n" + +interface Wording { + review_step: string + summary_step: string + counter: string + next: string + create_short: string + create_quick: string + create_empty: string + name_placeholder: string + currency_placeholder: string + ranking_off: string + ranking_on: string +} const LAST_STEP = 3 @@ -22,7 +37,7 @@ class GroupWizardController extends Controller { "back", "cancel", "next", "nextLabel", "nextLabelShort", "nextIcon", "submit", "counter", ] - static values = { presetUrl: String } + static values = { presetUrl: String, i18n: Object } declare readonly formTarget: HTMLFormElement declare readonly gridTarget: HTMLElement @@ -68,6 +83,7 @@ class GroupWizardController extends Controller { declare readonly submitTarget: HTMLInputElement declare readonly counterTarget: HTMLElement declare readonly presetUrlValue: string + declare readonly i18nValue: Wording private step = 0 @@ -180,7 +196,7 @@ class GroupWizardController extends Controller { this.stepTickTargets[index].hidden = !done }) - this.stepLabelTargets[LAST_STEP].textContent = this.quick ? "Przegląd zestawu" : "Podsumowanie" + this.stepLabelTargets[LAST_STEP].textContent = this.quick ? this.i18nValue.review_step : this.i18nValue.summary_step } private renderPanels() { @@ -197,23 +213,23 @@ class GroupWizardController extends Controller { } private renderBar() { - this.counterTarget.textContent = `Krok ${this.step + 1} z ${LAST_STEP + 1}` + this.counterTarget.textContent = interpolate(this.i18nValue.counter, { step: this.step + 1, total: LAST_STEP + 1 }) this.backTarget.hidden = this.step === 0 this.cancelTarget.hidden = this.step !== 0 const last = this.step === LAST_STEP this.nextIconTarget.hidden = last - this.nextLabelTarget.textContent = last ? this.createLabel : "Dalej" - this.nextLabelShortTarget.textContent = last ? "Utwórz grupę" : "Dalej" + this.nextLabelTarget.textContent = last ? this.createLabel : this.i18nValue.next + this.nextLabelShortTarget.textContent = last ? this.i18nValue.create_short : this.i18nValue.next } private get createLabel(): string { - return this.quick ? "Utwórz grupę z zestawem" : "Utwórz pustą grupę" + return this.quick ? this.i18nValue.create_quick : this.i18nValue.create_empty } private renderArt() { - this.nameEchoTarget.textContent = this.nameTarget.value.trim() || "Nazwa grupy" + this.nameEchoTarget.textContent = this.nameTarget.value.trim() || this.i18nValue.name_placeholder this.nameEchoTarget.classList.toggle("gh-placeholder-text", this.nameTarget.value.trim() === "") const art = this.chosen("[icon_glyph]") @@ -242,7 +258,7 @@ class GroupWizardController extends Controller { const name = this.currencyTarget.value.trim() this.currencyEchoTargets.forEach((echo) => { - echo.textContent = name || "Marchewek" + echo.textContent = name || this.i18nValue.currency_placeholder echo.classList.toggle("gh-placeholder-text", name === "") }) @@ -309,13 +325,13 @@ class GroupWizardController extends Controller { this.sumLivesTarget.textContent = this.livesField?.value ?? "" this.sumRankingTarget.textContent = this.rankingTarget.checked ? this.rankingLabel : - "Wyłączony" + this.i18nValue.ranking_off } private get rankingLabel(): string { const select = this.rankingModeTarget.querySelector("select") - return select?.selectedOptions[0]?.textContent?.trim() ?? "Widoczny dla studentów" + return select?.selectedOptions[0]?.textContent?.trim() ?? this.i18nValue.ranking_on } private get livesField(): HTMLInputElement | null { diff --git a/app/javascript/controllers/image_crop_controller.ts b/app/javascript/controllers/image_crop_controller.ts index ff886bc5..2d57742b 100644 --- a/app/javascript/controllers/image_crop_controller.ts +++ b/app/javascript/controllers/image_crop_controller.ts @@ -14,6 +14,7 @@ class ImageCropController extends Controller { static values = { aspect: { type: Number, default: 1.6 }, width: { type: Number, default: 1024 }, + alt: String, } declare readonly inputTarget: HTMLInputElement @@ -25,6 +26,7 @@ class ImageCropController extends Controller { declare readonly hasOwnPreviewTarget: boolean declare readonly aspectValue: number declare readonly widthValue: number + declare readonly altValue: string private cropper: Cropper | null = null private objectUrl: string | null = null @@ -64,7 +66,7 @@ class ImageCropController extends Controller { const image = document.createElement("img") image.src = this.objectUrl - image.alt = "Wgrana grafika" + image.alt = this.altValue this.boxTarget.replaceChildren(image) this.cropper = new Cropper(image, { template: this.template }) diff --git a/app/javascript/controllers/invite_form_controller.ts b/app/javascript/controllers/invite_form_controller.ts index 87a61106..10c490c9 100644 --- a/app/javascript/controllers/invite_form_controller.ts +++ b/app/javascript/controllers/invite_form_controller.ts @@ -1,18 +1,24 @@ import { application } from "@controllers/application" import { Controller } from "@hotwired/stimulus" +import { interpolate, pluralize, pluralTemplate, type PluralForms } from "@utils/i18n" + +// StoryGroupInvitesHelper#invite_form_wording — story_group_invites.summary.* +// plus the locale's short month names, so the date reads like gh_stamp's. +interface Wording { + sentence: string + limit: PluralForms + no_limit: string + expiry: string + no_expiry: string + unit: PluralForms + stamp: string + month_names: (string | null)[] +} class InviteFormController extends Controller { static targets = ["limitSwitch", "expirySwitch", "maxUses", "unit", "date", "time", "summary"] - static values = { - summaryPrefix: String, - limitPrefix: String, - noLimit: String, - expiryPrefix: String, - noExpiry: String, - peopleForms: Array, - unitForms: Array, - } + static values = { i18n: Object } declare readonly limitSwitchTarget: HTMLInputElement declare readonly expirySwitchTarget: HTMLInputElement @@ -22,20 +28,14 @@ class InviteFormController extends Controller { declare readonly timeTarget: HTMLInputElement declare readonly summaryTarget: HTMLElement - declare readonly summaryPrefixValue: string - declare readonly limitPrefixValue: string - declare readonly noLimitValue: string - declare readonly expiryPrefixValue: string - declare readonly noExpiryValue: string - declare readonly peopleFormsValue: string[] - declare readonly unitFormsValue: string[] + declare readonly i18nValue: Wording connect() { this.refresh() } refresh() { - this.unitTarget.textContent = this.plural(this.unitFormsValue, this.maxUses) + this.unitTarget.textContent = pluralTemplate(this.i18nValue.unit, this.maxUses) this.summaryTarget.textContent = this.summary() } @@ -55,21 +55,19 @@ class InviteFormController extends Controller { } private summary(): string { - return `${this.summaryPrefixValue} ${this.limitPhrase()} ${this.expiryPhrase()}.` + return interpolate(this.i18nValue.sentence, { limit: this.limitPhrase(), expiry: this.expiryPhrase() }) } private limitPhrase(): string { - if (!this.limitSwitchTarget.checked) return this.noLimitValue + if (!this.limitSwitchTarget.checked) return this.i18nValue.no_limit - const count = this.maxUses - - return `${this.limitPrefixValue} ${count} ${this.plural(this.peopleFormsValue, count)}` + return pluralize(this.i18nValue.limit, this.maxUses) } private expiryPhrase(): string { - if (!this.expirySwitchTarget.checked) return this.noExpiryValue + if (!this.expirySwitchTarget.checked) return this.i18nValue.no_expiry - return `${this.expiryPrefixValue} ${this.stamp()}` + return interpolate(this.i18nValue.expiry, { date: this.stamp() }) } private stamp(): string { @@ -77,24 +75,18 @@ class InviteFormController extends Controller { if (!year || !month || !day) return "" - return `${day}.${month}, ${this.timeTarget.value}` + return interpolate(this.i18nValue.stamp, { + day, + month, + dayNumber: Number(day), + monthName: this.i18nValue.month_names[Number(month)] ?? month, + time: this.timeTarget.value, + }) } private get maxUses(): number { return Number.parseInt(this.maxUsesTarget.value, 10) || 0 } - - private plural(forms: string[], count: number): string { - const [one, few, many] = forms - const n = Math.abs(count) - const lastTwo = n % 100 - const lastOne = n % 10 - - if (n === 1) return one - if (lastOne >= 2 && lastOne <= 4 && (lastTwo < 12 || lastTwo > 14)) return few - - return many - } } application.register("invite-form", InviteFormController) diff --git a/app/javascript/controllers/item_form_controller.ts b/app/javascript/controllers/item_form_controller.ts index 5bd52c1d..7678fb41 100644 --- a/app/javascript/controllers/item_form_controller.ts +++ b/app/javascript/controllers/item_form_controller.ts @@ -1,5 +1,6 @@ import { application } from "@controllers/application" import { Controller } from "@hotwired/stimulus" +import { andList, interpolate, locale, pluralTemplate, type PluralForms } from "@utils/i18n" const DISCOUNT_CAP = 50 @@ -7,6 +8,34 @@ const EXAMPLE_BADGES = 2 type Part = string | { b: string } +// ItemsHelper#item_form_wording — the same templates the server fills. +interface Wording { + unlock_everyone: string + unlock_restricted: string + unlock_rank: string + unlock_badges: PluralForms + lives_yes: string + lives_no: string + no_discount: string + example: string + max: string + holder_rank: string + holder_badges: PluralForms + holder_all_badges: string + holder_join_and: string + holder_join_with: string + warn_cap: string + warn_rank_everyone: string + warn_rank_gate: string + warn_badge_required: string + discount_up_to: string + seal_rank: string + seal_badge: string + name_placeholder: string + rules_placeholder: string + buy: string +} + class ItemFormController extends Controller { static targets = [ "name", "rules", "story", "price", "zero", "unlockRank", "discountRank", @@ -27,6 +56,7 @@ class ItemFormController extends Controller { badgeNames: Array, exampleRankOrder: Array, exampleBadgeOrder: Array, + i18n: Object, } declare readonly ladderDiscountValue: number @@ -35,6 +65,7 @@ class ItemFormController extends Controller { declare readonly badgeNamesValue: string[] declare readonly exampleRankOrderValue: string[] declare readonly exampleBadgeOrderValue: string[] + declare readonly i18nValue: Wording declare readonly nameTarget: HTMLInputElement declare readonly rulesTarget: HTMLTextAreaElement @@ -98,27 +129,31 @@ class ItemFormController extends Controller { this.applyState() } + private get t(): Wording { + return this.i18nValue + } + private renderCard() { const name = this.nameTarget.value.trim() const rules = this.rulesTarget.value.trim() const story = this.storyTarget.value.trim() - this.placeholder(this.frontNameTarget, name, "Nazwa przedmiotu") - this.placeholder(this.frontRulesTarget, rules, "Co daje studentowi…") + this.placeholder(this.frontNameTarget, name, this.t.name_placeholder) + this.placeholder(this.frontRulesTarget, rules, this.t.rules_placeholder) this.frontFlavorTarget.textContent = story this.frontFlavorTarget.hidden = story === "" this.frontCostTarget.textContent = String(this.price) - this.buyLabelTarget.textContent = `Kup za ${this.price}` + this.buyLabelTarget.textContent = interpolate(this.t.buy, { price: this.price }) this.needTarget.textContent = String(Math.max(1, Math.ceil(this.price * 0.4))) const discount = this.maxDiscount - this.tagDiscTarget.textContent = `Zniżki do −${discount}%` + this.tagDiscTarget.textContent = interpolate(this.t.discount_up_to, { discount }) this.tagDiscTarget.hidden = discount === 0 this.tagLifeTarget.hidden = !this.zeroTarget.checked - this.thumbNameTarget.textContent = name || "Nazwa przedmiotu" + this.thumbNameTarget.textContent = name || this.t.name_placeholder } private renderRequirements() { @@ -134,11 +169,8 @@ class ItemFormController extends Controller { this.reqListTarget.replaceChildren(...lines) const first = requirements[0] - this.sealLabelTarget.textContent = first - ? first.kind === "rank" - ? `Od rangi ${first.name}` - : `Za odznakę ${first.name}` - : "" + const seal = first?.kind === "rank" ? this.t.seal_rank : this.t.seal_badge + this.sealLabelTarget.textContent = first ? interpolate(seal, { name: first.name }) : "" } private renderSentences() { @@ -222,28 +254,19 @@ class ItemFormController extends Controller { const gateIndex = this.unlockRankTarget.selectedIndex if (gate && floorIndex > 0 && gateIndex >= floorIndex) { - notes.push( - gates - ? `Kupić mogą tylko studenci od rangi ${gate}, więc zniżka dla rang obejmie każdego kupującego.` - : "Zniżka dla rang obejmie każdego kupującego.", - ) + notes.push(gates ? interpolate(this.t.warn_rank_gate, { rank: gate }) : this.t.warn_rank_everyone) } const raw = this.rankDiscountReach.discount + this.badgeDiscountValue if (raw > DISCOUNT_CAP) { - notes.push( - `Zniżki sumują się do ${raw}%, a sklep odejmie najwyżej ${DISCOUNT_CAP}%. ` + - "Nadwyżka przepada — rozważ niższe zniżki przy rangach i odznakach.", - ) + notes.push(interpolate(this.t.warn_cap, { raw, cap: DISCOUNT_CAP })) } const required = new Set(this.chosen(this.unlockBadgeTargets).map(({ name }) => name)) this.chosen(this.discountBadgeTargets) .filter(({ name }) => required.has(name)) .forEach(({ name }) => { - notes.push( - `Odznaka ${name} jest wymagana do zakupu, więc jej zniżka obejmie każdego kupującego.`, - ) + notes.push(interpolate(this.t.warn_badge_required, { name })) }) return notes @@ -252,29 +275,29 @@ class ItemFormController extends Controller { private unlockParts(): Part[] { const rank = this.requirements.find(({ kind }) => kind === "rank")?.name ?? "" const badges = this.chosen(this.unlockBadgeTargets).map(({ name }) => name) - const tail = this.zeroTarget.checked - ? ". Także przy 0 życiach." - : ", jeśli mają co najmniej 1 życie." + const lives = this.zeroTarget.checked ? this.t.lives_yes : this.t.lives_no if (!rank && badges.length === 0) { - return ["Każdy student może kupić ten przedmiot", tail] + return fill(this.t.unlock_everyone, { lives }) } - const parts: Part[] = ["Kupią tylko studenci"] - if (rank) parts.push(" z rangą ", { b: rank }, " lub wyższą") - if (badges.length > 0) { - parts.push( - `, którzy mają ${badges.length > 1 ? "wszystkie odznaki" : "odznakę"} `, - { b: andList(badges) }, - ) - } - parts.push(tail) - return parts + return fill(this.t.unlock_restricted, { + rank: rank ? fill(this.t.unlock_rank, { rank: { b: rank } }) : [], + badges: this.badgePhrase(this.t.unlock_badges, badges) ?? [], + lives, + }) + } + + // "odznaką X" / "odznakami X i Y" — a plural template filled with the names. + private badgePhrase(forms: PluralForms, names: string[]): Part[] | null { + if (names.length === 0) return null + + return fill(pluralTemplate(forms, names.length), { badges: { b: andList(names) } }) } private discountParts(): Part[] { if (this.maxDiscount === 0) { - return ["Bez zniżek. Każdy kupujący zapłaci ", { b: String(this.price) }, "."] + return fill(this.t.no_discount, { price: { b: String(this.price) } }) } const { rank, badges, percent } = this.example @@ -282,18 +305,14 @@ class ItemFormController extends Controller { const paid = Math.ceil((this.price * (100 - percent)) / 100) - const names = badges.map((badge) => badge.name) - const phrase: Part[] | null = - names.length === 0 ? null : [names.length > 1 ? "odznakami " : "odznaką ", { b: andList(names) }] - const parts: Part[] = ["Przykładowo: student", ...holderClause(rank, phrase)] - parts.push( - " zaoszczędzi ", - { b: `${percent}%` }, - " i zapłaci ", - { b: String(paid) }, - ` zamiast ${this.price}.`, - ) - return parts + const phrase = this.badgePhrase(this.t.holder_badges, badges.map((badge) => badge.name)) + + return fill(this.t.example, { + holder: this.holderClause(rank, phrase), + percent: { b: `${percent}%` }, + paid: { b: String(paid) }, + price: String(this.price), + }) } private maxParts(): Part[] { @@ -302,11 +321,20 @@ class ItemFormController extends Controller { const reach = this.rankDiscountReach const badges = this.badgeNamesValue - const phrase: Part[] | null = - badges.length === 0 ? null : badges.length === 1 ? ["odznaką ", { b: badges[0] }] : ["wszystkimi odznakami"] - const clause = holderClause(reach.discount > 0 ? reach.name : "", phrase) + const phrase = badges.length > 1 ? [this.t.holder_all_badges] : this.badgePhrase(this.t.holder_badges, badges) - return ["Maksymalnie: student", ...clause, " uzyska zniżkę ", { b: `${percent}%` }, "."] + return fill(this.t.max, { + holder: this.holderClause(reach.discount > 0 ? reach.name : "", phrase), + percent: { b: `${percent}%` }, + }) + } + + private holderClause(rankName: string, badgePhrase: Part[] | null): Part[] { + const rank = rankName ? fill(this.t.holder_rank, { rank: { b: rankName } }) : [] + if (badgePhrase === null) return rank + + const join = rankName ? this.t.holder_join_and : this.t.holder_join_with + return [...rank, ...fill(join, { phrase: badgePhrase })] } private get example(): { rank: string; badges: { name: string; discount: number }[]; percent: number } { @@ -354,7 +382,7 @@ class ItemFormController extends Controller { } return picked - .sort((a, b) => a.localeCompare(b, "pl")) + .sort((a, b) => a.localeCompare(b, locale())) .map((name) => ({ name, discount: all.get(name) ?? 0 })) } @@ -384,7 +412,7 @@ class ItemFormController extends Controller { name: chip.dataset.ghName ?? "", discount: parseInt(chip.dataset.ghDiscount ?? "0", 10) || 0, })) - .sort((a, b) => a.name.localeCompare(b.name, "pl")) + .sort((a, b) => a.name.localeCompare(b.name, locale())) } private selectedOption(select: HTMLSelectElement): HTMLOptionElement | undefined { @@ -413,18 +441,18 @@ class ItemFormController extends Controller { } } -function holderClause(rankName: string, badgePhrase: Part[] | null): Part[] { - const clause: Part[] = [] - if (rankName) clause.push(" z rangą ", { b: rankName }) - if (badgePhrase === null) return clause - - return [...clause, rankName ? " oraz " : " z ", ...badgePhrase] -} +// The client twin of ApplicationHelper#gh_fill: splits a template on its +// %{slots} and drops in plain text, bold names or already-built fragments. +function fill(template: string, vars: Record): Part[] { + return template.split(/(%\{\w+\})/).flatMap((piece): Part[] => { + const key = /^%\{(\w+)\}$/.exec(piece)?.[1] + if (key === undefined) return piece === "" ? [] : [piece] -function andList(names: string[]): string { - if (names.length < 2) return names[0] ?? "" + const value = vars[key] + if (value === undefined) return [piece] - return `${names.slice(0, -1).join(", ")} i ${names[names.length - 1]}` + return Array.isArray(value) ? value : [value] + }) } application.register("item-form", ItemFormController) diff --git a/app/javascript/controllers/preset_row_controller.ts b/app/javascript/controllers/preset_row_controller.ts index 3aa2c20d..37c4f5a6 100644 --- a/app/javascript/controllers/preset_row_controller.ts +++ b/app/javascript/controllers/preset_row_controller.ts @@ -1,17 +1,20 @@ import { application } from "@controllers/application" import { Controller } from "@hotwired/stimulus" +import { interpolate } from "@utils/i18n" class PresetRowController extends Controller { static targets = ["keep", "label", "icon"] + static values = { name: String, remove: String, restore: String } + declare readonly keepTarget: HTMLInputElement declare readonly labelTarget: HTMLElement declare readonly iconTarget: HTMLElement - - private name = "" + declare readonly nameValue: string + declare readonly removeValue: string + declare readonly restoreValue: string connect() { - this.name = this.labelTarget.textContent?.replace(/^\w+:\s*/u, "") ?? "" this.render() } @@ -23,7 +26,7 @@ class PresetRowController extends Controller { const kept = this.keepTarget.checked this.element.classList.toggle("gh-preset-row--removed", !kept) - this.labelTarget.textContent = `${kept ? "Usuń" : "Przywróć"}: ${this.name}` + this.labelTarget.textContent = interpolate(kept ? this.removeValue : this.restoreValue, { name: this.nameValue }) this.iconTarget.classList.toggle("fa-xmark", kept) this.iconTarget.classList.toggle("fa-rotate-left", !kept) diff --git a/app/javascript/controllers/rank_form_controller.ts b/app/javascript/controllers/rank_form_controller.ts index 6f8e3271..d5401b75 100644 --- a/app/javascript/controllers/rank_form_controller.ts +++ b/app/javascript/controllers/rank_form_controller.ts @@ -1,8 +1,31 @@ import { application } from "@controllers/application" import { Controller } from "@hotwired/stimulus" +import { interpolate, pluralize, type PluralForms } from "@utils/i18n" interface Rung { id: number | string; name: string; min: number; disc: number } +interface Wording { + required: string + gives_discount: string + discount: string + starting: string + no_discount: string + name_placeholder: string + new_rank: string + from_short: string + impact_got_lead: string + impact_got: PluralForms + impact_got_tail: string + impact_nobody: string + impact_unchanged: string + impact_changed_lead: string + impact_changed: PluralForms + impact_up: string + impact_down: string + warn_below: string + warn_above: string +} + class RankFormController extends Controller { static targets = [ "name", "threshold", "discount", @@ -15,6 +38,7 @@ class RankFormController extends Controller { rungs: Array, totals: Array, editingId: Number, + i18n: Object, } declare readonly nameTarget: HTMLInputElement @@ -33,6 +57,7 @@ class RankFormController extends Controller { declare readonly rungsValue: Rung[] declare readonly totalsValue: number[] declare readonly editingIdValue: number + declare readonly i18nValue: Wording connect() { this.refresh() @@ -69,9 +94,9 @@ class RankFormController extends Controller { } private renderCard(draft: Rung) { - this.cardNameTarget.textContent = draft.name || "Nazwa rangi" + this.cardNameTarget.textContent = draft.name || this.i18nValue.name_placeholder this.cardNameTarget.classList.toggle("gh-placeholder-text", draft.name === "") - this.cardRulesTarget.textContent = rewardLine(draft) + this.cardRulesTarget.textContent = this.rewardLine(draft) this.renderArt() } @@ -96,9 +121,9 @@ class RankFormController extends Controller { } private renderDraftRow(draft: Rung) { - this.draftNameTarget.textContent = draft.name || "Nowa ranga" - this.draftTermsTarget.textContent = ladderTerms(draft) - this.draftValueTarget.textContent = `od ${draft.min}` + this.draftNameTarget.textContent = draft.name || this.i18nValue.new_rank + this.draftTermsTarget.textContent = this.ladderTerms(draft) + this.draftValueTarget.textContent = interpolate(this.i18nValue.from_short, { min: draft.min }) this.draftRow?.setAttribute("data-min", String(draft.min)) } @@ -127,18 +152,17 @@ class RankFormController extends Controller { private renderImpact(draft: Rung) { const { up, down, got } = this.impact(draft) + const t = this.i18nValue if (!this.editing) { if (got > 0) { this.impactTarget.replaceChildren( - text("Od razu dostanie ją "), - bold(`${got} ${plural(got, "student", "studentów", "studentów")}`), - text(". Pozostali zobaczą ją jako kolejny cel."), + text(t.impact_got_lead), + bold(pluralize(t.impact_got, got)), + text(t.impact_got_tail), ) } else { - this.impactTarget.replaceChildren( - text("Na razie nikt nie ma tylu zebranych. Studenci zobaczą ją jako kolejny cel."), - ) + this.impactTarget.replaceChildren(text(t.impact_nobody)) } return @@ -146,15 +170,18 @@ class RankFormController extends Controller { const moved = up + down if (moved === 0) { - this.impactTarget.replaceChildren(text("Żaden student nie zmieni rangi.")) + this.impactTarget.replaceChildren(text(t.impact_unchanged)) return } - const changes = [up && `${up} awansuje`, down && `${down} spadnie`].filter(Boolean) + const changes = [ + up && interpolate(t.impact_up, { count: up }), + down && interpolate(t.impact_down, { count: down }), + ].filter(Boolean) this.impactTarget.replaceChildren( - text("Po zapisaniu "), - bold(`${moved} ${plural(moved, "student zmieni", "studentów zmieni", "studentów zmieni")} rangę`), + text(t.impact_changed_lead), + bold(pluralize(t.impact_changed, moved)), text(`: ${changes.join(", ")}.`), ) } @@ -187,20 +214,27 @@ class RankFormController extends Controller { const messages: string[] = [] if (below && draft.disc < below.disc) { - messages.push( - `Ranga niżej (${below.name}) daje −${below.disc}%, a ta tylko −${draft.disc}%. ` + - "Awans obniżyłby zniżkę.", - ) + messages.push(interpolate(this.i18nValue.warn_below, { name: below.name, other: below.disc, discount: draft.disc })) } if (above && draft.disc > above.disc) { - messages.push( - `Ranga wyżej (${above.name}) daje tylko −${above.disc}%. Awans na nią obniżyłby zniżkę.`, - ) + messages.push(interpolate(this.i18nValue.warn_above, { name: above.name, other: above.disc })) } this.warningsTarget.replaceChildren(...messages.map(warning)) } + + private rewardLine(rung: Rung): string { + const line = interpolate(this.i18nValue.required, { min: rung.min }) + + return rung.disc > 0 ? `${line} ${interpolate(this.i18nValue.gives_discount, { discount: rung.disc })}` : line + } + + private ladderTerms(rung: Rung): string { + if (rung.disc > 0) return interpolate(this.i18nValue.discount, { discount: rung.disc }) + + return rung.min === 0 ? this.i18nValue.starting : this.i18nValue.no_discount + } } function numberIn(input: HTMLInputElement): number { @@ -218,29 +252,6 @@ function held(total: number, ladder: Rung[]): Rung | null { ) } -function rewardLine(rung: Rung): string { - const line = `Wymagane: ${rung.min} zebranych.` - - return rung.disc > 0 ? `${line} Daje −${rung.disc}% w sklepie.` : line -} - -function ladderTerms(rung: Rung): string { - if (rung.disc > 0) return `−${rung.disc}% w sklepie` - - return rung.min === 0 ? "Ranga startowa" : "Bez zniżki" -} - -function plural(count: number, one: string, few: string, many: string): string { - const n = Math.abs(count) - const lastTwo = n % 100 - const lastOne = n % 10 - - if (n === 1) return one - if (lastOne >= 2 && lastOne <= 4 && (lastTwo < 12 || lastTwo > 14)) return few - - return many -} - function text(value: string): Text { return document.createTextNode(value) } diff --git a/app/javascript/controllers/sheet_create_controller.ts b/app/javascript/controllers/sheet_create_controller.ts index 60735464..2464b0b9 100644 --- a/app/javascript/controllers/sheet_create_controller.ts +++ b/app/javascript/controllers/sheet_create_controller.ts @@ -1,5 +1,11 @@ import { application } from "@controllers/application" import { Controller } from "@hotwired/stimulus" +import { pluralize, type PluralForms } from "@utils/i18n" + +interface Wording { + create_one: string + create_many: PluralForms +} class SheetCreateController extends Controller { static targets = [ @@ -7,7 +13,7 @@ class SheetCreateController extends Controller { ] static values = { - sheets: Array, + i18n: Object, min: { type: Number, default: 2 }, } @@ -20,7 +26,7 @@ class SheetCreateController extends Controller { declare readonly downTarget: HTMLButtonElement declare readonly nameTargets: HTMLElement[] declare readonly submitLabelTarget: HTMLElement - declare readonly sheetsValue: string[] + declare readonly i18nValue: Wording declare readonly minValue: number connect() { @@ -42,9 +48,7 @@ class SheetCreateController extends Controller { chip.hidden = index >= count }) - this.submitLabelTarget.textContent = many - ? `Utwórz ${count} ${plural(count, this.sheetsValue)}` - : `Utwórz ${plural(1, this.sheetsValue)}` + this.submitLabelTarget.textContent = many ? pluralize(this.i18nValue.create_many, count) : this.i18nValue.create_one } up() { @@ -77,15 +81,4 @@ class SheetCreateController extends Controller { } } -function plural(count: number, forms: string[]): string { - const [one, few, many] = forms - const last = count % 10 - const teens = count % 100 - - if (count === 1) return one - if (last >= 2 && last <= 4 && (teens < 12 || teens > 14)) return few - - return many -} - application.register("sheet-create", SheetCreateController) diff --git a/app/javascript/controllers/sheet_form_controller.ts b/app/javascript/controllers/sheet_form_controller.ts index 9077c584..43d5c567 100644 --- a/app/javascript/controllers/sheet_form_controller.ts +++ b/app/javascript/controllers/sheet_form_controller.ts @@ -1,5 +1,17 @@ import { application } from "@controllers/application" import { Controller } from "@hotwired/stimulus" +import { interpolate, pluralize, type PluralForms } from "@utils/i18n" + +interface Wording { + columns: PluralForms + dirty: string + clean: string + name_label: string + reward_label: string + story_label: string + destroy_column: string + destroy_category: string +} class SheetFormController extends Controller { static targets = [ @@ -10,9 +22,8 @@ class SheetFormController extends Controller { ] static values = { - columns: Array, - currency: String, - categoryLabel: String, + i18n: Object, + sheet: Boolean, editing: String, } @@ -30,9 +41,8 @@ class SheetFormController extends Controller { declare readonly previewRowTargets: HTMLElement[] declare readonly previewHeadCellTarget: HTMLTemplateElement declare readonly previewBodyCellTarget: HTMLTemplateElement - declare readonly columnsValue: string[] - declare readonly currencyValue: string - declare readonly categoryLabelValue: string + declare readonly i18nValue: Wording + declare readonly sheetValue: boolean declare readonly editingValue: string private destination = "" @@ -167,7 +177,7 @@ class SheetFormController extends Controller { const count = columns.length const max = columns.reduce((total, row) => total + this.reward(row), 0) - this.summaryCountTarget.textContent = `${count} ${plural(count, this.columnsValue)}` + this.summaryCountTarget.textContent = pluralize(this.i18nValue.columns, count) this.summaryMaxTarget.textContent = String(max) } @@ -176,7 +186,7 @@ class SheetFormController extends Controller { const dirty = this.dirty - this.dirtyTarget.textContent = dirty ? "Niezapisane zmiany" : "Brak zmian" + this.dirtyTarget.textContent = dirty ? this.i18nValue.dirty : this.i18nValue.clean this.submitTarget.disabled = !dirty } @@ -189,12 +199,14 @@ class SheetFormController extends Controller { const position = row.querySelector("[data-sheet-form-target='position']") if (position) position.value = String(number - 1) - this.relabel(row, "name", `Kategoria ${number}: za co`) - this.relabel(row, "reward", `Nagroda za kategorię ${number}`) - this.relabel(row, "destroy", `Usuń ${this.categoryLabelValue} ${number}`) + const t = this.i18nValue + + this.relabel(row, "name", interpolate(t.name_label, { number })) + this.relabel(row, "reward", interpolate(t.reward_label, { number })) + this.relabel(row, "destroy", interpolate(this.sheetValue ? t.destroy_column : t.destroy_category, { number })) const story = row.querySelector("textarea") - story?.setAttribute("aria-label", `Opis fabularny kategorii ${number}`) + story?.setAttribute("aria-label", interpolate(t.story_label, { number })) }) const rows = this.liveRows @@ -269,15 +281,4 @@ class SheetFormController extends Controller { } } -function plural(count: number, forms: string[]): string { - const [one, few, many] = forms - const last = count % 10 - const teens = count % 100 - - if (count === 1) return one - if (last >= 2 && last <= 4 && (teens < 12 || teens > 14)) return few - - return many -} - application.register("sheet-form", SheetFormController) diff --git a/app/javascript/controllers/sidebar_controller.ts b/app/javascript/controllers/sidebar_controller.ts index e0af28e5..0934ed5d 100644 --- a/app/javascript/controllers/sidebar_controller.ts +++ b/app/javascript/controllers/sidebar_controller.ts @@ -5,8 +5,8 @@ class SidebarController extends Controller { static targets = ["sidebar", "label", "icon"] static values = { cookie: { type: String, default: "sidebar_collapsed" }, - expandLabel: { type: String, default: "Rozwiń" }, - collapseLabel: { type: String, default: "Zwiń panel" }, + expandLabel: String, + collapseLabel: String, } declare readonly sidebarTarget: HTMLElement diff --git a/app/javascript/controllers/teacher_picker_controller.ts b/app/javascript/controllers/teacher_picker_controller.ts index 7f5abfa5..cb79e9f0 100644 --- a/app/javascript/controllers/teacher_picker_controller.ts +++ b/app/javascript/controllers/teacher_picker_controller.ts @@ -1,15 +1,19 @@ import { application } from "@controllers/application" import { Controller } from "@hotwired/stimulus" +import { interpolate, pluralize, type PluralForms } from "@utils/i18n" + +interface Wording { + results: PluralForms + overflow: string + query: string +} class TeacherPickerController extends Controller { static targets = ["input", "row", "name", "count", "hint", "results", "overflow", "empty", "emptyQuery"] static values = { min: { type: Number, default: 2 }, max: { type: Number, default: 8 }, - one: String, - few: String, - many: String, - overflow: String, + i18n: Object, } declare readonly inputTarget: HTMLInputElement @@ -22,10 +26,7 @@ class TeacherPickerController extends Controller { declare readonly emptyQueryTarget: HTMLElement declare readonly minValue: number declare readonly maxValue: number - declare readonly oneValue: string - declare readonly fewValue: string - declare readonly manyValue: string - declare readonly overflowValue: string + declare readonly i18nValue: Wording noSubmit(event: KeyboardEvent) { event.preventDefault() @@ -46,7 +47,7 @@ class TeacherPickerController extends Controller { if (hits.length === 0) { this.rowTargets.forEach((row) => this.plain(row)) - this.emptyQueryTarget.textContent = `„${typed}”.` + this.emptyQueryTarget.textContent = interpolate(this.i18nValue.query, { query: typed }) this.show({ empty: true }) return @@ -62,7 +63,9 @@ class TeacherPickerController extends Controller { const over = hits.length > this.maxValue - this.countTarget.textContent = `${hits.length} ${this.plural(hits.length)}${over ? this.overflowValue : ""}` + const overflow = over ? interpolate(this.i18nValue.overflow, { max: this.maxValue }) : "" + + this.countTarget.textContent = `${pluralize(this.i18nValue.results, hits.length)}${overflow}` this.show({ count: true, results: true, overflow: over }) } @@ -102,17 +105,6 @@ class TeacherPickerController extends Controller { this.resultsTarget.hidden = !state.results this.overflowTarget.hidden = !state.overflow } - - private plural(count: number): string { - const n = Math.abs(count) - - if (n === 1) return this.oneValue - - const lastOne = n % 10 - const lastTwo = n % 100 - - return lastOne >= 2 && lastOne <= 4 && (lastTwo < 12 || lastTwo > 14) ? this.fewValue : this.manyValue - } } function fold(text: string): string { diff --git a/app/javascript/utils/i18n.ts b/app/javascript/utils/i18n.ts new file mode 100644 index 00000000..3937928c --- /dev/null +++ b/app/javascript/utils/i18n.ts @@ -0,0 +1,34 @@ +// Client-side halves of Rails I18n. The wording itself always comes from the +// view (config/locales/**), passed in as a Stimulus value; these only fill it in. + +// A pluralised key as Rails returns it when called without `count:` — +// pl has one/few/many/other, en has one/other. +export type PluralForms = Partial> + +type Vars = Record + +// The page's language, set on by the Localization concern. +export function locale(): string { + return document.documentElement.lang || "pl" +} + +// "%{name} ma %{count}" + { name: "Ala", count: 3 } -> "Ala ma 3". +export function interpolate(template: string, vars: Vars = {}): string { + return template.replace(/%\{(\w+)\}/g, (match, key: string) => (key in vars ? String(vars[key]) : match)) +} + +// The form for `count`, unfilled — the same rule rails-i18n applies on the server. +export function pluralTemplate(forms: PluralForms, count: number): string { + const rule = new Intl.PluralRules(locale()).select(count) + + return forms[rule] ?? forms.other ?? "" +} + +export function pluralize(forms: PluralForms, count: number, vars: Vars = {}): string { + return interpolate(pluralTemplate(forms, count), { count, ...vars }) +} + +// "a, b i c" / "a, b, and c" — matches Array#to_sentence with rails-i18n. +export function andList(names: string[]): string { + return new Intl.ListFormat(locale(), { type: "conjunction" }).format(names) +} diff --git a/app/mailers/invitation_mailer.rb b/app/mailers/invitation_mailer.rb index 28a3e886..f27b29b3 100644 --- a/app/mailers/invitation_mailer.rb +++ b/app/mailers/invitation_mailer.rb @@ -1,17 +1,13 @@ # frozen_string_literal: true class InvitationMailer < ApplicationMailer - ROLE_LABELS = { - admin: 'administrator', - member: 'członek', - }.freeze - def invitation_email @token_link = params[:token_link] @email = params[:email] @organization_name = params[:organization_name] - @role_label = ROLE_LABELS[params[:role]] + @role_label = t("invitation_mailer.roles.#{params[:role]}") - mail to: @email, subject: 'Zaproszenie do systemu GamificationHub' + # Subject: invitation_mailer.invitation_email.subject (default_i18n_subject). + mail to: @email end end diff --git a/app/mailers/passwordless_mailer.rb b/app/mailers/passwordless_mailer.rb index 1609e469..797a643e 100644 --- a/app/mailers/passwordless_mailer.rb +++ b/app/mailers/passwordless_mailer.rb @@ -9,6 +9,7 @@ def token_email @first_name = params[:user_name].to_s.split(' ').first @expires_in_minutes = LoginToken::EXPIRES_IN.in_minutes.to_i - mail to: @email, subject: 'Logowanie do systemu Gamification Hub' + # Subject: passwordless_mailer.token_email.subject (default_i18n_subject). + mail to: @email end end diff --git a/app/models/activity_group.rb b/app/models/activity_group.rb index 09dc9e05..dbd97927 100644 --- a/app/models/activity_group.rb +++ b/app/models/activity_group.rb @@ -10,7 +10,7 @@ class ActivityGroup < ApplicationRecord accepts_nested_attributes_for :activity_group_categories, allow_destroy: true - validates :name, presence: { message: 'Podaj nazwę arkusza.' }, length: { maximum: 100 } + validates :name, presence: true, length: { maximum: 100 } validate :at_least_one_visible_category, on: :settings scope :newest_first, -> { order(created_at: :desc, id: :desc) } @@ -51,6 +51,6 @@ def next_number_for_base(template) def at_least_one_visible_category return if activity_group_categories.any? { |category| !category.marked_for_destruction? && !category.hidden? } - errors.add(:base, 'Dodaj co najmniej jedną widoczną kategorię.') + errors.add(:base, :no_visible_category) end end diff --git a/app/models/activity_group_template.rb b/app/models/activity_group_template.rb index 192322f3..15a97cdf 100644 --- a/app/models/activity_group_template.rb +++ b/app/models/activity_group_template.rb @@ -9,7 +9,7 @@ class ActivityGroupTemplate < ApplicationRecord accepts_nested_attributes_for :categories, allow_destroy: true - validates :base_name, presence: { message: 'Podaj nazwę szablonu.' }, length: { maximum: 100 } + validates :base_name, presence: true, length: { maximum: 100 } validate :at_least_one_category, on: :settings scope :newest_first, -> { order(created_at: :desc, id: :desc) } @@ -21,6 +21,6 @@ def max_reward = categories.sum { |category| category.reward.to_i } def at_least_one_category return if categories.any? { |category| !category.marked_for_destruction? } - errors.add(:base, 'Dodaj co najmniej jedną widoczną kategorię.') + errors.add(:base, :no_category) end end diff --git a/app/models/badge.rb b/app/models/badge.rb index 68e6ae7b..bf5cce44 100644 --- a/app/models/badge.rb +++ b/app/models/badge.rb @@ -10,18 +10,16 @@ class Badge < ApplicationRecord has_icon_art :icon, glyphs: Glyphs::BADGE - validates :name, presence: { message: 'Podaj nazwę odznaki.' }, - length: { maximum: 50 } + validates :name, presence: true, length: { maximum: 50 } - validates :didactic_description, presence: { message: 'Napisz, jak zdobyć tę odznakę.' }, - length: { maximum: 255 } + validates :didactic_description, presence: true, length: { maximum: 255 } validates :story_description, length: { maximum: 255 } validates :discount, numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 100, - message: 'Zniżka musi mieścić się między 0 a 100%.', + message: :out_of_range, } scope :by_name, -> { order(:name, :id) } diff --git a/app/models/concerns/iconable.rb b/app/models/concerns/iconable.rb index 5350e46f..82f1f6fe 100644 --- a/app/models/concerns/iconable.rb +++ b/app/models/concerns/iconable.rb @@ -6,7 +6,7 @@ module Iconable ACCEPTABLE_ICON_TYPES = ['image/gif', 'image/jpeg', 'image/png'].freeze class_methods do - def has_icon_art(attribute, glyphs:, glyph_message: 'Nieznana grafika.', required: true) + def has_icon_art(attribute, glyphs:, glyph_message: :unknown_art, required: true) has_one_attached attribute glyph_attribute = :"#{attribute}_glyph" @@ -41,7 +41,7 @@ def define_icon_content_type_validator(validator_name, attribute) next unless attachment.attached? next if Iconable::ACCEPTABLE_ICON_TYPES.include?(attachment.content_type) - errors.add(attribute, 'Grafika musi być plikiem GIF, JPG lub PNG.') + errors.add(attribute, :art_type) end end @@ -49,7 +49,7 @@ def define_icon_art_chosen_validator(validator_name, attribute, glyph_attribute) define_method(validator_name) do next if public_send(glyph_attribute).present? || public_send(attribute).attached? - errors.add(glyph_attribute, 'Wybierz gotową grafikę albo wgraj własną.') + errors.add(glyph_attribute, :art_missing) end end diff --git a/app/models/concerns/reward_category.rb b/app/models/concerns/reward_category.rb index 8d1e93ee..9886685b 100644 --- a/app/models/concerns/reward_category.rb +++ b/app/models/concerns/reward_category.rb @@ -6,12 +6,12 @@ module RewardCategory included do default_scope { order(position: :asc) } - validates :didactic_description, presence: { message: 'Podaj, za co jest nagroda.' } + validates :didactic_description, presence: true validates :reward, numericality: { only_integer: true, greater_than_or_equal_to: 1, - message: 'Nagroda musi wynosić co najmniej 1.', + message: :too_low, } end end diff --git a/app/models/item.rb b/app/models/item.rb index 12839098..8940f241 100644 --- a/app/models/item.rb +++ b/app/models/item.rb @@ -17,21 +17,20 @@ class Item < ApplicationRecord has_many :discount_badges, through: :items_min_badges_for_discounts, source: :badge has_many :students_items, dependent: :destroy + has_many :notifications, dependent: :destroy has_icon_art :icon, glyphs: Glyphs::ITEM - validates :name, presence: { message: 'Podaj nazwę przedmiotu.' }, - length: { maximum: 50 } + validates :name, presence: true, length: { maximum: 50 } - validates :didactic_description, presence: { message: 'Napisz, co przedmiot daje studentowi.' }, - length: { maximum: 255 } + validates :didactic_description, presence: true, length: { maximum: 255 } validates :story_description, length: { maximum: 255 } validates :price, numericality: { only_integer: true, greater_than_or_equal_to: 1, - message: 'Cena musi wynosić co najmniej 1.', + message: :too_low, } scope :by_price, -> { order(:price, :name, :id) } diff --git a/app/models/organization.rb b/app/models/organization.rb index 0e226bc3..efabda32 100644 --- a/app/models/organization.rb +++ b/app/models/organization.rb @@ -34,13 +34,13 @@ def max_members_not_below_members_count members_count = users.count return if max_members >= members_count - errors.add(:max_members, "can't be lower than the current number of members (#{members_count})") + errors.add(:max_members, :below_members, count: members_count) end def keep_usos_organization return unless usos? - errors.add(:base, 'Organizacji powiązanej z USOS nie można usunąć.') + errors.add(:base, :usos_undeletable) throw :abort end end diff --git a/app/models/rank.rb b/app/models/rank.rb index e08cd720..de8917e3 100644 --- a/app/models/rank.rb +++ b/app/models/rank.rb @@ -7,19 +7,18 @@ class Rank < ApplicationRecord has_icon_art :icon, glyphs: Glyphs::RANK - validates :name, presence: { message: 'Podaj nazwę rangi.' }, - length: { maximum: 40 } + validates :name, presence: true, length: { maximum: 40 } validates :discount, numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 100, - message: 'Zniżka musi mieścić się między 0 a 100%.', + message: :out_of_range, } validates :required_currency_value, numericality: { only_integer: true, greater_than_or_equal_to: 0, - message: 'Próg nie może być ujemny.', + message: :negative, } validate :threshold_free_in_group @@ -34,8 +33,8 @@ def threshold_free_in_group .first return if clash.nil? - errors.add(:required_currency_value, - "Ranga #{clash.name} ma już próg #{clash.required_currency_value}. Wybierz inny.",) + errors.add(:required_currency_value, :threshold_taken, + rank: clash.name, threshold: clash.required_currency_value,) end def starting? diff --git a/app/models/story_group.rb b/app/models/story_group.rb index 13d98ee2..34f35b47 100644 --- a/app/models/story_group.rb +++ b/app/models/story_group.rb @@ -5,10 +5,9 @@ class StoryGroup < ApplicationRecord enum :ranking_mode, { podium_and_own: 0, full: 1 }, prefix: :ranking - RANKING_MODE_LABELS = { - 'podium_and_own' => 'Podium i własne miejsce', - 'full' => 'Pełny ranking', - }.freeze + class << self + def ranking_mode_label(mode) = I18n.t("story_groups.ranking_modes.#{mode}") + end belongs_to :owner, class_name: 'User', foreign_key: 'owner_id' has_many :notifications, dependent: :destroy @@ -24,20 +23,18 @@ class StoryGroup < ApplicationRecord has_many :invites, class_name: 'StoryGroupInvite', foreign_key: 'story_group_id', dependent: :destroy has_icon_art :icon, glyphs: GroupArt::KEYS, required: false - has_icon_art :currency_icon, glyphs: CurrencyIcons::KEYS, glyph_message: 'Nieznana ikona.', required: false + has_icon_art :currency_icon, glyphs: CurrencyIcons::KEYS, glyph_message: :unknown_icon, required: false - validates :name, presence: { message: 'Podaj nazwę grupy.' }, - length: { maximum: 40 } - validates :currency_name, presence: { message: 'Podaj nazwę waluty.' }, - length: { maximum: 40 } + validates :name, presence: true, length: { maximum: 40 } + validates :currency_name, presence: true, length: { maximum: 40 } validates :description, length: { maximum: 1024 } validates :default_lives, numericality: { only_integer: true, greater_than_or_equal_to: 0 } delegate :organization, to: :owner def ranking_summary - return 'Wyłączony' unless ranking_enabled? + return I18n.t('story_groups.ranking_off') unless ranking_enabled? - RANKING_MODE_LABELS.fetch(ranking_mode, ranking_mode) + self.class.ranking_mode_label(ranking_mode) end end diff --git a/app/models/story_group_invite.rb b/app/models/story_group_invite.rb index b92e9896..957767b3 100644 --- a/app/models/story_group_invite.rb +++ b/app/models/story_group_invite.rb @@ -1,18 +1,10 @@ # frozen_string_literal: true class StoryGroupInvite < ApplicationRecord - include ApplicationHelper - CODE_LENGTH = 6 CODE_ALPHABET = 'ABCDEFGHJKMNPQRSTUVWXYZ23456789' - STATUS_LABELS = { - active: 'Aktywne', - expired: 'Wygasło', - exhausted: 'Wyczerpane', - }.freeze - before_create :generate_code belongs_to :story_group @@ -24,7 +16,7 @@ class StoryGroupInvite < ApplicationRecord numericality: { only_integer: true, greater_than_or_equal_to: 1, - message: 'Limit musi wynosić co najmniej 1.', + message: :too_low, }, allow_nil: true @@ -73,7 +65,7 @@ def status def active? = status == :active - def status_label = STATUS_LABELS[status] + def status_label = I18n.t("story_group_invites.statuses.#{status}") def use! return unless usable? @@ -86,10 +78,6 @@ def use! def max_uses_covers_existing_uses return if max_uses.blank? || uses.to_i.zero? || max_uses >= uses - errors.add( - :max_uses, - "Z tego kodu skorzystało już #{uses} #{gh_plural(uses, 'osoba', 'osoby', 'osób')}. " \ - 'Limit nie może być mniejszy.', - ) + errors.add(:max_uses, :below_uses, count: uses) end end diff --git a/app/models/story_group_student.rb b/app/models/story_group_student.rb index dce4cd5d..e7f7ac1c 100644 --- a/app/models/story_group_student.rb +++ b/app/models/story_group_student.rb @@ -23,11 +23,10 @@ class StoryGroupStudent < ApplicationRecord validates :nickname, - length: { in: 2..24, message: 'Pseudonim musi mieć od 2 do 24 znaków.' }, + length: { in: 2..24, message: :length }, uniqueness: { scope: :story_group_id, case_sensitive: false, - message: 'Ten pseudonim jest już zajęty w tej grupie.', }, allow_blank: true diff --git a/app/models/story_group_teacher.rb b/app/models/story_group_teacher.rb index c49ac518..1b18c63b 100644 --- a/app/models/story_group_teacher.rb +++ b/app/models/story_group_teacher.rb @@ -7,5 +7,5 @@ class StoryGroupTeacher < ApplicationRecord delegate :full_name, :university_number, :email, to: :user scope :with_user, -> { includes(:user) } - validates :user_id, uniqueness: { scope: :story_group_id, message: 'Ta osoba jest już w grupie.' } + validates :user_id, uniqueness: { scope: :story_group_id } end diff --git a/app/models/user.rb b/app/models/user.rb index 06a79fbd..f0edfad4 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -65,6 +65,6 @@ def consume_login_token! private def organization_has_free_slot - errors.add(:organization, 'has reached its member limit') if organization&.full? + errors.add(:organization, :member_limit) if organization&.full? end end diff --git a/app/services/currency_adjustment_form.rb b/app/services/currency_adjustment_form.rb index 406de213..db7c9f44 100644 --- a/app/services/currency_adjustment_form.rb +++ b/app/services/currency_adjustment_form.rb @@ -59,11 +59,11 @@ def validate errors.clear if value.nil? - errors.add(:amount, 'Podaj kwotę.') + errors.add(:amount, :missing) elsif value.zero? - errors.add(:amount, 'Podaj kwotę większą od zera.') + errors.add(:amount, :not_positive) elsif new_balance.negative? - errors.add(:amount, "Student ma tylko #{balance} do wydania.") + errors.add(:amount, :overdrawn, balance: balance) end errors.empty? diff --git a/app/services/currency_ledger.rb b/app/services/currency_ledger.rb index 4c88bd61..eb229ac0 100644 --- a/app/services/currency_ledger.rb +++ b/app/services/currency_ledger.rb @@ -2,14 +2,7 @@ class CurrencyLedger # The filter chips, in the order they are shown. `nil` is "Wszystkie". - # Plural, unlike LedgerEntry::KIND_LABELS — a chip names a set of rows, a - # chip on a row names one. - KINDS = [ - [nil, 'Wszystkie'], - %w[reward Nagrody], - %w[purchase Zakupy], - %w[adjustment Korekty], - ].freeze + KINDS = [nil, 'reward', 'purchase', 'adjustment'].freeze def initialize(student:) @student = student @@ -76,11 +69,15 @@ def build_entry(transaction, running) def title_for(transaction, subject) case subject when Item then subject.name - when ActivityGroupCategory then subject.didactic_description.presence || 'Nagroda' - else transaction.reward? ? 'Nagroda' : 'Korekta waluty' + when ActivityGroupCategory then subject.didactic_description.presence || entry_title(:reward) + else entry_title(transaction.reward? ? :reward : :adjustment) end end + def entry_title(key) + I18n.t("currency_transactions.entry_titles.#{key}") + end + # The second, smaller line. It answers "where did this come from" — the # sheet for a reward, the teacher for a correction. A purchase needs none: # the item name above it is the whole answer. diff --git a/app/services/dashboard/global_admin.rb b/app/services/dashboard/global_admin.rb index fd77c8d6..50cc452d 100644 --- a/app/services/dashboard/global_admin.rb +++ b/app/services/dashboard/global_admin.rb @@ -22,10 +22,10 @@ def to_partial_path = 'home/global_admin_start' def stat_tiles [ - Dashboard::Tile.new(label: 'Organizacje', value: organizations.size, note: nil), - Dashboard::Tile.new(label: 'Konta w organizacjach', value: organizations.sum(&:member_count), note: nil), - Dashboard::Tile.new(label: 'Wolne miejsca', value: organizations.sum { |org| org.free.to_i }, note: nil), - Dashboard::Tile.new(label: 'Niezaakceptowane zaproszenia', value: organizations.sum(&:pending_admins), note: nil), + Dashboard::Tile.new(label: tile_label(:organizations), value: organizations.size, note: nil), + Dashboard::Tile.new(label: tile_label(:accounts), value: organizations.sum(&:member_count), note: nil), + Dashboard::Tile.new(label: tile_label(:free), value: organizations.sum { |org| org.free.to_i }, note: nil), + Dashboard::Tile.new(label: tile_label(:pending), value: organizations.sum(&:pending_admins), note: nil), ] end @@ -35,6 +35,10 @@ def attention? = without_admin.any? || nearly_full.any? private + def tile_label(key) + I18n.t("home.global_admin_start.tiles.#{key}") + end + def build_organizations organizations = Organization.order(:name).to_a users = User.where(organization: organizations).group(:organization_id) diff --git a/app/services/dashboard/organization_admin.rb b/app/services/dashboard/organization_admin.rb index 825fa4e2..442f9343 100644 --- a/app/services/dashboard/organization_admin.rb +++ b/app/services/dashboard/organization_admin.rb @@ -27,19 +27,24 @@ def to_partial_path = 'home/organization_admin_start' def stat_tiles [ - Dashboard::Tile.new(label: 'Studenci', value: @counts.fetch('student', 0), note: nil), - Dashboard::Tile.new(label: 'Nauczyciele', value: @counts.fetch('teacher', 0), note: nil), + Dashboard::Tile.new(label: tile_text(:students), value: @counts.fetch('student', 0), note: nil), + Dashboard::Tile.new(label: tile_text(:teachers), value: @counts.fetch('teacher', 0), note: nil), free_tile, - Dashboard::Tile.new(label: 'Oczekujące zaproszenia', value: pending_users.size, note: nil), + Dashboard::Tile.new(label: tile_text(:pending), value: pending_users.size, note: nil), ] end private def free_tile - return Dashboard::Tile.new(label: 'Wolne miejsca', value: 'Bez limitu', note: nil) if unlimited? + return Dashboard::Tile.new(label: tile_text(:free), value: tile_text(:no_limit), note: nil) if unlimited? - Dashboard::Tile.new(label: 'Wolne miejsca', value: free, note: "z #{organization.max_members}") + Dashboard::Tile.new(label: tile_text(:free), value: free, + note: tile_text(:of_max, max: organization.max_members),) + end + + def tile_text(key, **) + I18n.t("home.organization_admin_start.tiles.#{key}", **) end def invitable diff --git a/app/services/dashboard/teacher.rb b/app/services/dashboard/teacher.rb index 71b76912..98cbed25 100644 --- a/app/services/dashboard/teacher.rb +++ b/app/services/dashboard/teacher.rb @@ -2,7 +2,7 @@ class Dashboard::Teacher PURCHASE_LIMIT = 30 - DAYS = %w[Dziś Wczoraj Wcześniej].freeze + DAYS = %i[today yesterday earlier].freeze RECENT_DAYS = DAYS.first(2).freeze Purchase = Data.define(:transaction, :student, :story_group, :item, :day) do @@ -69,7 +69,7 @@ def build_groups summaries = story_groups.map do |story_group| Group.new( story_group: story_group, - role: story_group.owner_id == @user.id ? 'Prowadzisz' : 'Wspierasz', + role: I18n.t("story_groups.listing.roles.#{story_group.owner_id == @user.id ? 'own' : 'sup'}"), students_count: counts.fetch(story_group.id, 0), new_purchases: fresh.fetch(story_group.id, []).size, ) diff --git a/app/services/group_chrome.rb b/app/services/group_chrome.rb index 6995d2bd..17389850 100644 --- a/app/services/group_chrome.rb +++ b/app/services/group_chrome.rb @@ -1,13 +1,6 @@ # frozen_string_literal: true class GroupChrome - ROLE_LABELS = { - own: 'Prowadzisz tę grupę', - sup: 'Wspierasz tę grupę', - lrn: 'Jesteś uczestnikiem', - adm: 'Nadzorujesz tę grupę', - }.freeze - attr_reader :user, :story_group, :student_membership class << self @@ -33,7 +26,9 @@ def member? = student? || owner? || supporting_teacher? || user.administers?(sto def name = story_group.name - def role_label = ROLE_LABELS[role] + def role_label + I18n.t("layouts.chrome_roles.#{role}") + end def role return :lrn if student? diff --git a/app/services/invite_form.rb b/app/services/invite_form.rb index 82724749..f09d990e 100644 --- a/app/services/invite_form.rb +++ b/app/services/invite_form.rb @@ -63,25 +63,25 @@ def save def error_for(attribute) = invite.errors[attribute].first def summary - "Kod będzie działał #{limit_phrase} #{expiry_phrase}." + text(:sentence, limit: limit_phrase, expiry: expiry_phrase) end - def unit_label = gh_plural(max_uses.to_i, 'osoba', 'osoby', 'osób') + def unit_label = text(:unit, count: max_uses.to_i) private - def limit_phrase - return 'bez limitu osób' unless limit_enabled? + def text(key, **) = I18n.t("story_group_invites.summary.#{key}", **) - count = max_uses.to_i + def limit_phrase + return text(:no_limit) unless limit_enabled? - "dla #{count} #{gh_plural(count, 'osoby', 'osób', 'osób')}" + text(:limit, count: max_uses.to_i) end def expiry_phrase - return 'i bez daty ważności' unless expiry_enabled? + return text(:no_expiry) unless expiry_enabled? - "do #{gh_stamp(combined_expiry)}" + text(:expiry, date: gh_stamp(combined_expiry)) end def combined_expiry @@ -92,6 +92,6 @@ def reject_past_expiry return unless expiry_enabled? return if combined_expiry&.future? - invite.errors.add(:expires_at, 'Ta data już minęła. Wybierz późniejszą.') + invite.errors.add(:expires_at, :past) end end diff --git a/app/services/invite_lookup.rb b/app/services/invite_lookup.rb index 6fe53c2b..4e8a5544 100644 --- a/app/services/invite_lookup.rb +++ b/app/services/invite_lookup.rb @@ -1,21 +1,13 @@ # frozen_string_literal: true class InviteLookup - MESSAGES = { - missing: 'Podaj kod zaproszenia.', - not_found: 'Nie znaleźliśmy takiego kodu. Sprawdź go z prowadzącym.', - expired: 'Ten kod wygasł. Poproś prowadzącego o nowy.', - exhausted: 'Z tego kodu skorzystała już maksymalna liczba osób. ' \ - 'Poproś prowadzącego o nowy.', - }.freeze - Result = Data.define(:invite, :reason, :member_of) do def initialize(invite: nil, reason: nil, member_of: nil) super end def ok? = reason.nil? && member_of.nil? - def error = reason && MESSAGES[reason] + def error = reason && I18n.t("join.errors.#{reason}") def story_group = invite&.story_group end diff --git a/app/services/item_purchase_service.rb b/app/services/item_purchase_service.rb index 25ad0d4d..cbcc8342 100644 --- a/app/services/item_purchase_service.rb +++ b/app/services/item_purchase_service.rb @@ -28,7 +28,7 @@ def call price = PriceCalculatorService.new(price: @item.price, discount: discount_info).calculate if @student.current_currency < price - return Result.new(['Masz za mało waluty, aby kupić ten przedmiot.']) + return Result.new([I18n.t('shop.errors.not_enough')]) end execute_purchase!(price, discount_info.value) @@ -38,7 +38,7 @@ def call Result.new rescue StandardError => e - Result.new(["Wystąpił błąd podczas zakupu: #{e.message}"]) + Result.new([I18n.t('shop.errors.failed', message: e.message)]) end private diff --git a/app/services/purchase_eligibility_service.rb b/app/services/purchase_eligibility_service.rb index 97acb37d..7ea5dcd4 100644 --- a/app/services/purchase_eligibility_service.rb +++ b/app/services/purchase_eligibility_service.rb @@ -36,7 +36,7 @@ def call def check_lives! return unless @student.lives == 0 && !@item.can_buy_at_0_lives - @errors << 'Wymagane jest posiadanie przynajmniej jednego życia.' + @errors << I18n.t('shop.errors.no_lives') @reasons << Reason.new(:lives, nil) end @@ -45,7 +45,7 @@ def check_rank! return unless @student.rank.nil? || @student.total_currency < @item.unlock_rank.required_currency_value - @errors << "Wymagana ranga: #{@item.unlock_rank.name}." + @errors << I18n.t('shop.errors.rank_required', rank: @item.unlock_rank.name) @reasons << Reason.new(:rank, @item.unlock_rank) end @@ -57,7 +57,7 @@ def check_badges! return if missing_badges.none? badge_names = missing_badges.map(&:name).join(', ') - @errors << "Brakujące odznaki: #{badge_names}." + @errors << I18n.t('shop.errors.badges_missing', badges: badge_names) # One reason per badge, unlike the single error above: a card lists them as # separate lines and the seal names only the first. missing_badges.sort_by { |badge| badge.name.to_s } diff --git a/app/services/shop.rb b/app/services/shop.rb index 9943b483..33e8a304 100644 --- a/app/services/shop.rb +++ b/app/services/shop.rb @@ -1,11 +1,7 @@ # frozen_string_literal: true class Shop - ZONES = [ - [:afford, 'Stać cię teraz'], - [:save, 'Zbierasz na to'], - [:sealed, 'Zapieczętowane'], - ].freeze + ZONES = %i[afford save sealed].freeze KIND_ORDER = { rank: 0, badge: 1, lives: 2 }.freeze @@ -19,10 +15,10 @@ def initialize(story_group:, student:) end def zones - @zones ||= ZONES.filter_map do |key, label| + @zones ||= ZONES.filter_map do |key| found = offers.select { |offer| state_of(offer.item) == key } - Zone.new(key: key, label: label, offers: found) if found.any? + Zone.new(key: key, label: I18n.t("shop.zones.#{key}"), offers: found) if found.any? end end diff --git a/app/services/starter_pack_builder.rb b/app/services/starter_pack_builder.rb index 640f44c8..1a7a1415 100644 --- a/app/services/starter_pack_builder.rb +++ b/app/services/starter_pack_builder.rb @@ -70,7 +70,7 @@ def create_items(ranks, badges) end def create_template - template = story_group.activity_group_templates.new(base_name: StarterPack::TEMPLATE_NAME) + template = story_group.activity_group_templates.new(base_name: StarterPack.template_name) kept(:cats, preset.categories).each_with_index do |category, position| template.categories.new(didactic_description: category.didactic_description, @@ -101,7 +101,7 @@ def row_for(zone, index) def guard_threshold!(seen, value, name) if seen.key?(value) raise InvalidSelection, - "Dwie rangi nie mogą mieć tego samego progu: #{seen[value]} i #{name} mają #{value}." + I18n.t('story_groups.flash.duplicate_threshold', first: seen[value], second: name, value: value) end seen[value] = name diff --git a/app/services/story_groups_listing.rb b/app/services/story_groups_listing.rb index 75331bda..e3ad2021 100644 --- a/app/services/story_groups_listing.rb +++ b/app/services/story_groups_listing.rb @@ -1,19 +1,15 @@ # frozen_string_literal: true class StoryGroupsListing - include ApplicationHelper - - ROLE_LABELS = { own: 'Prowadzisz', sup: 'Wspierasz', lrn: 'Uczysz się' }.freeze - TABS = { - nil => ['Wszystkie', nil], - 'mine' => ['Moje', :own], - 'teacher' => ['Nauczam', :sup], - 'student' => ['Uczę się', :lrn], + nil => nil, + 'mine' => :own, + 'teacher' => :sup, + 'student' => :lrn, }.freeze Group = Data.define(:story_group, :role, :students_count, :meta) do - def role_label = ROLE_LABELS[role] + def role_label = I18n.t("story_groups.listing.roles.#{role}") end Tab = Data.define(:key, :label, :total) @@ -35,7 +31,7 @@ def load private def matches_filter?(group) - role = TABS[filter].last + role = TABS[filter] role.nil? || group.role == role end @@ -71,22 +67,22 @@ def role_for(story_group) def meta_for(story_group, role) case role when :own then owner_meta(story_group) - when :sup then "Właściciel: #{story_group.owner.full_name}" + when :sup then I18n.t('story_groups.listing.owner', name: story_group.owner.full_name) else student_meta(story_group) end end def owner_meta(story_group) count = purchase_counts.fetch(story_group.id, 0) - return 'Brak nowych zakupów' if count.zero? + return I18n.t('story_groups.listing.no_purchases') if count.zero? - "#{count} #{gh_plural(count, 'nowy zakup', 'nowe zakupy', 'nowych zakupów')}" + I18n.t('story_groups.listing.purchases', count: count) end def student_meta(story_group) balance = memberships[story_group.id]&.current_currency.to_i - "Masz #{balance} #{story_group.currency_name}" + I18n.t('story_groups.listing.balance', balance: balance, currency: story_group.currency_name) end def teacher_group_ids @@ -127,11 +123,11 @@ def build_tabs total = all_groups.size by_role = all_groups.group_by(&:role) - TABS.filter_map do |key, (label, role)| + TABS.filter_map do |key, role| count = role.nil? ? total : by_role.fetch(role, []).size next if role && !count.between?(1, total - 1) - Tab.new(key: key, label: label, total: count) + Tab.new(key: key, label: I18n.t("story_groups.listing.tabs.#{key || 'all'}"), total: count) end end end diff --git a/app/services/student_sheet.rb b/app/services/student_sheet.rb index a2244115..80604e59 100644 --- a/app/services/student_sheet.rb +++ b/app/services/student_sheet.rb @@ -1,17 +1,13 @@ # frozen_string_literal: true class StudentSheet - TABS = [ - ['badges', 'Odznaki'], - ['items', 'Przedmioty'], - ['hist', 'Historia waluty'], - ].freeze + TABS = %w[badges items hist].freeze DEFAULT_TAB = 'badges' class << self def tab_for(param) - TABS.map(&:first).include?(param.to_s) ? param.to_s : DEFAULT_TAB + TABS.include?(param.to_s) ? param.to_s : DEFAULT_TAB end end diff --git a/app/services/teacher_overview.rb b/app/services/teacher_overview.rb index 576c3cb4..b39e9305 100644 --- a/app/services/teacher_overview.rb +++ b/app/services/teacher_overview.rb @@ -48,13 +48,16 @@ def build_stat_tiles earned = window_scope.reward.sum(:amount) [ - StatTile.new(label: 'Studenci', value: students_count), - StatTile.new(label: 'Zakupy w tym tygodniu', value: bought, note: (spent.positive? ? "za #{spent}" : nil)), - StatTile.new(label: 'Nagrody w tym tygodniu', value: earned), - StatTile.new(label: 'Ranking', value: story_group.ranking_summary), + StatTile.new(label: stat(:students), value: students_count), + StatTile.new(label: stat(:purchases), value: bought, + note: (spent.positive? ? stat(:purchases_note, amount: spent) : nil),), + StatTile.new(label: stat(:rewards), value: earned), + StatTile.new(label: stat(:ranking), value: story_group.ranking_summary), ] end + def stat(key, **) = I18n.t("story_groups.overview.stats.#{key}", **) + def window_scope CurrencyTransaction.where(student_id: membership_ids) .where(created_at: WINDOW.ago..) @@ -83,9 +86,9 @@ def out_of_lives .map do |membership| AttentionItem.new( icon: 'fa-heart', tone: 'zero', - title: "#{membership.display_name} ma 0 żyć", - detail: 'Kupi tylko przedmioty dostępne przy 0 życiach.', - action_label: 'Otwórz', + title: I18n.t('story_groups.overview.zero_lives_title', name: membership.display_name), + detail: I18n.t('story_groups.overview.zero_lives_detail'), + action_label: I18n.t('story_groups.overview.open'), action_path: story_group_student_path(story_group, membership), ) end @@ -104,10 +107,10 @@ def ungraded_sheet AttentionItem.new( icon: 'fa-table', emphasis: true, - title: "#{activity_group.name} w trakcie", - detail: "Przyznano #{awarded} #{Plural.pick(awarded, 'nagrodę', 'nagrody', 'nagród')}, " \ - "#{pending} #{Plural.pick(pending, 'kolumna', 'kolumny', 'kolumn')} bez ocen.", - action_label: 'Oceń', + title: I18n.t('story_groups.overview.sheet_in_progress', name: activity_group.name), + detail: "#{I18n.t('story_groups.overview.sheet_awarded', count: awarded)}, " \ + "#{I18n.t('story_groups.overview.sheet_pending', count: pending)}", + action_label: I18n.t('story_groups.overview.grade'), action_path: edit_story_group_activity_group_students_activity_group_categories_path(story_group, activity_group,), ) diff --git a/app/values/currency_icons.rb b/app/values/currency_icons.rb index f8edb3d5..bb2bfb69 100644 --- a/app/values/currency_icons.rb +++ b/app/values/currency_icons.rb @@ -3,16 +3,7 @@ class CurrencyIcons < PresetSet KEYS = %w[carrot coin gem pearl bit].freeze - LABELS = { - 'bit' => 'Bit', - 'carrot' => 'Marchewka', - 'coin' => 'Moneta', - 'gem' => 'Klejnot', - 'pearl' => 'Perła', - }.freeze - class << self def dir = 'currency' - def labels = LABELS end end diff --git a/app/values/glyphs.rb b/app/values/glyphs.rb index f423cf2c..754e51db 100644 --- a/app/values/glyphs.rb +++ b/app/values/glyphs.rb @@ -13,36 +13,7 @@ class Glyphs < PresetSet shield hourglass chat retake percent paperCheck heartPlus note clock papers3 starPlus flask ].freeze - LABELS = { - 'bolt' => 'Błyskawica', - 'carrot' => 'Marchewka', - 'carrotStar' => 'Marchewka z gwiazdą', - 'chat' => 'Rozmowa', - 'chev1' => 'Belka', - 'chev2' => 'Podwójna belka', - 'clock' => 'Zegar', - 'compass' => 'Kompas', - 'crew' => 'Załoga', - 'crown' => 'Korona', - 'flask' => 'Kolba', - 'heartPlus' => 'Serce z plusem', - 'hourglass' => 'Klepsydra', - 'note' => 'Koperta', - 'paperCheck' => 'Kartka z ptaszkiem', - 'papers3' => 'Trzy kartki', - 'papers3shield' => 'Trzy kartki z tarczą', - 'percent' => 'Procent', - 'rabbit' => 'Królik', - 'retake' => 'Kartka ze strzałką', - 'rocket' => 'Rakieta', - 'shield' => 'Tarcza', - 'starPlus' => 'Gwiazda z gwiazdką', - 'starTrail' => 'Gwiazda ze smugą', - 'wrench' => 'Klucz', - }.freeze - class << self def dir = 'glyphs' - def labels = LABELS end end diff --git a/app/values/group_art.rb b/app/values/group_art.rb index 47720abc..d7ed6880 100644 --- a/app/values/group_art.rb +++ b/app/values/group_art.rb @@ -3,16 +3,7 @@ class GroupArt < PresetSet KEYS = %w[castle waves tables flow brackets].freeze - LABELS = { - 'brackets' => 'Kod', - 'castle' => 'Zamek', - 'flow' => 'Algorytm', - 'tables' => 'Tabele', - 'waves' => 'Morze', - }.freeze - class << self def dir = 'art' - def labels = LABELS end end diff --git a/app/values/item_card.rb b/app/values/item_card.rb index 9ade76ed..9f9243df 100644 --- a/app/values/item_card.rb +++ b/app/values/item_card.rb @@ -69,11 +69,13 @@ def badges_discount = badges.sum { |badge| badge.discount.to_i } def discount_label return if max_discount.zero? - "Zniżki do −#{max_discount}%" + I18n.t('items.labels.discount_up_to', discount: max_discount) end def lock_labels - requirements.map { |requirement| requirement.rank? ? "Od rangi #{requirement.name}" : requirement.name } + requirements.map do |requirement| + requirement.rank? ? I18n.t('items.labels.lock_rank', name: requirement.name) : requirement.name + end end def discounts? = max_discount.positive? diff --git a/app/values/item_offer.rb b/app/values/item_offer.rb index 103b7ca8..7a8fec7a 100644 --- a/app/values/item_offer.rb +++ b/app/values/item_offer.rb @@ -37,7 +37,7 @@ def discount_label return card.discount_label if discount.nil? return if discount.value.zero? - "−#{discount.value}% #{discount_source}" + I18n.t('items.labels.discount', discount: discount.value, source: discount_source) end def rank_progress @@ -53,10 +53,10 @@ def rank_progress private def discount_source - return 'za rangę i odznaki' if rank_discount? && badge_discount? - return 'za rangę' if rank_discount? + return I18n.t('items.labels.source_rank_and_badges') if rank_discount? && badge_discount? + return I18n.t('items.labels.source_rank') if rank_discount? - 'za odznaki' + I18n.t('items.labels.source_badges') end def rank_discount? = student&.rank&.discount.to_i.positive? diff --git a/app/values/ledger_entry.rb b/app/values/ledger_entry.rb index 0ab6616f..03f574d2 100644 --- a/app/values/ledger_entry.rb +++ b/app/values/ledger_entry.rb @@ -1,12 +1,6 @@ # frozen_string_literal: true class LedgerEntry - KIND_LABELS = { - 'reward' => 'Nagroda', - 'purchase' => 'Zakup', - 'adjustment' => 'Korekta', - }.freeze - KIND_TONES = { 'reward' => 'earn', 'purchase' => 'spend', @@ -28,7 +22,7 @@ def amount = transaction.amount.to_i def kind = transaction.kind.to_s def occurred_at = transaction.created_at - def label = KIND_LABELS.fetch(kind, kind) + def label = I18n.t("currency_transactions.kinds.#{kind}", default: kind) def tone = KIND_TONES.fetch(kind, 'corr') def reward? = kind == 'reward' diff --git a/app/values/navigation.rb b/app/values/navigation.rb index 3a8df82a..a48e1633 100644 --- a/app/values/navigation.rb +++ b/app/values/navigation.rb @@ -1,10 +1,11 @@ # frozen_string_literal: true +# Items carry a key so the tab bar can pick them without matching on wording. class Navigation include Rails.application.routes.url_helpers - Item = Data.define(:label, :path, :icon, :short, :match, :frame) do - def initialize(label:, path:, icon:, short: nil, match: nil, frame: nil) + Item = Data.define(:key, :label, :path, :icon, :short, :match, :frame) do + def initialize(key:, label:, path:, icon:, short: nil, match: nil, frame: nil) super end @@ -17,8 +18,8 @@ def current?(current_path) end end - TEACHER_TABS = ['Przegląd', 'Studenci', 'Arkusze ocen'].freeze - STUDENT_TABS = ['Przegląd', 'Sklep', 'Moje przedmioty', 'Ranking'].freeze + TEACHER_TABS = %i[overview students sheets].freeze + STUDENT_TABS = %i[overview shop my_items ranking].freeze def initialize(user) @user = user @@ -26,18 +27,15 @@ def initialize(user) def primary_items [ - Item.new(label: 'Start', path: home_path, icon: 'fa-house'), - (Item.new(label: 'Grupy', path: story_groups_path, icon: 'fa-book') unless @user.global_admin?), + item(:start, home_path, 'fa-house'), + (item(:groups, story_groups_path, 'fa-book') unless @user.global_admin?), organization_item, ].compact end def tab_items items = primary_items - if @user.teacher? - items += [Item.new(label: 'Powiadomienia', path: notifications_path, - icon: 'fa-bell', frame: 'panel',)] - end + items += [item(:notifications, notifications_path, 'fa-bell', frame: 'panel')] if @user.teacher? items end @@ -50,9 +48,9 @@ def group_items(chrome) def group_tab_items(chrome) wanted = chrome.student? ? STUDENT_TABS : TEACHER_TABS - by_label = group_items(chrome).index_by(&:label) + by_key = group_items(chrome).index_by(&:key) - wanted.filter_map { |label| by_label[label] } + wanted.filter_map { |key| by_key[key] } end private @@ -62,14 +60,14 @@ def teacher_items(chrome) [ overview(group), - section('Studenci', story_group_students_path(group), 'fa-graduation-cap'), - section('Arkusze ocen', story_group_activity_groups_path(group), 'fa-table', short: 'Arkusze'), - section('Przedmioty', story_group_items_path(group), 'fa-flask'), + section(:students, story_group_students_path(group), 'fa-graduation-cap'), + section(:sheets, story_group_activity_groups_path(group), 'fa-table', short: true), + section(:items, story_group_items_path(group), 'fa-flask'), *shared_items(group), ranking_item(chrome), - section('Zaproszenia', story_group_invites_path(group), 'fa-qrcode'), - section('Nauczyciele', story_group_teachers_path(group), 'fa-briefcase'), - section('Ustawienia grupy', edit_story_group_path(group), 'fa-gear'), + section(:invites, story_group_invites_path(group), 'fa-qrcode'), + section(:teachers, story_group_teachers_path(group), 'fa-briefcase'), + section(:settings, edit_story_group_path(group), 'fa-gear'), ] end @@ -79,47 +77,54 @@ def student_items(chrome) [ overview(group), - section('Sklep', story_group_shop_index_path(group), 'fa-sack-dollar'), - section('Moje przedmioty', + section(:shop, story_group_shop_index_path(group), 'fa-sack-dollar'), + section(:my_items, story_group_student_students_items_path(group, membership), - 'fa-flask', short: 'Przedmioty',), + 'fa-flask', short: true,), *shared_items(group), - section('Historia waluty', + section(:history, story_group_student_currency_transactions_path(group, membership), 'fa-clock-rotate-left',), ranking_item(chrome), - section('Ustawienia w grupie', edit_story_group_membership_path(group), 'fa-gear'), + section(:membership, edit_story_group_membership_path(group), 'fa-gear'), ] end def shared_items(group) [ - section('Rangi', story_group_ranks_path(group), 'fa-angles-up'), - section('Odznaki', story_group_badges_path(group), 'fa-award'), + section(:ranks, story_group_ranks_path(group), 'fa-angles-up'), + section(:badges, story_group_badges_path(group), 'fa-award'), ] end def ranking_item(chrome) return unless chrome.ranking? - section('Ranking', story_group_ranking_path(chrome.story_group), 'fa-ranking-star') + section(:ranking, story_group_ranking_path(chrome.story_group), 'fa-ranking-star') end def organization_item if @user.global_admin? - Item.new(label: 'Organizacje', path: organizations_path, icon: 'fa-briefcase', match: organizations_path) + section(:organizations, organizations_path, 'fa-briefcase') elsif @user.organization_admin? && @user.organization path = organization_path(@user.organization) - Item.new(label: @user.organization.name, path: path, icon: 'fa-briefcase', - short: 'Organizacja', match: path,) + Item.new(key: :organization, label: @user.organization.name, path: path, icon: 'fa-briefcase', + short: label('short.organization'), match: path,) end end def overview(group) - Item.new(label: 'Przegląd', path: story_group_path(group), icon: 'fa-table-columns') + item(:overview, story_group_path(group), 'fa-table-columns') + end + + def item(key, path, icon, frame: nil) + Item.new(key: key, label: label(key), path: path, icon: icon, frame: frame) end - def section(label, path, icon, short: nil) - Item.new(label: label, path: path, icon: icon, short: short, match: path) + def section(key, path, icon, short: false) + Item.new(key: key, label: label(key), path: path, icon: icon, match: path, + short: (label("short.#{key}") if short),) end + + def label(key) = I18n.t("layouts.navigation.#{key}") end diff --git a/app/values/plural.rb b/app/values/plural.rb deleted file mode 100644 index fe83e65f..00000000 --- a/app/values/plural.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -module Plural - extend self - - def pick(count, one, few, many) - n = count.abs - last_two = n % 100 - last_one = n % 10 - - return one if n == 1 - return few if last_one.between?(2, 4) && !last_two.between?(12, 14) - - many - end -end diff --git a/app/values/preset_set.rb b/app/values/preset_set.rb index 2fdddcb7..34032169 100644 --- a/app/values/preset_set.rb +++ b/app/values/preset_set.rb @@ -3,10 +3,9 @@ class PresetSet class << self def dir = raise(NotImplementedError, "#{name} must define .dir") - def labels = {}.freeze def label_for(key) - labels.fetch(key.to_s, key.to_s) + I18n.t("application.presets.#{dir}.#{key}", default: key.to_s) end def all diff --git a/app/values/requirement_phrasing.rb b/app/values/requirement_phrasing.rb index 1a74b783..746e2971 100644 --- a/app/values/requirement_phrasing.rb +++ b/app/values/requirement_phrasing.rb @@ -11,17 +11,19 @@ def lives? = kind == :lives def seal_label_for(requirement) return if requirement.nil? - return 'Niedostępne przy 0 życiach' if requirement.lives? + return I18n.t('items.labels.seal_lives') if requirement.lives? - requirement.rank? ? "Od rangi #{requirement.name}" : "Za odznakę #{requirement.name}" + key = requirement.rank? ? 'items.labels.seal_rank' : 'items.labels.seal_badge' + I18n.t(key, name: requirement.name) end + # [lead, name] pairs; the view sets the name in bold after the lead. def requirement_lines_for(requirements) requirements.map do |requirement| case requirement.kind - when :rank then ['Wymaga rangi ', requirement.name] - when :badge then ['Wymaga odznaki ', requirement.name] - else ['Masz 0 żyć. Najpierw odzyskaj życie.', nil] + when :rank then [I18n.t('items.labels.requires_rank'), requirement.name] + when :badge then [I18n.t('items.labels.requires_badge'), requirement.name] + else [I18n.t('items.labels.requires_life'), nil] end end end diff --git a/app/values/starter_pack.rb b/app/values/starter_pack.rb index a49f67f2..bf55682e 100644 --- a/app/values/starter_pack.rb +++ b/app/values/starter_pack.rb @@ -1,6 +1,12 @@ # frozen_string_literal: true +# The quick-start set. Numbers and icons live here; every name and description +# comes from story_groups.starter_pack in config/locales/views/story_groups.*.yml, +# in the language of whoever creates the group — the builder writes them to the +# database as plain text, so a group keeps the language it was created in. class StarterPack + I18N_SCOPE = 'story_groups.starter_pack' + CLASS_MAX = 11 KEYS = %w[neutral fantasy scifi].freeze @@ -8,20 +14,6 @@ class StarterPack CLASSES_RANGE = (4..30) DEFAULT_CLASSES = 12 - LABELS = { - 'neutral' => 'Neutralny', - 'fantasy' => 'Fantasy', - 'scifi' => 'Sci-fi', - }.freeze - - DESCRIPTIONS = { - 'neutral' => 'Bez fabularnego klimatu. Pasuje do każdego przedmiotu.', - 'fantasy' => 'Zamki, zakony i rycerskie tytuły.', - 'scifi' => 'Statki, załogi i kosmiczne misje.', - }.freeze - - TEMPLATE_NAME = 'Zajęcia' - Rank = Data.define(:index, :name, :threshold, :discount, :icon_glyph) Badge = Data.define(:index, :name, :didactic_description, :story_description, :discount, :icon_glyph,) @@ -29,202 +21,60 @@ class StarterPack :unlock_rank, :min_rank_for_discount, :discount_badges, :can_buy_at_0_lives,) Category = Data.define(:index, :didactic_description, :story_description, :reward) - CATEGORIES = [ - ['Obecność', 1], - ['Punktualność', 1], - ['Wejściówka zaliczona', 1], - ['Wejściówka ≥ średnia grupy', 2], - ['Zgłoszenie do zadania', 2], - ['Zadanie zrobione poprawnie', 1], - ['Pomoc innym', 2], - ['Ciekawa uwaga', 1], - ].freeze + # Reward per category, in the order of starter_pack.categories. + CATEGORY_REWARDS = [1, 1, 1, 2, 2, 1, 2, 1].freeze RANK_FRACTIONS = [0.0, 0.15, 0.30, 0.50, 0.75].freeze RANK_DISCOUNTS = [0, 3, 5, 10, 15].freeze - BADGE_RULES = [ - 'Pierwsza wejściówka bez błędów', - 'Obecność na wszystkich zajęciach w miesiącu', - 'Co najmniej 3 razy pomógł innym', - 'Co najmniej 3 ciekawe uwagi', - '3 wejściówki z rzędu ≥ średniej grupy', - 'Wyjaśnienie trudnego zadania całej grupie', - ].freeze - BADGE_DISCOUNTS = [2, 5, 5, 3, 5, 7].freeze + # In the order of starter_pack.items. ITEMS = [ - { - name: '+5 minut do wejściówki', - x: 1.0, - glyph: 'clock', - unlock: nil, - discount_rank: 2, - discount_badges: [], - didactic: 'Dodaje 5 minut do czasu na najbliższą wejściówkę.', - }, - { - name: 'Konsultacja', - x: 1.5, - glyph: 'chat', - unlock: nil, - discount_rank: 2, - discount_badges: [], - didactic: 'Kwadrans konsultacji z prowadzącym poza zajęciami.', - }, - { - name: 'Poprawa wejściówki', - x: 1.0, - glyph: 'retake', - unlock: 1, - discount_rank: 3, - discount_badges: [], - didactic: 'Pozwala napisać jedną wejściówkę jeszcze raz.', - }, - { - name: 'Bezpieczna poprawa', - x: 2.0, - glyph: 'shield', - unlock: 2, - discount_rank: 3, - discount_badges: [1, 3], - didactic: 'Poprawa wejściówki bez ryzyka — liczy się lepszy z dwóch wyników.', - }, - { - name: 'Usprawiedliwienie', - x: 2.5, - glyph: 'note', - unlock: 2, - discount_rank: 3, - discount_badges: [], - didactic: 'Usprawiedliwia jedną nieobecność, bez utraty życia.', - }, - { - name: '1up: odzyskanie życia', - m: 0.25, - glyph: 'heartPlus', - unlock: nil, - discount_rank: 4, - discount_badges: [3, 5, 2], - at_zero_lives: true, - didactic: 'Przywraca jedno utracone życie.', - }, - { - name: '+0.5 oceny końcowej', - m: 0.60, - glyph: 'percent', - unlock: 3, - discount_rank: 4, - discount_badges: [5, 1], - didactic: 'Podnosi ocenę końcową o pół stopnia.', - }, + { x: 1.0, glyph: 'clock', unlock: nil, discount_rank: 2, discount_badges: [] }, + { x: 1.5, glyph: 'chat', unlock: nil, discount_rank: 2, discount_badges: [] }, + { x: 1.0, glyph: 'retake', unlock: 1, discount_rank: 3, discount_badges: [] }, + { x: 2.0, glyph: 'shield', unlock: 2, discount_rank: 3, discount_badges: [1, 3] }, + { x: 2.5, glyph: 'note', unlock: 2, discount_rank: 3, discount_badges: [] }, + { m: 0.25, glyph: 'heartPlus', unlock: nil, discount_rank: 4, discount_badges: [3, 5, 2], at_zero_lives: true }, + { m: 0.60, glyph: 'percent', unlock: 3, discount_rank: 4, discount_badges: [5, 1] }, ].freeze - RANK_NAMES = { - 'neutral' => %w[Nowicjusz Uczeń Adept Ekspert Mistrz], - 'fantasy' => %w[Giermek Rycerz Kasztelan Hetman Król], - 'scifi' => %w[Kadet Pilot Nawigator Komandor Admirał], - }.freeze - RANK_GLYPHS = { 'neutral' => %w[chev1 chev2 shield starPlus crown], 'fantasy' => %w[chev1 chev2 shield crown carrotStar], 'scifi' => %w[chev1 chev2 rocket compass crown], }.freeze - BADGE_NAMES = { - 'neutral' => ['Bez skazy', - 'Stała obecność', - 'Pomocna dłoń', - 'Iskra ciekawości', - 'Seria sukcesów', - 'Głos grupy',], - 'fantasy' => ['Czysta klinga', - 'Wierna straż', - 'Tarcza drużyny', - 'Księga mądrości', - 'Pasmo zwycięstw', - 'Bard drużyny',], - 'scifi' => ['Czysty lot', - 'Zawsze na pokładzie', - 'Mechanik załogi', - 'Sygnał z kosmosu', - 'Seria misji', - 'Głos floty',], - }.freeze - BADGE_GLYPHS = { 'neutral' => %w[starPlus shield heartPlus bolt starTrail crew], 'fantasy' => %w[starPlus shield heartPlus bolt starTrail crown], 'scifi' => %w[starPlus rocket wrench compass starTrail crew], }.freeze - BADGE_STORIES = { - 'neutral' => [nil] * 6, - 'fantasy' => ['Stal bez jednej rysy — pierwszy pojedynek wygrany bez draśnięcia.', - 'Stałeś na posterunku każdej nocy tego miesiąca.', - 'Trzykrotnie zasłoniłeś towarzysza własną tarczą.', - 'Trzy razy dopisałeś coś, czego w księgach jeszcze nie było.', - 'Trzy potyczki z rzędu rozstrzygnięte na twoją korzyść.', - 'Wytłumaczyłeś całej drużynie to, czego nikt inny nie potrafił nazwać.',], - 'scifi' => ['Pierwszy lot bez jednego odczytu w czerwieni.', - 'Ani jednej zmiany opuszczonej w tym cyklu.', - 'Trzy razy postawiłeś na nogi cudzy sprzęt.', - 'Trzy razy wychwyciłeś sygnał, który inni przegapili.', - 'Trzy misje z rzędu zaliczone powyżej normy floty.', - 'Twoje wyjaśnienie poszło na cały kanał i wszyscy zrozumieli.',], - }.freeze + class << self + def include?(key) = KEYS.include?(key.to_s) - ITEM_STORIES = { - 'neutral' => [nil] * 7, - 'fantasy' => ['Klepsydra sypie piaskiem wolniej, niż powinna.', - 'Audiencja u mistrza zakonu.', - 'Drugie podejście do pojedynku.', - 'Pojedynek na tępe klingi — przegrana nic nie kosztuje.', - 'List żelazny tłumaczący twoją nieobecność.', - 'Eliksir przywracający życie.', - 'Pieczęć króla na twoim świadectwie.',], - 'scifi' => ['Chwilowe spowolnienie zegara pokładowego.', - 'Połączenie z komandorem poza godzinami służby.', - 'Drugie podejście do symulacji.', - 'Lot treningowy — awaria nic nie kosztuje.', - 'Wpis do dziennika usprawiedliwiający nieobecność.', - 'Regeneracja w kapsule medycznej.', - 'Awans wpisany do akt floty.',], - }.freeze + def label_for(key) = text("packs.#{key}.label", default: key.to_s) - CATEGORY_STORIES = { - 'neutral' => [nil] * 8, - 'fantasy' => ['Stawiłeś się na zbiórce.', - 'Na miejscu, zanim zabrzmiał róg.', - 'Próba sprawności zaliczona.', - 'Wynik powyżej średniej drużyny.', - 'Zgłosiłeś się na ochotnika.', - 'Zadanie wykonane bez zarzutu.', - 'Podałeś rękę towarzyszowi.', - 'Rzuciłeś światło na rzecz, której nikt nie dostrzegł.',], - 'scifi' => ['Zameldowałeś się na pokładzie.', - 'Na stanowisku przed odliczaniem.', - 'Test kwalifikacyjny zaliczony.', - 'Wynik powyżej średniej załogi.', - 'Zgłosiłeś się do zadania.', - 'Zadanie wykonane zgodnie z procedurą.', - 'Wsparłeś innego członka załogi.', - 'Zauważyłeś coś, co umknęło czujnikom.',], - }.freeze + def description_for(key) = text("packs.#{key}.description", default: '') - class << self - def include?(key) = KEYS.include?(key.to_s) + def rank_names(key) = list("packs.#{key}.ranks") - def label_for(key) = LABELS.fetch(key.to_s, key.to_s) + def template_name = text('template_name') - def description_for(key) = DESCRIPTIONS.fetch(key.to_s, '') + def category_names = text('categories') def for(pack:, classes:) new(pack: include?(pack) ? pack.to_s : KEYS.first, classes: classes.to_i.clamp(CLASSES_RANGE.min, CLASSES_RANGE.max),) end + + def text(key, **) = I18n.t("#{I18N_SCOPE}.#{key}", **) + + # An array key, or [] when absent. Not `default: []` — I18n reads an Array + # default as fallback keys, and an empty list of them is still missing. + def list(key) = I18n.exists?("#{I18N_SCOPE}.#{key}") ? text(key) : [] end def initialize(pack:, classes:) @@ -241,7 +91,7 @@ def total_earnable = classes * CLASS_MAX def ranks @ranks ||= begin - names = RANK_NAMES[pack] + names = self.class.rank_names(pack) glyphs = RANK_GLYPHS[pack] thresholds = RANK_FRACTIONS.each_with_object([]) do |fraction, kept| @@ -258,13 +108,14 @@ def ranks def badges @badges ||= begin - names = BADGE_NAMES[pack] + names = pack_text(:badges) + rules = self.class.text('badge_rules') glyphs = BADGE_GLYPHS[pack] - stories = BADGE_STORIES[pack] + stories = pack_text(:badge_stories) names.each_with_index.map do |name, index| Badge.new(index: index, name: name, - didactic_description: BADGE_RULES[index], + didactic_description: rules[index], story_description: stories[index], discount: BADGE_DISCOUNTS[index], icon_glyph: glyphs[index],) end @@ -273,11 +124,12 @@ def badges def items @items ||= begin - stories = ITEM_STORIES[pack] + texts = self.class.text('items') + stories = pack_text(:item_stories) ITEMS.each_with_index.map do |spec, index| - Item.new(index: index, name: spec[:name], price: price_for(spec), - didactic_description: spec[:didactic], + Item.new(index: index, name: texts[index][:name], price: price_for(spec), + didactic_description: texts[index][:didactic], story_description: stories[index], icon_glyph: spec[:glyph], unlock_rank: spec[:unlock], @@ -290,17 +142,20 @@ def items def categories @categories ||= begin - stories = CATEGORY_STORIES[pack] + stories = pack_text(:category_stories) - CATEGORIES.each_with_index.map do |(label, reward), index| + self.class.category_names.each_with_index.map do |label, index| Category.new(index: index, didactic_description: label, - story_description: stories[index], reward: reward,) + story_description: stories[index], reward: CATEGORY_REWARDS[index],) end end end private + # A pack without story lines (neutral) simply has no such key. + def pack_text(key) = self.class.list("packs.#{pack}.#{key}") + def price_for(spec) raw = spec[:x] ? spec[:x] * CLASS_MAX : spec[:m] * total_earnable diff --git a/app/views/account_setups/edit.html.haml b/app/views/account_setups/edit.html.haml index b2913c78..2525ebad 100644 --- a/app/views/account_setups/edit.html.haml +++ b/app/views/account_setups/edit.html.haml @@ -1,20 +1,20 @@ -- content_for :title, 'Konfiguracja konta' +- content_for :title, t('.page_title') -%h1.gh-h1 Uzupełnij swoje dane -%p.gh-lead To jednorazowy krok. Imię i nazwisko zobaczą prowadzący w Twoich grupach. +%h1.gh-h1= t('.title') +%p.gh-lead= t('.lead') = form_with model: @current_user, url: account_setup_path, method: :patch do |f| .gh-form-field - = f.label :email, 'Adres e-mail' + = f.label :email, t('.email') .gh-text-input = f.email_field :email, disabled: true = render 'organizations/field', form: f, record: @current_user, attribute: :full_name, - label: 'Imię i nazwisko', + label: t('.full_name'), options: { maxlength: 80, autofocus: true, required: true, - autocomplete: 'name', placeholder: 'Jan Nowak' } + autocomplete: 'name', placeholder: t('.full_name_placeholder') } .gh-button-stack - %button.gh-btn{ type: 'submit' } Zapisz i przejdź dalej + %button.gh-btn{ type: 'submit' }= t('.submit') -= button_to 'Wyloguj', logout_path, method: :delete, class: 'gh-link-button' += button_to t('.logout'), logout_path, method: :delete, class: 'gh-link-button' diff --git a/app/views/activity_group_templates/_confirm_destroy.html.haml b/app/views/activity_group_templates/_confirm_destroy.html.haml index 2c6ea4e9..b4b7040c 100644 --- a/app/views/activity_group_templates/_confirm_destroy.html.haml +++ b/app/views/activity_group_templates/_confirm_destroy.html.haml @@ -1,22 +1,19 @@ -%h2#gh-del-title.gh-h2= "Usunąć szablon „#{template.base_name}”?" -%p Szablon zniknie z listy, a utworzone z niego arkusze zostają. +%h2#gh-del-title.gh-h2= t('.title', name: template.base_name) +%p= t('.body') - if sheet_names.any? %p.gh-info-banner %i.fa-solid.fa-table{ 'aria-hidden': 'true' } - %span - = "#{sheet_names.size} #{gh_plural(sheet_names.size, 'arkusz', 'arkusze', 'arkuszy')}" - = "#{gh_plural(sheet_names.size, 'zostaje', 'zostają', 'zostaje')} bez zmian:" - = gh_and_list(sheet_names) + %span= t('.sheets', count: sheet_names.size, names: gh_and_list(sheet_names)) .gh-dialog-button-row - if @in_modal %button.gh-btn.gh-btn--secondary{ type: 'button', data: { action: 'dialog#close' }, autofocus: true } - Anuluj + = t('shared.actions.cancel') - else - = link_to 'Anuluj', edit_story_group_activity_group_template_path(@story_group, template), + = link_to t('shared.actions.cancel'), edit_story_group_activity_group_template_path(@story_group, template), class: 'gh-btn gh-btn--secondary' - = button_to 'Usuń szablon', story_group_activity_group_template_path(@story_group, template), + = button_to t('.remove'), story_group_activity_group_template_path(@story_group, template), method: :delete, class: 'gh-btn', form: { data: { turbo_frame: (@in_modal ? 'modal' : nil) } } diff --git a/app/views/activity_group_templates/confirm_destroy.html.haml b/app/views/activity_group_templates/confirm_destroy.html.haml index f278387b..03318dde 100644 --- a/app/views/activity_group_templates/confirm_destroy.html.haml +++ b/app/views/activity_group_templates/confirm_destroy.html.haml @@ -1,4 +1,4 @@ -- content_for :title, "Usuń szablon #{@activity_group_template.base_name}" +- content_for :title, t('.page_title', name: @activity_group_template.base_name) - if @in_modal = turbo_frame_tag 'modal' do diff --git a/app/views/activity_group_templates/edit.html.haml b/app/views/activity_group_templates/edit.html.haml index 7c45851d..5a3fcb1c 100644 --- a/app/views/activity_group_templates/edit.html.haml +++ b/app/views/activity_group_templates/edit.html.haml @@ -1,4 +1,4 @@ -- content_for :title, "Edytuj szablon #{@activity_group_template.base_name}" +- content_for :title, t('.page_title', name: @activity_group_template.base_name) = render 'shared/sheet_form', record: @activity_group_template, variant: :template_edit, sheet_names: @sheet_names diff --git a/app/views/activity_group_templates/new.html.haml b/app/views/activity_group_templates/new.html.haml index 9830432a..c0b82cbc 100644 --- a/app/views/activity_group_templates/new.html.haml +++ b/app/views/activity_group_templates/new.html.haml @@ -1,3 +1,3 @@ -- content_for :title, 'Nowy szablon arkusza' +- content_for :title, t('.page_title') = render 'shared/sheet_form', record: @activity_group_template, variant: :template_new diff --git a/app/views/activity_groups/_confirm_destroy.html.haml b/app/views/activity_groups/_confirm_destroy.html.haml index d43c2d11..4ea17021 100644 --- a/app/views/activity_groups/_confirm_destroy.html.haml +++ b/app/views/activity_groups/_confirm_destroy.html.haml @@ -1,21 +1,21 @@ -%h2#gh-del-title.gh-h2= "Usunąć arkusz „#{activity_group.name}”?" -%p Arkusz zniknie z listy, a przyznane nagrody zostają u studentów i w ich historii. +%h2#gh-del-title.gh-h2= t('.title', name: activity_group.name) +%p= t('.body') - if awards_count.positive? %p.gh-info-banner %i.fa-solid.fa-lock{ 'aria-hidden': 'true' } %span - = "Przyznano z niego #{awards_count} #{gh_plural(awards_count, 'nagrodę', 'nagrody', 'nagród')}." - Usunięcie arkusza niczego nie cofa. + = t('.awarded', count: awards_count) + = t('.nothing_undone') .gh-dialog-button-row - if @in_modal %button.gh-btn.gh-btn--secondary{ type: 'button', data: { action: 'dialog#close' }, autofocus: true } - Anuluj + = t('shared.actions.cancel') - else - = link_to 'Anuluj', edit_story_group_activity_group_path(@story_group, activity_group), + = link_to t('shared.actions.cancel'), edit_story_group_activity_group_path(@story_group, activity_group), class: 'gh-btn gh-btn--secondary' - = button_to 'Usuń arkusz', story_group_activity_group_path(@story_group, activity_group), + = button_to t('.remove'), story_group_activity_group_path(@story_group, activity_group), method: :delete, class: 'gh-btn', form: { data: { turbo_frame: (@in_modal ? 'modal' : nil) } } diff --git a/app/views/activity_groups/_new_form.html.haml b/app/views/activity_groups/_new_form.html.haml index 6d542d25..74f8a699 100644 --- a/app/views/activity_groups/_new_form.html.haml +++ b/app/views/activity_groups/_new_form.html.haml @@ -1,53 +1,53 @@ - count = ActivityGroupsController::BULK_RANGE.min + 1 - columns = template.categories.size -%h2#gh-new-sheet-title.gh-h2= "Nowy arkusz z szablonu #{template.base_name}" +%h2#gh-new-sheet-title.gh-h2= t('.title', name: template.base_name) %p - = "Każdy arkusz dostanie #{columns} #{gh_plural(columns, 'kategorię', 'kategorie', 'kategorii')} z szablonu." - Kolumny zmienisz później w ustawieniach arkusza. + = t('.columns', count: columns) + = t('.columns_note') - form_data = { controller: 'sheet-create', - sheet_create_sheets_value: %w[arkusz arkusze arkuszy].to_json, + sheet_create_i18n_value: t('.js').to_json, turbo_frame: (@in_modal ? 'modal' : nil) } = form_with url: story_group_activity_groups_path(@story_group), method: :post, data: form_data do |f| = hidden_field_tag 'activity_group[activity_group_template_id]', template.id - .gh-segmented-toggle-3.gh-octagon-clip{ role: 'radiogroup', 'aria-label': 'Ile arkuszy' } - - [%w[one Jeden\ arkusz], %w[many Kilka\ naraz]].each do |value, label| + .gh-segmented-toggle-3.gh-octagon-clip{ role: 'radiogroup', 'aria-label': t('.how_many') } + - [['one', t('.one')], ['many', t('.many')]].each do |value, label| %label.gh-segmented-toggle-3-option = radio_button_tag 'activity_group[mode]', value, value == 'one', data: { sheet_create_target: 'mode', action: 'sheet-create#refresh' } %span= label .gh-form-field{ data: { sheet_create_target: 'onePane' } } - = label_tag 'gh-sheet-name', 'Nazwa' + = label_tag 'gh-sheet-name', t('shared.actions.name') .gh-text-input = text_field_tag 'activity_group[name]', suggested_names.first, id: 'gh-sheet-name', autocomplete: 'off', autofocus: true - %span.gh-field-hint= "Możesz dopisać datę, np. #{suggested_names.first} (14.10)." + %span.gh-field-hint= t('.name_hint', example: suggested_names.first) .gh-form-field{ data: { sheet_create_target: 'manyPane' } } - %p#gh-sheet-count-label.gh-form-label Ile arkuszy utworzyć + %p#gh-sheet-count-label.gh-form-label= t('.count') .gh-number-stepper{ role: 'group', 'aria-labelledby': 'gh-sheet-count-label' } - %button.gh-number-stepper-button.gh-octagon-clip{ type: 'button', 'aria-label': 'Mniej', + %button.gh-number-stepper-button.gh-octagon-clip{ type: 'button', 'aria-label': t('shared.actions.fewer'), data: { sheet_create_target: 'down', action: 'sheet-create#down' } } %i.fa-solid.fa-minus{ 'aria-hidden': 'true' } %output{ 'aria-live': 'polite', data: { sheet_create_target: 'output' } }= count - %button.gh-number-stepper-button.gh-octagon-clip{ type: 'button', 'aria-label': 'Więcej', + %button.gh-number-stepper-button.gh-octagon-clip{ type: 'button', 'aria-label': t('shared.actions.more'), data: { sheet_create_target: 'up', action: 'sheet-create#up' } } %i.fa-solid.fa-plus{ 'aria-hidden': 'true' } = hidden_field_tag :count, count, data: { sheet_create_target: 'count' } - .gh-sample-name-chips{ 'aria-label': 'Nazwy' } + .gh-sample-name-chips{ 'aria-label': t('.names') } - suggested_names.each_with_index do |name, index| %span.gh-octagon-clip{ hidden: index >= count, data: { sheet_create_target: 'name' } }= name .gh-dialog-button-row - if @in_modal - %button.gh-btn.gh-btn--secondary{ type: 'button', data: { action: 'dialog#close' } } Anuluj + %button.gh-btn.gh-btn--secondary{ type: 'button', data: { action: 'dialog#close' } }= t('shared.actions.cancel') - else - = link_to 'Anuluj', story_group_activity_groups_path(@story_group), class: 'gh-btn gh-btn--secondary' + = link_to t('shared.actions.cancel'), story_group_activity_groups_path(@story_group), class: 'gh-btn gh-btn--secondary' %button.gh-btn{ type: 'submit', data: { sheet_create_target: 'submit' } } %i.fa-solid.fa-plus{ 'aria-hidden': 'true' } - %span{ data: { sheet_create_target: 'submitLabel' } } Utwórz arkusz + %span{ data: { sheet_create_target: 'submitLabel' } }= t('.js.create_one') diff --git a/app/views/activity_groups/_sheet_row.html.haml b/app/views/activity_groups/_sheet_row.html.haml index a3d504a3..6d0aae57 100644 --- a/app/views/activity_groups/_sheet_row.html.haml +++ b/app/views/activity_groups/_sheet_row.html.haml @@ -4,18 +4,18 @@ %span.gh-sheet-row-name %b= record.name - if sheet.columns_modified? - %span.gh-tag.gh-tag--modified Zmienione kolumny + %span.gh-tag.gh-tag--modified= t('.modified') %span.gh-sheet-row-stats - if sheet.graded? - = "Przyznano #{sheet.awards_count} #{gh_plural(sheet.awards_count, 'nagrodę', 'nagrody', 'nagród')}" + = t('.awarded', count: sheet.awards_count) - else - Jeszcze nic nie przyznano + = t('.nothing') %span.gh-sheet-row-actions - = link_to 'Oceń', + = link_to t('.grade'), edit_story_group_activity_group_students_activity_group_categories_path(@story_group, record), class: "gh-btn gh-btn--small#{' gh-btn--secondary' unless primary}" = link_to edit_story_group_activity_group_path(@story_group, record), - class: 'gh-icon-button', title: 'Ustawienia arkusza', 'aria-label': "Ustawienia: #{record.name}" do + class: 'gh-icon-button', title: t('.settings'), 'aria-label': t('.settings_label', name: record.name) do %i.fa-solid.fa-gear{ 'aria-hidden': 'true' } diff --git a/app/views/activity_groups/_template_panel.html.haml b/app/views/activity_groups/_template_panel.html.haml index b4b628ae..467e490b 100644 --- a/app/views/activity_groups/_template_panel.html.haml +++ b/app/views/activity_groups/_template_panel.html.haml @@ -1,21 +1,19 @@ - record = template.record -- count = template.category_count %section.gh-panel.gh-template-panel{ 'aria-labelledby': "gh-tpl-#{record.id}" } .gh-template-panel-header %div %h2.gh-h2{ id: "gh-tpl-#{record.id}" }= record.base_name %p.gh-template-panel-meta - = "Szablon: #{count} #{gh_plural(count, 'kategoria', 'kategorie', 'kategorii')}," - = "do #{template.max_reward} #{@story_group.currency_name} na studenta za arkusz." + = t('.meta', count: template.category_count, max: template.max_reward, currency: @story_group.currency_name) %span.gh-template-panel-spacer - = link_to 'Edytuj szablon', edit_story_group_activity_group_template_path(@story_group, record), + = link_to t('.edit'), edit_story_group_activity_group_template_path(@story_group, record), class: 'gh-btn gh-btn--secondary gh-btn--small' = link_to new_story_group_activity_group_path(@story_group, template_id: record.id), class: 'gh-btn gh-btn--small', data: { turbo_frame: 'modal' } do %i.fa-solid.fa-plus{ 'aria-hidden': 'true' } - Utwórz arkusz + = t('.create_sheet') - if template.any_sheets? %ul.gh-sheet-list @@ -23,4 +21,4 @@ = render 'sheet_row', sheet: sheet, primary: index.zero?, fresh: fresh_sheet_ids.include?(sheet.record.id) - else - %p.gh-explainer-note Z tego szablonu nie utworzono jeszcze arkuszy. + %p.gh-explainer-note= t('.no_sheets') diff --git a/app/views/activity_groups/confirm_destroy.html.haml b/app/views/activity_groups/confirm_destroy.html.haml index 59275650..90461e93 100644 --- a/app/views/activity_groups/confirm_destroy.html.haml +++ b/app/views/activity_groups/confirm_destroy.html.haml @@ -1,4 +1,4 @@ -- content_for :title, "Usuń arkusz #{@activity_group.name}" +- content_for :title, t('.page_title', name: @activity_group.name) - if @in_modal = turbo_frame_tag 'modal' do diff --git a/app/views/activity_groups/edit.html.haml b/app/views/activity_groups/edit.html.haml index 8f984ab0..dd5877a0 100644 --- a/app/views/activity_groups/edit.html.haml +++ b/app/views/activity_groups/edit.html.haml @@ -1,4 +1,4 @@ -- content_for :title, "Ustawienia arkusza #{@activity_group.name}" +- content_for :title, t('.page_title', name: @activity_group.name) = render 'shared/sheet_form', record: @activity_group, variant: :sheet, awards_count: @activity_group.activity_group_categories.sum(&:awards_count) diff --git a/app/views/activity_groups/index.html.haml b/app/views/activity_groups/index.html.haml index 253543ee..653fc315 100644 --- a/app/views/activity_groups/index.html.haml +++ b/app/views/activity_groups/index.html.haml @@ -1,21 +1,19 @@ -- content_for :title, 'Arkusze ocen' +- content_for :title, t('.title') %section.gh-panel.gh-page-header .gh-page-header-text - %h1.gh-h1 Arkusze ocen - %p.gh-lead Arkusze pogrupowane według szablonów. Najnowsze są na górze. + %h1.gh-h1= t('.title') + %p.gh-lead= t('.lead') .gh-button-row = link_to new_story_group_activity_group_template_path(@story_group), class: 'gh-btn gh-btn--secondary' do %i.fa-solid.fa-plus{ 'aria-hidden': 'true' } - Nowy szablon + = t('.new_template') - if @index.any? - @index.templates.each do |template| = render 'template_panel', template: template, fresh_sheet_ids: @fresh_sheet_ids - else %section.gh-panel.gh-empty-state - %h2.gh-h2 Nie masz jeszcze żadnego szablonu - %p.gh-lead - Szablon to zestaw kategorii, za które nagradzasz studentów. Z jednego - szablonu tworzysz potem kolejne arkusze — po jednym na zajęcia. + %h2.gh-h2= t('.empty_title') + %p.gh-lead= t('.empty_body') diff --git a/app/views/activity_groups/new.html.haml b/app/views/activity_groups/new.html.haml index a32b2e28..8dbb26d1 100644 --- a/app/views/activity_groups/new.html.haml +++ b/app/views/activity_groups/new.html.haml @@ -1,4 +1,4 @@ -- content_for :title, "Nowy arkusz z szablonu #{@template.base_name}" +- content_for :title, t('.page_title', name: @template.base_name) - if @in_modal = turbo_frame_tag 'modal' do diff --git a/app/views/auth/passwordless/inbox.html.haml b/app/views/auth/passwordless/inbox.html.haml index 8d2c9625..8478311f 100644 --- a/app/views/auth/passwordless/inbox.html.haml +++ b/app/views/auth/passwordless/inbox.html.haml @@ -1,16 +1,16 @@ -- content_for :title, 'Sprawdź skrzynkę' -- waiting_label = 'Wyślij ponownie za %{time}' -- idle_label = 'Wyślij link ponownie' +- content_for :title, t('.title') +- waiting_label = t('.waiting', time: '%{time}') +- idle_label = t('.resend') .text-center .gh-icon-circle-lg.gh-octagon-clip.mx-auto %i.fa-solid.fa-envelope - %h1.gh-h1 Sprawdź skrzynkę - %p.gh-lead.mx-auto Wysłaliśmy link na adres + %h1.gh-h1= t('.title') + %p.gh-lead.mx-auto= t('.sent_to') %p.gh-email-display= @email %p.gh-small.mt-4 - = "Link działa przez #{gh_minutes(LoginToken::EXPIRES_IN.in_minutes.to_i)}." - Jeśli go nie widzisz, zajrzyj do folderu Spam. + = t('.valid_for', minutes: gh_minutes(LoginToken::EXPIRES_IN.in_minutes.to_i)) + = t('.spam') .gh-button-stack = button_to auth_passwordless_path, @@ -27,4 +27,4 @@ - else = idle_label - = link_to 'Zmień adres e-mail', new_auth_passwordless_path, class: 'gh-link-button' + = link_to t('.change_email'), new_auth_passwordless_path, class: 'gh-link-button' diff --git a/app/views/auth/passwordless/new.html.haml b/app/views/auth/passwordless/new.html.haml index bf2b2f54..d24d944d 100644 --- a/app/views/auth/passwordless/new.html.haml +++ b/app/views/auth/passwordless/new.html.haml @@ -1,23 +1,23 @@ -- content_for :title, 'Logowanie e-mailem' +- content_for :title, t('.title') = link_to login_path, class: 'gh-link-button gh-back-link' do %i.fa-solid.fa-chevron-left - %span Inne sposoby logowania + %span= t('.back') -%h1.gh-h1 Logowanie e-mailem -%p.gh-lead Wyślemy Ci link do logowania. Hasło nie jest potrzebne. +%h1.gh-h1= t('.title') +%p.gh-lead= t('.lead') = form_with url: auth_passwordless_path, method: :post do |f| .gh-form-field - = f.label :email, 'Adres e-mail' + = f.label :email, t('.email') .gh-text-input = f.email_field :email, - placeholder: 'jan.nowak@example.com', + placeholder: t('.placeholder'), autocomplete: 'email', autofocus: true, required: true .gh-button-stack - %button.gh-btn{ type: 'submit' } Wyślij link + %button.gh-btn{ type: 'submit' }= t('.send') = render 'shared/register_prompt' diff --git a/app/views/badges/_back.html.haml b/app/views/badges/_back.html.haml index 726aac0e..a6491a67 100644 --- a/app/views/badges/_back.html.haml +++ b/app/views/badges/_back.html.haml @@ -8,10 +8,10 @@ = badge_name(badge) %p.gh-badge-rule-text - %span Jak zdobyć + %span= t('.how') %span{ data: targets[:how] }= badge_rule(badge) - if flipper - %button.gh-card-flip-btn{ type: 'button', 'aria-label': "Pokaż odznakę #{badge.name}" } + %button.gh-card-flip-btn{ type: 'button', 'aria-label': t('.flip_label', name: badge.name) } %i.fa-solid.fa-arrows-rotate{ 'aria-hidden': 'true' } - Pokaż odznakę + = t('.flip') diff --git a/app/views/badges/_confirm_destroy.html.haml b/app/views/badges/_confirm_destroy.html.haml index e06c24de..2ab75bb2 100644 --- a/app/views/badges/_confirm_destroy.html.haml +++ b/app/views/badges/_confirm_destroy.html.haml @@ -1,24 +1,18 @@ -%h2#gh-del-title.gh-h2= "Usunąć odznakę „#{badge.name}”?" -%p - Studenci, którzy mają tę odznakę, zachowają ją w historii, ale nikt nowy jej - nie dostanie. +%h2#gh-del-title.gh-h2= t('.title', name: badge.name) +%p= t('.body') - if unlocking_items.any? - - one = unlocking_items.one? %p.gh-warning-note{ role: 'alert' } %i.fa-solid.fa-triangle-exclamation{ 'aria-hidden': 'true' } - %span - = "#{one ? 'Przedmiot' : 'Przedmioty'} #{gh_and_list(unlocking_items.map(&:name))}" - = one ? 'odblokowuje się tą odznaką' : 'odblokowują się tą odznaką' - = "i po usunięciu #{one ? 'kupi go' : 'kupią je'} tylko ci, którzy już ją mają." + %span= t('.unlocking', count: unlocking_items.size, items: gh_and_list(unlocking_items.map(&:name))) .gh-dialog-button-row - if @in_modal %button.gh-btn.gh-btn--secondary{ type: 'button', data: { action: 'dialog#close' }, autofocus: true } - Anuluj + = t('shared.actions.cancel') - else - = link_to 'Anuluj', edit_story_group_badge_path(@story_group, badge), class: 'gh-btn gh-btn--secondary' + = link_to t('shared.actions.cancel'), edit_story_group_badge_path(@story_group, badge), class: 'gh-btn gh-btn--secondary' - = button_to 'Usuń odznakę', story_group_badge_path(@story_group, badge), + = button_to t('.remove'), story_group_badge_path(@story_group, badge), method: :delete, class: 'gh-btn', form: { data: { turbo_frame: (@in_modal ? 'modal' : nil) } } diff --git a/app/views/badges/_flip_card.html.haml b/app/views/badges/_flip_card.html.haml index b1518343..c1f7ff7b 100644 --- a/app/views/badges/_flip_card.html.haml +++ b/app/views/badges/_flip_card.html.haml @@ -3,7 +3,7 @@ - withdrawn = local_assigns.fetch(:withdrawn, false) - targets = local_assigns.fetch(:targets, {}) -- label = earned ? "Odznaka #{badge.name}" : "Odznaka #{badge.name}, jeszcze niezdobyta" +- label = t(earned ? 'badges.flip_card.label' : 'badges.flip_card.label_unearned', name: badge.name) - card_data = targets[:flip].to_h - card_data.merge!(controller: 'badge-flip', action: 'click->badge-flip#toggle') if interactive diff --git a/app/views/badges/_form.html.haml b/app/views/badges/_form.html.haml index 02ce2808..28c6eb18 100644 --- a/app/views/badges/_form.html.haml +++ b/app/views/badges/_form.html.haml @@ -5,14 +5,16 @@ %section.gh-panel.gh-page-header.gh-page-header--stacked %p.gh-breadcrumbs - = link_to 'Odznaki', story_group_badges_path(@story_group) + = link_to t('.breadcrumb'), story_group_badges_path(@story_group) %i.fa-solid.fa-chevron-right{ 'aria-hidden': 'true' } - %span= editing ? (badge.name_was.presence || badge.name) : 'Nowa odznaka' - %h1.gh-h1= editing ? 'Edytuj odznakę' : 'Nowa odznaka' - %p.gh-lead - Podgląd obok pokazuje obie strony karty: zdobytą i jeszcze niezdobytą. + %span= editing ? (badge.name_was.presence || badge.name) : t('.new_title') + %h1.gh-h1= editing ? t('.edit_title') : t('.new_title') + %p.gh-lead= t('.lead') -- form_data = { controller: 'badge-form', action: 'gh:image-crop->badge-form#art' } +- form_data = { controller: 'badge-form', action: 'gh:image-crop->badge-form#art', + badge_form_name_placeholder_value: t('badges.helpers.name_placeholder'), + badge_form_rule_placeholder_value: t('badges.helpers.rule_placeholder'), + badge_form_discount_label_value: t('badges.helpers.discount', discount: '%{discount}'), } = form_with model: [@story_group, badge], data: form_data do |f| @@ -22,19 +24,18 @@ %p.gh-info-banner %i.fa-solid.fa-users{ 'aria-hidden': 'true' } %span - %b - = "#{holders} #{gh_plural(holders, 'student ma', 'studentów ma', 'studentów ma')} tę odznakę." - Zmiana zniżki od razu obejmie ich kolejne zakupy. + %b= t('.holders', count: holders) + = t('.holders_note') .gh-form-section - %h2.gh-h3 Karta odznaki - %p.gh-field-hint Tak odznaka wygląda w talii studenta i w oknie przyznawania. + %h2.gh-h3= t('.card') + %p.gh-field-hint= t('.card_hint') .gh-form-field - = f.label :name, 'Nazwa' + = f.label :name, t('shared.actions.name') .gh-text-input{ class: ('gh-text-input--invalid' if error.call(:name)) } = f.text_field :name, maxlength: 50, autocomplete: 'off', - placeholder: 'np. Perfekcyjny Lot', + placeholder: t('.name_placeholder'), 'aria-invalid': (error.call(:name) ? 'true' : nil), 'aria-describedby': (error.call(:name) ? 'gh-badge-name-error' : nil), data: { badge_form_target: 'name', action: 'badge-form#refresh' } @@ -44,10 +45,10 @@ %span= error.call(:name) .gh-form-field - = f.label :didactic_description, 'Jak zdobyć' + = f.label :didactic_description, t('.rule') .gh-text-input{ class: ('gh-text-input--invalid' if error.call(:didactic_description)) } = f.text_area :didactic_description, rows: 2, maxlength: 255, - placeholder: 'np. Co najmniej 3 wejściówki z rzędu bez błędów', + placeholder: t('.rule_placeholder'), 'aria-invalid': (error.call(:didactic_description) ? 'true' : nil), 'aria-describedby': (error.call(:didactic_description) ? 'gh-badge-rule-error' : nil), data: { badge_form_target: 'rule', action: 'badge-form#refresh' } @@ -55,17 +56,15 @@ %p#gh-badge-rule-error.gh-field-error{ role: 'alert' } %i.fa-solid.fa-triangle-exclamation{ 'aria-hidden': 'true' } %span= error.call(:didactic_description) - %span.gh-field-hint - Studenci widzą to na odwrocie odznaki, której jeszcze nie mają. - Pisz konkretnie. + %span.gh-field-hint= t('.rule_hint') .gh-form-field = f.label :story_description do - Opis fabularny - %small (opcjonalnie) + = t('.story') + %small= t('shared.actions.optional') .gh-text-input{ class: ('gh-text-input--invalid' if error.call(:story_description)) } = f.text_area :story_description, rows: 2, maxlength: 255, - placeholder: 'np. Lot bez najmniejszej rysy', + placeholder: t('.story_placeholder'), 'aria-invalid': (error.call(:story_description) ? 'true' : nil), 'aria-describedby': (error.call(:story_description) ? 'gh-badge-story-error' : nil), data: { badge_form_target: 'story', action: 'badge-form#refresh' } @@ -79,13 +78,11 @@ refresh_action: 'badge-form#refresh' .gh-form-section - %h2.gh-h3 Zniżka w sklepie - %p.gh-field-hint - Działa tylko na przedmioty, które ją uwzględniają. Ustawiasz to - w formularzu przedmiotu. + %h2.gh-h3= t('.discount_section') + %p.gh-field-hint= t('.discount_hint') .gh-form-field - = f.label :discount, 'Zniżka w sklepie' + = f.label :discount, t('.discount') .gh-price-field-row .gh-text-input.gh-text-input--numeric{ class: ('gh-text-input--invalid' if error.call(:discount)) } = f.number_field :discount, min: 0, max: 100, step: 1, @@ -101,34 +98,32 @@ - using = editing ? shelf.dependent_items(badge) : [] - if using.any? - %p.gh-form-label.gh-label--spaced Uwzględniają ją + %p.gh-form-label.gh-label--spaced= t('.used_by') %ul.gh-badge-usage-list - using.each do |item| %li = render 'layouts/item_thumb', item: item = item.name - else - %p.gh-explainer-note Na razie żaden przedmiot jej nie uwzględnia. + %p.gh-explainer-note= t('.unused') - if editing .gh-form-section - %h2.gh-h3 Usuwanie - %p.gh-field-hint - Odznaka zniknie z listy i z okna przyznawania. Studenci, którzy ją - mają, zachowają ją w historii. + %h2.gh-h3= t('shared.actions.removal') + %p.gh-field-hint= t('.removal_hint') .gh-button-row = link_to confirm_destroy_story_group_badge_path(@story_group, badge), class: 'gh-btn gh-btn--secondary', data: { turbo_frame: 'modal' } do %i.fa-solid.fa-trash{ 'aria-hidden': 'true' } - Usuń odznakę + = t('.remove') - %aside.gh-preview-column{ 'aria-label': 'Podgląd' } + %aside.gh-preview-column{ 'aria-label': t('shared.actions.preview') } = render 'preview', badge: badge .gh-panel.gh-form-actions-bar - = link_to 'Anuluj', story_group_badges_path(@story_group), class: 'gh-link-button' + = link_to t('shared.actions.cancel'), story_group_badges_path(@story_group), class: 'gh-link-button' %span.gh-form-actions-spacer %button.gh-btn{ type: 'submit' } - unless editing %i.fa-solid.fa-plus{ 'aria-hidden': 'true' } - = editing ? 'Zapisz zmiany' : 'Dodaj odznakę' + = editing ? t('shared.actions.save_changes') : t('.create') diff --git a/app/views/badges/_front.html.haml b/app/views/badges/_front.html.haml index 9ad1cac9..198e4618 100644 --- a/app/views/badges/_front.html.haml +++ b/app/views/badges/_front.html.haml @@ -12,7 +12,7 @@ - if local_assigns[:withdrawn] .gh-card-tags - %span.gh-tag.gh-tag--removed Usunięta z listy + %span.gh-tag.gh-tag--removed= t('.removed') %p.gh-rule-text{ class: ('gh-placeholder-text' if badge.didactic_description.blank?), data: targets[:rules] } = badge_rule(badge) @@ -24,6 +24,6 @@ %span.gh-tag.gh-tag--discount{ hidden: discount.nil?, data: targets[:tag] }= discount - if flipper %button.gh-card-flip-btn{ type: 'button', - 'aria-label': "Pokaż, jak zdobyć odznakę #{badge.name}" } + 'aria-label': t('.flip_label', name: badge.name) } %i.fa-solid.fa-arrows-rotate{ 'aria-hidden': 'true' } - Jak zdobyć + = t('.flip') diff --git a/app/views/badges/_preview.html.haml b/app/views/badges/_preview.html.haml index 8e9c5950..a5179633 100644 --- a/app/views/badges/_preview.html.haml +++ b/app/views/badges/_preview.html.haml @@ -9,26 +9,26 @@ %details.gh-preview-details{ open: true } %summary - Podgląd karty + = t('.summary') %i.fa-solid.fa-chevron-down{ 'aria-hidden': 'true' } %section.gh-panel.gh-preview-panel - %h3 Tak zobaczą to studenci + %h3= t('.students_view') - .gh-segmented-toggle-2.gh-octagon-clip{ role: 'group', 'aria-label': 'Strona karty' } + .gh-segmented-toggle-2.gh-octagon-clip{ role: 'group', 'aria-label': t('.side') } %button{ type: 'button', 'aria-pressed': 'true', value: 'won', data: { action: 'badge-form#side', badge_form_target: 'side' } } - Zdobyta + = t('.earned') %button{ type: 'button', 'aria-pressed': 'false', value: 'lost', data: { action: 'badge-form#side', badge_form_target: 'side' } } - Jeszcze niezdobyta + = t('.unearned') .gh-preview-flip-card = render 'flip_card', badge: badge, earned: true, interactive: false, targets: targets %section.gh-panel.gh-preview-panel - %h3 W oknie przyznawania + %h3= t('.picker') .gh-preview-thumb-row = render 'badge_thumb', badge: badge, inner_data: { badge_form_target: 'thumbArt' } diff --git a/app/views/badges/_student_deck.html.haml b/app/views/badges/_student_deck.html.haml index 51d10899..c454dd1f 100644 --- a/app/views/badges/_student_deck.html.haml +++ b/app/views/badges/_student_deck.html.haml @@ -1,10 +1,7 @@ %section.gh-panel.gh-page-header .gh-page-header-text - %h1.gh-h1 Odznaki - %p.gh-lead - = "Masz #{shelf.earned_count} z #{shelf.size}." - Odznaki przyznaje prowadzący. Na odwrocie każdej niezdobytej znajdziesz, - jak ją zdobyć. + %h1.gh-h1= t('.title') + %p.gh-lead= t('.lead', earned: shelf.earned_count, total: shelf.size) - if shelf.any? .gh-card-grid.gh-card-grid--compact @@ -15,7 +12,5 @@ withdrawn: slot.withdrawn? - else %section.gh-panel.gh-empty-state - %h2.gh-h2 W tej grupie nie ma jeszcze odznak - %p.gh-lead - Prowadzący ich nie dodał. Kiedy się pojawią, zobaczysz tu, co trzeba - zrobić, żeby je zdobyć. + %h2.gh-h2= t('.empty_title') + %p.gh-lead= t('.empty_body') diff --git a/app/views/badges/_teacher_grid.html.haml b/app/views/badges/_teacher_grid.html.haml index fcc6de68..7a1e625b 100644 --- a/app/views/badges/_teacher_grid.html.haml +++ b/app/views/badges/_teacher_grid.html.haml @@ -1,14 +1,12 @@ %section.gh-panel.gh-page-header .gh-page-header-text - %h1.gh-h1 Odznaki - %p.gh-lead - = "#{shelf.size} #{gh_plural(shelf.size, 'odznaka', 'odznaki', 'odznak')}." - Przyznajesz je ręcznie, z tej listy albo z listy studentów. + %h1.gh-h1= t('.title') + %p.gh-lead= t('.lead', count: shelf.size) .gh-button-row = link_to new_story_group_badge_path(@story_group), class: 'gh-btn' do %i.fa-solid.fa-plus - Nowa odznaka + = t('.new') - if shelf.any? .gh-badge-grid--teacher @@ -34,14 +32,12 @@ .gh-card-footer %span.gh-card-meta= badge_holders_line(slot.holders) .gh-card-actions - = link_to 'Przyznaj', story_group_students_path(@story_group), + = link_to t('.award'), story_group_students_path(@story_group), class: 'gh-btn gh-btn--small' = link_to edit_story_group_badge_path(@story_group, badge), - class: 'gh-icon-button', 'aria-label': "Edytuj odznakę #{badge.name}" do + class: 'gh-icon-button', 'aria-label': t('.edit_label', name: badge.name) do %i.fa-solid.fa-pen{ 'aria-hidden': 'true' } - else %section.gh-panel.gh-empty-state - %h2.gh-h2 Nie ma jeszcze żadnych odznak - %p.gh-lead - Dodaj odznaki, żeby mieć czym nagradzać to, czego nie widać w punktach. - Przyznajesz je ręcznie, a każda może dawać zniżkę w sklepie. + %h2.gh-h2= t('.empty_title') + %p.gh-lead= t('.empty_body') diff --git a/app/views/badges/confirm_destroy.html.haml b/app/views/badges/confirm_destroy.html.haml index 16b7407a..7391b1c4 100644 --- a/app/views/badges/confirm_destroy.html.haml +++ b/app/views/badges/confirm_destroy.html.haml @@ -1,4 +1,4 @@ -- content_for :title, "Usuń odznakę #{@badge.name}" +- content_for :title, t('.page_title', name: @badge.name) - if @in_modal = turbo_frame_tag 'modal' do diff --git a/app/views/badges/edit.html.haml b/app/views/badges/edit.html.haml index 432b26ba..c6544703 100644 --- a/app/views/badges/edit.html.haml +++ b/app/views/badges/edit.html.haml @@ -1,3 +1,3 @@ -- content_for :title, "Edytuj odznakę - #{@badge.name}" +- content_for :title, t('.page_title', name: @badge.name) = render 'form', badge: @badge, shelf: @shelf diff --git a/app/views/badges/index.html.haml b/app/views/badges/index.html.haml index 2878a01f..2c36d44b 100644 --- a/app/views/badges/index.html.haml +++ b/app/views/badges/index.html.haml @@ -1,4 +1,4 @@ -- content_for :title, 'Odznaki' +- content_for :title, t('.title') - if @shelf.membership = render 'student_deck', shelf: @shelf diff --git a/app/views/badges/new.html.haml b/app/views/badges/new.html.haml index 2b4eaf97..0235db66 100644 --- a/app/views/badges/new.html.haml +++ b/app/views/badges/new.html.haml @@ -1,3 +1,3 @@ -- content_for :title, 'Nowa odznaka' +- content_for :title, t('.page_title') = render 'form', badge: @badge, shelf: @shelf diff --git a/app/views/currency_adjustments/_form.html.haml b/app/views/currency_adjustments/_form.html.haml index e6a69a70..0f5050ea 100644 --- a/app/views/currency_adjustments/_form.html.haml +++ b/app/views/currency_adjustments/_form.html.haml @@ -2,29 +2,27 @@ - error = form.error_for(:amount) - rungs = @story_group.ranks.by_threshold.map { |r| { v: r.required_currency_value.to_i, n: r.name } } -- add_note = 'Dodanie podnosi też sumę zebranych, więc może podnieść rangę.' -- take_note = 'Odjęcie zmniejsza tylko to, co student ma do wydania. Suma zebranych i ranga zostają bez zmian.' - form_data = { controller: 'currency-adjust', turbo_frame: (@in_modal ? 'modal' : nil), currency_adjust_balance_value: form.balance, currency_adjust_total_value: form.total, currency_adjust_rungs_value: rungs.to_json, - currency_adjust_over_value: "Student ma tylko #{form.balance} do wydania.", - currency_adjust_unchanged_value: 'bez zmian', - currency_adjust_add_value: 'Dodaj', - currency_adjust_take_value: 'Odejmij', - currency_adjust_blank_value: 'Podaj kwotę', - currency_adjust_add_note_value: add_note, - currency_adjust_take_note_value: take_note, } + currency_adjust_over_value: t('.over', balance: form.balance), + currency_adjust_unchanged_value: t('.unchanged'), + currency_adjust_add_value: t('shared.actions.add'), + currency_adjust_take_value: t('.take'), + currency_adjust_blank_value: t('.blank'), + currency_adjust_add_note_value: t('.add_note'), + currency_adjust_take_note_value: t('.take_note'), } = form_with url: story_group_student_currency_adjustment_path(@story_group, student), method: :post, scope: :currency_adjustment, data: form_data do |f| - %h2#gh-adjust-title.gh-h2 Koryguj walutę - %p= "#{student.display_name} ma teraz #{form.balance} do wydania." + %h2#gh-adjust-title.gh-h2= t('.title') + %p= t('.lead', name: student.display_name, balance: form.balance) - .gh-segmented-toggle-3.gh-octagon-clip{ role: 'radiogroup', 'aria-label': 'Rodzaj korekty' } - - [[1, 'Dodaj'], [-1, 'Odejmij']].each do |value, label| + .gh-segmented-toggle-3.gh-octagon-clip{ role: 'radiogroup', 'aria-label': t('.kind_label') } + - [[1, t('shared.actions.add')], [-1, t('.take')]].each do |value, label| %label.gh-segmented-toggle-3-option = f.radio_button :sign, value, checked: (form.sign == value), @@ -32,7 +30,7 @@ %span= label .gh-form-field - = f.label :amount, 'Ile' + = f.label :amount, t('.amount') .gh-price-field-row .gh-text-input.gh-text-input--numeric{ class: ('gh-text-input--invalid' if error), data: { currency_adjust_target: 'box' } } = f.number_field :amount, min: 1, step: 1, placeholder: '0', autocomplete: 'off', @@ -49,29 +47,25 @@ - shown = form.value.present? && error.nil? %dl.gh-adjustment-preview{ hidden: !shown, data: { currency_adjust_target: 'preview' } } - %dt Do wydania + %dt= t('.balance') %dd{ data: { currency_adjust_target: 'balance' } }= "#{form.balance} → #{form.new_balance}" - %dt Zebrane łącznie + %dt= t('.total') %dd{ data: { currency_adjust_target: 'total' } } - = form.total_changed? ? "#{form.total} → #{form.new_total}" : "#{form.total} (bez zmian)" - %dt.gh-cprev2-r{ hidden: !form.rank_changed?, data: { currency_adjust_target: 'rankLabel' } } Ranga + = form.total_changed? ? "#{form.total} → #{form.new_total}" : t('.total_unchanged', total: form.total) + %dt.gh-cprev2-r{ hidden: !form.rank_changed?, data: { currency_adjust_target: 'rankLabel' } }= t('.rank') %dd.gh-cprev2-r{ hidden: !form.rank_changed?, data: { currency_adjust_target: 'rank' } } = "#{form.current_rank&.name || '—'} → #{form.new_rank&.name || '—'}" %p.gh-small.gh-adjustment-preview-note{ hidden: !shown, data: { currency_adjust_target: 'note' } } - - if form.positive? - Dodanie podnosi też sumę zebranych, więc może podnieść rangę. - - else - Odjęcie zmniejsza tylko to, co student ma do wydania. Suma zebranych - i ranga zostają bez zmian. + = form.positive? ? t('.add_note') : t('.take_note') .gh-dialog-button-row - if @in_modal - %button.gh-btn.gh-btn--secondary{ type: 'button', data: { action: 'dialog#close' } } Anuluj + %button.gh-btn.gh-btn--secondary{ type: 'button', data: { action: 'dialog#close' } }= t('shared.actions.cancel') - else - = link_to 'Anuluj', story_group_student_path(@story_group, student), class: 'gh-btn gh-btn--secondary' + = link_to t('shared.actions.cancel'), story_group_student_path(@story_group, student), class: 'gh-btn gh-btn--secondary' %button.gh-btn{ type: 'submit', data: { currency_adjust_target: 'submit' } } - if form.value.present? - = "#{form.positive? ? 'Dodaj' : 'Odejmij'} #{form.value}" + = t(form.positive? ? '.submit_add' : '.submit_take', value: form.value) - else - Podaj kwotę + = t('.blank') diff --git a/app/views/currency_adjustments/new.html.haml b/app/views/currency_adjustments/new.html.haml index d0a08b54..42ba7eba 100644 --- a/app/views/currency_adjustments/new.html.haml +++ b/app/views/currency_adjustments/new.html.haml @@ -1,4 +1,4 @@ -- content_for :title, "Koryguj walutę - #{@student.display_name}" +- content_for :title, t('.page_title', name: @student.display_name) - if @in_modal = turbo_frame_tag 'modal' do diff --git a/app/views/currency_transactions/_ledger.html.haml b/app/views/currency_transactions/_ledger.html.haml index 62a69ce3..06636e48 100644 --- a/app/views/currency_transactions/_ledger.html.haml +++ b/app/views/currency_transactions/_ledger.html.haml @@ -7,13 +7,13 @@ %span.gh-ledger-header-spacer %span.gh-small= summary -%div{ role: 'table', 'aria-label': 'Historia waluty' } +%div{ role: 'table', 'aria-label': t('.table_label') } .gh-ledger-row.gh-ledger-row--header{ role: 'row' } - %span{ role: 'columnheader' } Kwota - %span{ role: 'columnheader' } Typ - %span{ role: 'columnheader' } Za co - %span{ role: 'columnheader' } Kiedy - %span.gh-ledger-cell--right{ role: 'columnheader' } Saldo po + %span{ role: 'columnheader' }= t('.columns.amount') + %span{ role: 'columnheader' }= t('.columns.kind') + %span{ role: 'columnheader' }= t('.columns.source') + %span{ role: 'columnheader' }= t('.columns.when') + %span.gh-ledger-cell--right{ role: 'columnheader' }= t('.columns.balance_after') - entries.each do |entry| .gh-ledger-row{ role: 'row' } @@ -22,7 +22,7 @@ %span.gh-ledger-source{ role: 'cell' } = entry.title - if entry.withdrawn? - %span.gh-tag.gh-tag--removed Usunięty z oferty + %span.gh-tag.gh-tag--removed= t('.withdrawn') - if entry.context.present? %small= entry.context %span.gh-small.gh-ledger-date{ role: 'cell' }= ledger_when(entry) diff --git a/app/views/currency_transactions/index.html.haml b/app/views/currency_transactions/index.html.haml index 97aa9964..1354b668 100644 --- a/app/views/currency_transactions/index.html.haml +++ b/app/views/currency_transactions/index.html.haml @@ -1,24 +1,22 @@ -- content_for :title, 'Historia waluty' +- content_for :title, t('.title') - path_for = ->(kind) { story_group_student_currency_transactions_path(@story_group, @student, kind: kind) } %section.gh-panel.gh-page-header .gh-page-header-text - %h1.gh-h1 Historia waluty + %h1.gh-h1= t('.title') %p.gh-lead= ledger_balance_sentence(@ledger) .gh-button-row = link_to story_group_shop_index_path(@story_group), class: 'gh-btn gh-btn--secondary' do %i.fa-solid.fa-bag-shopping{ 'aria-hidden': 'true' } - Przejdź do sklepu + = t('.go_to_shop') - if @ledger.any? %section.gh-panel.gh-tab-panel = render 'currency_transactions/ledger', ledger: @ledger, kind: @kind, path_for: path_for, - empty_message: 'Nie masz jeszcze wpisów tego typu.' + empty_message: t('.empty_kind') - else %section.gh-panel.gh-empty-state - %h2.gh-h2 Pierwsze wpisy pojawią się po zajęciach - %p.gh-lead - Prowadzący przyznaje walutę za obecność, punktualność i aktywność. - Wszystko, co zdobędziesz i wydasz, zapisze się tutaj. + %h2.gh-h2= t('.empty_title') + %p.gh-lead= t('.empty_body') diff --git a/app/views/home/_capacity_bar.html.haml b/app/views/home/_capacity_bar.html.haml index b60baa28..427c3e6f 100644 --- a/app/views/home/_capacity_bar.html.haml +++ b/app/views/home/_capacity_bar.html.haml @@ -1,5 +1,5 @@ - if max.nil? - %span.gh-small= "#{members} #{gh_plural(members, 'konto', 'konta', 'kont')} · bez limitu" + %span.gh-small= t('home.capacity_bar.unlimited', count: members) - else .gh-capacity-bar .gh-progress-bar.gh-octagon-clip{ class: ('gh-progress-bar--full' if full), @@ -7,6 +7,6 @@ 'aria-valuemin': 0, 'aria-valuemax': max, 'aria-valuenow': members, - 'aria-label': 'Zajęte miejsca', } + 'aria-label': t('home.capacity_bar.label'), } %i.gh-octagon-clip{ style: "--gh-p: #{percent}%" } %span.gh-small= "#{members} / #{max}" diff --git a/app/views/home/_global_admin_start.html.haml b/app/views/home/_global_admin_start.html.haml index 19d7d234..ff1c8510 100644 --- a/app/views/home/_global_admin_start.html.haml +++ b/app/views/home/_global_admin_start.html.haml @@ -5,75 +5,69 @@ %h1.gh-h1= home_greeting(@current_user) %p.gh-lead - if organizations.empty? - Nie ma jeszcze żadnej organizacji. Utwórz pierwszą, żeby nowe konta miały gdzie trafić. + = t('.no_organizations_lead') - elsif @dashboard.attention? - = "#{organizations.size} #{gh_plural(organizations.size, 'organizacja', 'organizacje', 'organizacji')} —" - część z nich potrzebuje Twojej uwagi. + = t('.attention_lead', count: organizations.size) - else - = "#{organizations.size} #{gh_plural(organizations.size, 'organizacja', 'organizacje', 'organizacji')}." - Wszystkie mają administratora i wolne miejsca. + = t('.fine_lead', count: organizations.size) = link_to new_organization_path, class: 'gh-btn', data: { turbo_frame: 'modal' } do %i.fa-solid.fa-plus{ 'aria-hidden': 'true' } - Utwórz organizację + = t('.create') - if organizations.any? - = render 'stat_tiles', tiles: @dashboard.stat_tiles, label: 'Podsumowanie organizacji' + = render 'stat_tiles', tiles: @dashboard.stat_tiles, label: t('.stats_label') .gh-dashboard-layout %section.gh-panel.gh-recent-purchases-panel{ 'aria-labelledby': 'gh-orgs-title' } - %h2#gh-orgs-title.gh-h2 Organizacje + %h2#gh-orgs-title.gh-h2= t('.organizations') .gh-start-org-list - organizations.each do |org| = link_to organization_path(org.organization), class: 'gh-start-org-row' do %span.gh-name-cell %b= org.organization.name %small - = "#{org.admins} #{gh_plural(org.admins, 'administrator', 'administratorów', 'administratorów')}" + = t('.admins', count: org.admins) - if org.pending_admins.positive? - = "· #{org.pending_admins} #{gh_plural(org.pending_admins, 'zaproszenie czeka', 'zaproszenia czekają', 'zaproszeń czeka')}" + = "· #{t('.pending_admins', count: org.pending_admins)}" = render 'capacity_bar', members: org.member_count, max: org.organization.max_members, percent: org.fill_percent, full: org.full? - = link_to 'Wszystkie organizacje', organizations_path, class: 'gh-btn gh-btn--secondary gh-start-more' + = link_to t('.all_organizations'), organizations_path, class: 'gh-btn gh-btn--secondary gh-start-more' .gh-dashboard-side-column - if @dashboard.attention? %section.gh-card.gh-todo-card .gh-card-frame .gh-card-inset - %p.gh-page-kicker Wymaga uwagi + %p.gh-page-kicker= t('.attention') %ul.gh-start-attention-list - @dashboard.without_admin.each do |org| %li = link_to org.organization.name, organization_path(org.organization) - %small Brak administratora + %small= t('.no_admin') = link_to new_admin_organization_path(org.organization), class: 'gh-btn gh-btn--secondary gh-btn--small', data: { turbo_frame: 'modal' } do %i.fa-solid.fa-user-shield{ 'aria-hidden': 'true' } - Dodaj + = t('.add') - @dashboard.nearly_full.each do |org| %li = link_to org.organization.name, organization_path(org.organization) - %small= org.full? ? 'Brak wolnych miejsc' : "Zostało #{org.free} #{gh_plural(org.free, 'miejsce', 'miejsca', 'miejsc')}" + %small= org.full? ? t('.no_free_seats') : t('.free_seats', count: org.free) = link_to edit_organization_path(org.organization), class: 'gh-btn gh-btn--secondary gh-btn--small', data: { turbo_frame: 'modal' } do %i.fa-solid.fa-pen{ 'aria-hidden': 'true' } - Limit + = t('.limit') - else %section.gh-panel.gh-my-groups-panel - %h2.gh-h2 Wszystko w porządku - %p.gh-small Każda organizacja ma administratora i wolne miejsca. + %h2.gh-h2= t('.fine_title') + %p.gh-small= t('.fine_body') %section.gh-panel.gh-my-groups-panel - %h2.gh-h2 Jak to działa - %p.gh-small - Każdy, kto zaloguje się przez USOS, trafia do organizacji - #{Organization::USOS_NAME}. Nie ma ona limitu miejsc i nie można jej - usunąć. Do pozostałych organizacji konta dodają ich administratorzy. + %h2.gh-h2= t('.how_title') + %p.gh-small= t('.how_body', usos: Organization::USOS_NAME) - else %section.gh-panel.gh-empty-state - %h2.gh-h2 Zacznij od pierwszej organizacji - %p.gh-lead - Ustaw limit kont i dodaj administratora — on zaprosi resztę osób. + %h2.gh-h2= t('.empty_title') + %p.gh-lead= t('.empty_lead') .gh-button-row - = link_to 'Utwórz organizację', new_organization_path, class: 'gh-btn', data: { turbo_frame: 'modal' } + = link_to t('.create'), new_organization_path, class: 'gh-btn', data: { turbo_frame: 'modal' } diff --git a/app/views/home/_organization_admin_start.html.haml b/app/views/home/_organization_admin_start.html.haml index 0cb76d74..b01af678 100644 --- a/app/views/home/_organization_admin_start.html.haml +++ b/app/views/home/_organization_admin_start.html.haml @@ -3,9 +3,7 @@ - if organization.nil? %section.gh-panel.gh-empty-state %h1.gh-h1= home_greeting(@current_user) - %p.gh-lead - Twoje konto administratora nie jest przypisane do żadnej organizacji. - Skontaktuj się z administratorem systemu. + %p.gh-lead= t('.no_organization') - else %section.gh-panel.gh-page-header .gh-page-header-text @@ -13,27 +11,25 @@ %h1.gh-h1= home_greeting(@current_user) %p.gh-lead - if @dashboard.full? - Organizacja nie ma wolnych miejsc — nowe osoby nie dołączą, dopóki - ktoś nie zostanie usunięty albo limit nie wzrośnie. + = t('.full_lead') - elsif @dashboard.pending_users.any? - - pending = @dashboard.pending_users.size - = "#{pending} #{gh_plural(pending, 'osoba jeszcze nie przyjęła', 'osoby jeszcze nie przyjęły', 'osób jeszcze nie przyjęło')} zaproszenia." + = t('.pending_lead', count: @dashboard.pending_users.size) - else = organization_capacity_lead(organization, @dashboard.member_count) .gh-button-row = link_to organization_path(organization), class: 'gh-btn gh-btn--secondary' do %i.fa-solid.fa-users{ 'aria-hidden': 'true' } - Użytkownicy + = t('.users') = link_to new_user_organization_path(organization), class: 'gh-btn', data: { turbo_frame: 'modal' } do %i.fa-solid.fa-user-plus{ 'aria-hidden': 'true' } - Dodaj użytkownika + = t('.add_user') - = render 'stat_tiles', tiles: @dashboard.stat_tiles, label: 'Podsumowanie organizacji' + = render 'stat_tiles', tiles: @dashboard.stat_tiles, label: t('.stats_label') .gh-dashboard-layout %section.gh-panel.gh-recent-purchases-panel{ 'aria-labelledby': 'gh-recent-users-title' } - %h2#gh-recent-users-title.gh-h2 Ostatnio dodani + %h2#gh-recent-users-title.gh-h2= t('.recent') - if @dashboard.recent_users.any? .gh-start-org-list - @dashboard.recent_users.each do |user| @@ -41,31 +37,27 @@ = render 'organizations/person', user: user %span.gh-teacher-role = user_role_label(user.role) - %small= user.first_login? ? 'Zaproszenie wysłane' : "Dodano #{user.created_at.strftime('%d.%m.%Y')}" - = link_to 'Wszyscy użytkownicy', organization_path(organization), class: 'gh-btn gh-btn--secondary gh-start-more' + %small= user.first_login? ? t('.invited') : t('.added', when: gh_when(user.created_at)) + = link_to t('.all_users'), organization_path(organization), class: 'gh-btn gh-btn--secondary gh-start-more' - else - %p.gh-small - Nikogo tu jeszcze nie ma. Dodaj studentów i nauczycieli albo poczekaj, - aż zalogują się przez uczelnię. + %p.gh-small= t('.nobody') .gh-dashboard-side-column %section.gh-panel.gh-my-groups-panel - %h2.gh-h2 Miejsca + %h2.gh-h2= t('.seats') = render 'capacity_bar', members: @dashboard.member_count, max: organization.max_members, percent: @dashboard.fill_percent, full: @dashboard.full? %p.gh-small - if organization.unlimited? - Ta organizacja nie ma limitu miejsc. Każdy, kto zaloguje się przez - USOS, trafia tutaj automatycznie. + = t('.unlimited_note') - else - Limit ustala administrator systemu. Każde konto, także administratora, - zajmuje jedno miejsce. + = t('.limited_note') - if @dashboard.pending_users.any? %section.gh-card.gh-todo-card .gh-card-frame .gh-card-inset - %p.gh-page-kicker Czeka na przyjęcie zaproszenia + %p.gh-page-kicker= t('.pending_title') %ul.gh-start-attention-list - @dashboard.pending_users.first(Dashboard::OrganizationAdmin::RECENT_LIMIT).each do |user| %li diff --git a/app/views/home/_student_start.html.haml b/app/views/home/_student_start.html.haml index 890e0162..b7b7615e 100644 --- a/app/views/home/_student_start.html.haml +++ b/app/views/home/_student_start.html.haml @@ -2,13 +2,8 @@ %section.gh-panel.gh-page-header .gh-page-header-text - %h1.gh-h1 Cześć, #{@current_user.full_name.to_s.split(' ').first} - %p.gh-lead - - if groups.any? - = "Należysz do #{groups.size} #{gh_plural(groups.size, 'grupy', 'grup', 'grup')}." - Waluta, rangi i odznaki są osobne w każdej z nich. - - else - Nie należysz jeszcze do żadnej grupy. Poproś prowadzącego o kod i dołącz poniżej. + %h1.gh-h1= t('.greeting', name: @current_user.full_name.to_s.split(' ').first) + %p.gh-lead= groups.any? ? t('.groups_lead', count: groups.size) : t('.no_groups_lead') .gh-group-card-grid - groups.each do |group| @@ -26,19 +21,18 @@ %h2.gh-card-name= story_group.name - if group.fresh? - %p.gh-explainer-note - = "Dopiero zaczynasz. Pierwsze #{story_group.currency_name} zdobędziesz na zajęciach." + %p.gh-explainer-note= t('.fresh', currency: story_group.currency_name) %dl.gh-group-card-stats %div - %dt Do wydania + %dt= t('.to_spend') %dd = render 'layouts/currency_token', story_group: story_group = group.balance %small= story_group.currency_name %div - %dt Życia + %dt= t('.lives') %dd %span.gh-lives-badge{ class: ('gh-lives-badge--zero' if group.lives.zero?) } %i.fa-solid.fa-heart{ 'aria-hidden': 'true' } @@ -46,43 +40,42 @@ .gh-stat-span-full %dt - Ranga: - %b= group.rank&.name || 'brak' + = t('.rank') + %b= group.rank&.name || t('.no_rank') - if group.next_rank %dd.gh-rank-progress-target - = "#{group.total} z #{group.next_threshold} do rangi #{group.next_rank.name}" + = t('.to_next', total: group.total, needed: group.next_threshold, rank: group.next_rank.name) - else - %dd.gh-rank-progress-target - = "#{group.total} zebranych. To najwyższa ranga." + %dd.gh-rank-progress-target= t('.top', total: group.total) .gh-progress-bar.gh-octagon-clip{ role: 'progressbar', 'aria-valuemin': 0, 'aria-valuemax': group.next_threshold.positive? ? group.next_threshold : group.total, 'aria-valuenow': group.total, - 'aria-label': "Postęp do następnej rangi w grupie #{story_group.name}" } + 'aria-label': t('.progress_label', name: story_group.name) } %i.gh-octagon-clip{ style: "--gh-p: #{gh_percent(group.total, group.next_threshold)}%" } %div - %dt Odznaki + %dt= t('.badges') %dd = group.badges_earned - %small= "z #{group.badges_total}" + %small= t('.badges_of', total: group.badges_total) .gh-card-footer - = link_to 'Otwórz grupę', story_group_path(story_group), class: 'gh-btn' + = link_to t('.open_group'), story_group_path(story_group), class: 'gh-btn' = link_to story_group_shop_index_path(story_group), class: 'gh-btn gh-btn--secondary' do %i.fa-solid.fa-bag-shopping - Sklep + = t('.shop') .gh-panel.gh-join-code-card - %h2.gh-h3 Masz kod od prowadzącego? - %p.gh-small Kod ma 6 znaków. Możesz też zeskanować kod QR aparatem telefonu. + %h2.gh-h3= t('.have_code') + %p.gh-small= t('.code_hint') = link_to new_join_path, class: 'gh-btn gh-btn--secondary', data: { turbo_frame: 'modal' } do %i.fa-solid.fa-plus - Dołącz do grupy + = t('.join') - if @dashboard.feed.any? %section.gh-panel.gh-activity-feed - %h2.gh-h2 Ostatnio we wszystkich grupach + %h2.gh-h2= t('.feed') %ul.gh-ledger-list - @dashboard.feed.each do |entry| - tone = entry.reward? ? 'earn' : (entry.purchase? ? 'spend' : nil) @@ -90,6 +83,6 @@ %span{ class: "gh-activity-feed-row-marker #{"gh-activity-feed-row-marker--#{tone}" if tone}", 'aria-hidden': 'true' } %span.gh-activity-feed-row-text = gh_feed_title(entry) - %small= "#{entry.student.story_group.name}, #{gh_when(entry.created_at)}" + %small= t('.feed_meta', group: entry.student.story_group.name, when: gh_when(entry.created_at)) %span{ class: "gh-activity-feed-row-amount #{"gh-activity-feed-row-amount--#{tone}" if tone}" } = entry.amount.positive? ? "+#{entry.amount}" : entry.amount diff --git a/app/views/home/_teacher_start.html.haml b/app/views/home/_teacher_start.html.haml index 09e56f30..04cdbb23 100644 --- a/app/views/home/_teacher_start.html.haml +++ b/app/views/home/_teacher_start.html.haml @@ -3,24 +3,23 @@ %section.gh-panel.gh-page-header .gh-page-header-text - %h1.gh-h1 Dzień dobry, #{@current_user.full_name.to_s.split(' ').first} + %h1.gh-h1= t('.greeting', name: @current_user.full_name.to_s.split(' ').first) %p.gh-lead - if recent.any? - - group_count = @dashboard.recent_group_count - = "Od wczoraj studenci kupili #{recent.size} #{gh_plural(recent.size, 'przedmiot', 'przedmioty', 'przedmiotów')}" - = "w #{group_count} #{gh_plural(group_count, 'grupie', 'grupach', 'grupach')}." + = t('.recent_lead', items: t('.recent_items', count: recent.size), + groups: t('.recent_groups', count: @dashboard.recent_group_count)) - elsif groups.any? - Od wczoraj nikt nic nie kupił. Zajrzyj do swoich grup poniżej. + = t('.quiet_lead') - else - Nie prowadzisz jeszcze żadnej grupy. Utwórz pierwszą, żeby zacząć. + = t('.no_groups_lead') .gh-dashboard-layout %section.gh-panel.gh-recent-purchases-panel{ 'aria-labelledby': 'gh-purch-title' } - %h2#gh-purch-title.gh-h2 Ostatnie zakupy + %h2#gh-purch-title.gh-h2= t('.purchases') - if groups.size > 1 - .gh-purchase-filter-bar{ role: 'group', 'aria-label': 'Pokaż zakupy z grupy' } - = link_to 'Wszystkie grupy', home_path, class: 'gh-purchase-filter-chip', + .gh-purchase-filter-bar{ role: 'group', 'aria-label': t('.filter') } + = link_to t('.all_groups'), home_path, class: 'gh-purchase-filter-chip', 'aria-current': @dashboard.filter.nil?.to_s - groups.each do |group| = link_to home_path(group: group.story_group.id), class: 'gh-purchase-filter-chip', @@ -32,13 +31,13 @@ - Dashboard::Teacher::DAYS.each do |day| - rows = @dashboard.purchases_on(day) - next if rows.empty? - %h3.gh-purchase-day-label= day + %h3.gh-purchase-day-label= t("home.days.#{day}") %ul.gh-purchase-list - rows.each do |purchase| %li.gh-purchase-row = render 'layouts/item_thumb', item: purchase.item %span.gh-purchase-row-main - %b= purchase.item&.name || 'Przedmiot usunięty z oferty' + %b= purchase.item&.name || t('.removed_item') %span= purchase.student_name %span.gh-purchase-row-group--mobile = render 'layouts/group_chip', story_group: purchase.story_group @@ -49,11 +48,7 @@ = render 'layouts/currency_token', story_group: purchase.story_group, size: 16 %span.gh-purchase-row-time= purchase.time - else - %p.gh-small.mt-4 - - if @dashboard.filter - W tej grupie nie ma jeszcze żadnych zakupów. - - else - Nikt jeszcze niczego nie kupił. Zakupy pojawią się tutaj same. + %p.gh-small.mt-4= @dashboard.filter ? t('.no_group_purchases') : t('.no_purchases') .gh-dashboard-side-column - if @dashboard.pending @@ -61,18 +56,18 @@ %section.gh-card.gh-todo-card .gh-card-frame .gh-card-inset - %p.gh-page-kicker Czeka na ocenę + %p.gh-page-kicker= t('.pending') %h2.gh-todo-card-title= pending.activity_group.name = render 'layouts/group_chip', story_group: pending.story_group %p.gh-small - = "Zostało #{pending.categories_count} #{gh_plural(pending.categories_count, 'kategoria', 'kategorie', 'kategorii')}" - = "dla #{pending.students_count} #{gh_plural(pending.students_count, 'studenta', 'studentów', 'studentów')}." - = link_to 'Oceń', + = t('.pending_left', categories: t('.pending_categories', count: pending.categories_count), + students: t('.pending_students', count: pending.students_count)) + = link_to t('.grade'), edit_story_group_activity_group_students_activity_group_categories_path(pending.story_group, pending.activity_group), class: 'gh-btn' %section.gh-panel.gh-my-groups-panel - %h2.gh-h2 Twoje grupy + %h2.gh-h2= t('.your_groups') - if groups.any? %ul.gh-my-groups-list - groups.each do |group| @@ -81,14 +76,13 @@ = render 'layouts/group_thumb', story_group: group.story_group, size: :lg %span %b= group.story_group.name - %small - = "#{group.role}, #{group.students_count} #{gh_plural(group.students_count, 'student', 'studentów', 'studentów')}" + %small= t('.group_meta', role: group.role, students: t('.students', count: group.students_count)) - if group.new_purchases.positive? %span.gh-my-groups-list-item-alert - = "#{group.new_purchases} #{gh_plural(group.new_purchases, 'nowy zakup', 'nowe zakupy', 'nowych zakupów')}" + = t('.new_purchases', count: group.new_purchases) - else - %p.gh-small Nie prowadzisz jeszcze żadnej grupy. + %p.gh-small= t('.no_groups') = link_to new_story_group_path, class: 'gh-btn gh-btn--secondary' do %i.fa-solid.fa-plus - Utwórz grupę + = t('.create_group') diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml index fa4939ec..85a4f7a8 100644 --- a/app/views/home/index.html.haml +++ b/app/views/home/index.html.haml @@ -1,3 +1,3 @@ -- content_for :title, 'Start' +- content_for :title, t('.title') = render @dashboard diff --git a/app/views/invitation_mailer/invitation_email.html.haml b/app/views/invitation_mailer/invitation_email.html.haml index 822985ed..b23e11ec 100644 --- a/app/views/invitation_mailer/invitation_email.html.haml +++ b/app/views/invitation_mailer/invitation_email.html.haml @@ -1,3 +1,3 @@ = render 'shared/invitation_email', - lead: "Zostałeś dodany jako #{@role_label} organizacji #{@organization_name} w Gamification Hub.", + lead: t('.lead', role: @role_label, organization: @organization_name), token_link: @token_link diff --git a/app/views/invitation_mailer/invitation_email.text.haml b/app/views/invitation_mailer/invitation_email.text.haml index 822985ed..b23e11ec 100644 --- a/app/views/invitation_mailer/invitation_email.text.haml +++ b/app/views/invitation_mailer/invitation_email.text.haml @@ -1,3 +1,3 @@ = render 'shared/invitation_email', - lead: "Zostałeś dodany jako #{@role_label} organizacji #{@organization_name} w Gamification Hub.", + lead: t('.lead', role: @role_label, organization: @organization_name), token_link: @token_link diff --git a/app/views/items/_card.html.haml b/app/views/items/_card.html.haml index 9819a7e0..a360df4b 100644 --- a/app/views/items/_card.html.haml +++ b/app/views/items/_card.html.haml @@ -34,7 +34,7 @@ = offer.discount_label %span.gh-tag.gh-tag--no-lives{ hidden: !offer.zero_lives?, data: targets[:tag_life] } %i.fa-solid.fa-heart{ 'aria-hidden': 'true' } - Działa przy 0 życiach + = t('.zero_lives') %p.gh-rule-text{ class: ('gh-placeholder-text' if item.didactic_description.blank?), data: targets[:rules] } = item_rules(item) @@ -49,12 +49,12 @@ = buy - else %button.gh-btn{ type: 'button', tabindex: -1 } - %span{ data: targets[:buy_label] }= "Kup za #{price}" + %span{ data: targets[:buy_label] }= t('.buy', price: price) - if all_feet || state == :save .gh-requirement-block{ hidden: hide.call(:save), data: targets[:foot_save] } .gh-requirement-title - Zbierz jeszcze + = t('.collect_more') %b{ data: targets[:need] }= need .gh-progress-bar.gh-octagon-clip %i.gh-octagon-clip{ style: "--gh-p: #{(progress.clamp(0, 1) * 100).round}%", data: targets[:bar] } @@ -73,5 +73,5 @@ 'aria-valuemin': 0, 'aria-valuemax': rp.target, 'aria-valuenow': rp.collected, - 'aria-label': "Postęp do rangi #{rp.rank_name}" } + 'aria-label': t('.progress_label', rank: rp.rank_name) } %i.gh-octagon-clip{ style: "--gh-p: #{gh_percent(rp.collected, rp.target)}%" } diff --git a/app/views/items/_confirm_destroy.html.haml b/app/views/items/_confirm_destroy.html.haml index fee14152..32f7d23a 100644 --- a/app/views/items/_confirm_destroy.html.haml +++ b/app/views/items/_confirm_destroy.html.haml @@ -1,22 +1,18 @@ -%h2#gh-del-title.gh-h2= "Usunąć „#{item.name}” ze sklepu?" -%p - Przedmiot zniknie ze sklepu i z list. Studenci, którzy go kupili, zachowają - go, a w historii zostanie oznaczony jako „Usunięty z oferty”. +%h2#gh-del-title.gh-h2= t('.title', name: item.name) +%p= t('.body') - if bought.positive? %p.gh-warning-note{ role: 'alert' } %i.fa-solid.fa-triangle-exclamation{ 'aria-hidden': 'true' } - %span - = "#{bought} #{gh_plural(bought, 'student ma', 'studentów ma', 'studentów ma')} ten przedmiot" - i zachowa go po usunięciu. + %span= t('.owners', count: bought) .gh-dialog-button-row - if @in_modal %button.gh-btn.gh-btn--secondary{ type: 'button', data: { action: 'dialog#close' }, autofocus: true } - Anuluj + = t('shared.actions.cancel') - else - = link_to 'Anuluj', edit_story_group_item_path(@story_group, item), class: 'gh-btn gh-btn--secondary' + = link_to t('shared.actions.cancel'), edit_story_group_item_path(@story_group, item), class: 'gh-btn gh-btn--secondary' - = button_to 'Usuń z oferty', story_group_item_path(@story_group, item), + = button_to t('.remove'), story_group_item_path(@story_group, item), method: :delete, class: 'gh-btn', form: { data: { turbo_frame: (@in_modal ? 'modal' : nil) } } diff --git a/app/views/items/_form.html.haml b/app/views/items/_form.html.haml index 6ef08f40..359f13d7 100644 --- a/app/views/items/_form.html.haml +++ b/app/views/items/_form.html.haml @@ -7,12 +7,11 @@ %section.gh-panel.gh-page-header.gh-page-header--stacked %p.gh-breadcrumbs - = link_to 'Przedmioty', story_group_items_path(@story_group) + = link_to t('.breadcrumb'), story_group_items_path(@story_group) %i.fa-solid.fa-chevron-right{ 'aria-hidden': 'true' } - %span= editing ? (item.name_was.presence || item.name) : 'Nowy przedmiot' - %h1.gh-h1= editing ? 'Edytuj przedmiot' : 'Nowy przedmiot' - %p.gh-lead - = editing ? 'Podgląd obok pokazuje kartę po zmianach.' : 'Wypełnij kartę, a podgląd obok pokaże, jak zobaczą ją studenci.' + %span= editing ? (item.name_was.presence || item.name) : t('.new_title') + %h1.gh-h1= editing ? t('.edit_title') : t('.new_title') + %p.gh-lead= editing ? t('.edit_lead') : t('.new_lead') - form_data = { controller: 'item-form', action: 'gh:image-crop->item-form#art', @@ -21,7 +20,8 @@ item_form_badge_discount_value: card.badges_discount, item_form_badge_names_value: card.discount_badges.map(&:name).to_json, item_form_example_rank_order_value: example.rank_order.to_json, - item_form_example_badge_order_value: example.badge_order.to_json, } + item_form_example_badge_order_value: example.badge_order.to_json, + item_form_i18n_value: item_form_wording.to_json, } = form_with model: [@story_group, item], data: form_data do |f| @@ -31,19 +31,18 @@ %p.gh-info-banner %i.fa-solid.fa-users{ 'aria-hidden': 'true' } %span - %b - = "#{bought} #{gh_plural(bought, 'student ma', 'studentów ma', 'studentów ma')} ten przedmiot." - Zmiany ceny, wymagań i zniżek dotyczą tylko nowych zakupów. + %b= t('.owners', count: bought) + = t('.owners_note') .gh-form-section - %h2.gh-h3 Karta przedmiotu - %p.gh-field-hint Tak przedmiot wygląda w sklepie i w ekwipunku studenta. + %h2.gh-h3= t('.card') + %p.gh-field-hint= t('.card_hint') .gh-form-field - = f.label :name, 'Nazwa' + = f.label :name, t('shared.actions.name') .gh-text-input{ class: ('gh-text-input--invalid' if error.call(:name)) } = f.text_field :name, maxlength: 50, autocomplete: 'off', - placeholder: 'np. Poprawa wejściówki', + placeholder: t('.name_placeholder'), 'aria-invalid': (error.call(:name) ? 'true' : nil), 'aria-describedby': (error.call(:name) ? 'gh-item-name-error' : nil), data: { item_form_target: 'name', action: 'item-form#refresh' } @@ -53,10 +52,10 @@ %span= error.call(:name) .gh-form-field - = f.label :didactic_description, 'Co daje studentowi' + = f.label :didactic_description, t('.rules') .gh-text-input{ class: ('gh-text-input--invalid' if error.call(:didactic_description)) } = f.text_area :didactic_description, rows: 2, maxlength: 255, - placeholder: 'np. Możliwość ponownego napisania wejściówki', + placeholder: t('.rules_placeholder'), 'aria-invalid': (error.call(:didactic_description) ? 'true' : nil), 'aria-describedby': (error.call(:didactic_description) ? 'gh-item-rules-error' : nil), data: { item_form_target: 'rules', action: 'item-form#refresh' } @@ -64,15 +63,15 @@ %p#gh-item-rules-error.gh-field-error{ role: 'alert' } %i.fa-solid.fa-triangle-exclamation{ 'aria-hidden': 'true' } %span= error.call(:didactic_description) - %span.gh-field-hint Opis dydaktyczny: konkretna korzyść, bez fabuły. + %span.gh-field-hint= t('.rules_hint') .gh-form-field = f.label :story_description do - Opis fabularny - %small (opcjonalnie) + = t('.story') + %small= t('shared.actions.optional') .gh-text-input{ class: ('gh-text-input--invalid' if error.call(:story_description)) } = f.text_area :story_description, rows: 2, maxlength: 255, - placeholder: 'np. Awaryjny powrót statku do bazy', + placeholder: t('.story_placeholder'), 'aria-invalid': (error.call(:story_description) ? 'true' : nil), 'aria-describedby': (error.call(:story_description) ? 'gh-item-story-error' : nil), data: { item_form_target: 'story', action: 'item-form#refresh' } @@ -86,10 +85,10 @@ refresh_action: 'item-form#refresh' .gh-form-section - %h2.gh-h3 Cena + %h2.gh-h3= t('.price_section') .gh-form-field - = f.label :price, 'Cena' + = f.label :price, t('.price') .gh-price-field-row .gh-text-input.gh-text-input--numeric{ class: ('gh-text-input--invalid' if error.call(:price)) } = f.number_field :price, min: 1, step: 1, @@ -109,57 +108,53 @@ 'aria-labelledby': 'gh-item-zero-label', data: { item_form_target: 'zero', action: 'change->item-form#refresh' } %span - %b#gh-item-zero-label Można kupić przy 0 życiach - %span.gh-field-hint Przydaje się dla przedmiotów, które przywracają życie. + %b#gh-item-zero-label= t('.zero_lives') + %span.gh-field-hint= t('.zero_lives_hint') .gh-form-section - %h2.gh-h3 Kto może kupić - %p.gh-field-hint Bez wymagań przedmiot jest dostępny dla wszystkich. + %h2.gh-h3= t('.who_section') + %p.gh-field-hint= t('.who_hint') .gh-sentence-row - %span Kupić może + %span= t('.who') %span.gh-select-input = f.select :unlock_rank_id, item_unlock_rank_options(shelf.ranks, item.unlock_rank_id), {}, - 'aria-label': 'Kto może kupić', + 'aria-label': t('.who_label'), data: { item_form_target: 'unlockRank', action: 'item-form#refresh' } %i.fa-solid.fa-chevron-down{ 'aria-hidden': 'true' } .gh-sentence-row - %span Wymagane odznaki + %span= t('.required_badges') - if badges.any? = render 'badge_chips', f: f, field: :unlock_badge_ids, badges: badges, - legend: 'Wymagane odznaki', placeholder: 'Dodaj odznakę…', + legend: t('.required_badges'), placeholder: t('.add_badge'), form_target: 'unlockBadge' - else - %span.gh-field-hint W tej grupie nie ma jeszcze odznak. + %span.gh-field-hint= t('.no_badges') %p.gh-explainer-note{ data: { item_form_target: 'unlockText' } }= item_unlock_sentence(item) .gh-form-section - %h2.gh-h3 Zniżki - %p.gh-field-hint - Zniżka z rangi to procent przypisany do rangi studenta. Zniżki z odznak - się sumują. Wystarczy spełnić jeden z warunków, żeby zniżka zadziałała - — bez warunków dostaje ją każdy kupujący. Wysokość liczy się zawsze - z rangi i wszystkich odznak studenta, także tych niewybranych tutaj. + %h2.gh-h3= t('.discounts_section') + %p.gh-field-hint= t('.discounts_hint') .gh-sentence-row - %span Zniżka dla rang + %span= t('.rank_discount') %span.gh-select-input = f.select :min_rank_for_discount_id, item_discount_rank_options(shelf.ranks, item.min_rank_for_discount_id), {}, - 'aria-label': 'Zniżka dla rang', + 'aria-label': t('.rank_discount'), data: { item_form_target: 'discountRank', action: 'item-form#refresh' } %i.fa-solid.fa-chevron-down{ 'aria-hidden': 'true' } .gh-sentence-row - %span Zniżka za odznaki + %span= t('.badge_discount') - if badges.any? = render 'badge_chips', f: f, field: :discount_badge_ids, badges: badges, - legend: 'Odznaki ze zniżką', placeholder: 'Dodaj odznakę ze zniżką…', + legend: t('.discount_badges'), placeholder: t('.add_discount_badge'), form_target: 'discountBadge', show_discount: true - else - %span.gh-field-hint W tej grupie nie ma jeszcze odznak. + %span.gh-field-hint= t('.no_badges') .gh-explainer-note %p{ data: { item_form_target: 'discountText' } }= item_discount_sentence(item, card, example) @@ -173,26 +168,24 @@ - if editing .gh-form-section - %h2.gh-h3 Usuwanie - %p.gh-field-hint - Przedmiot zniknie ze sklepu i z list. Studenci, którzy go kupili, - nadal go mają, a w ich historii zostanie jako „Usunięty z oferty”. + %h2.gh-h3= t('shared.actions.removal') + %p.gh-field-hint= t('.removal_hint') .gh-button-row = link_to confirm_destroy_story_group_item_path(@story_group, item), class: 'gh-btn gh-btn--secondary', data: { turbo_frame: 'modal' } do %i.fa-solid.fa-trash{ 'aria-hidden': 'true' } - Usuń przedmiot + = t('.remove') - %aside.gh-preview-column{ 'aria-label': 'Podgląd' } + %aside.gh-preview-column{ 'aria-label': t('shared.actions.preview') } = render 'preview', item: item, card: card %template{ data: { item_form_target: 'reqRank' } } %li - Wymaga rangi + = t('items.labels.requires_rank') %b %template{ data: { item_form_target: 'reqBadge' } } %li - Wymaga odznaki + = t('items.labels.requires_badge') %b %template{ data: { item_form_target: 'warnTemplate' } } %p.gh-warning-note--alt @@ -200,9 +193,9 @@ %span .gh-panel.gh-form-actions-bar - = link_to 'Anuluj', story_group_items_path(@story_group), class: 'gh-link-button' + = link_to t('shared.actions.cancel'), story_group_items_path(@story_group), class: 'gh-link-button' %span.gh-form-actions-spacer %button.gh-btn{ type: 'submit' } - unless editing %i.fa-solid.fa-plus{ 'aria-hidden': 'true' } - = editing ? 'Zapisz zmiany' : 'Dodaj przedmiot' + = editing ? t('shared.actions.save_changes') : t('.create') diff --git a/app/views/items/_grid_card.html.haml b/app/views/items/_grid_card.html.haml index f634b195..940aec9c 100644 --- a/app/views/items/_grid_card.html.haml +++ b/app/views/items/_grid_card.html.haml @@ -16,11 +16,11 @@ %i.fa-solid.fa-lock{ 'aria-hidden': 'true' } = label - if card.discounts? - %span.gh-tag.gh-tag--discount Zniżka + %span.gh-tag.gh-tag--discount= t('.discount') - if card.zero_lives? %span.gh-tag.gh-tag--no-lives %i.fa-solid.fa-heart{ 'aria-hidden': 'true' } - Przy 0 życiach + = t('.zero_lives') %p.gh-rule-text= item.didactic_description - if item.story_description.present? @@ -30,5 +30,5 @@ %span.gh-card-meta= item_bought_line(bought) .gh-card-actions = link_to edit_story_group_item_path(@story_group, item), - class: 'gh-icon-button', 'aria-label': "Edytuj przedmiot #{item.name}" do + class: 'gh-icon-button', 'aria-label': t('.edit_label', name: item.name) do %i.fa-solid.fa-pen{ 'aria-hidden': 'true' } diff --git a/app/views/items/_preview.html.haml b/app/views/items/_preview.html.haml index 5df08c5b..f27b4f71 100644 --- a/app/views/items/_preview.html.haml +++ b/app/views/items/_preview.html.haml @@ -21,36 +21,36 @@ %details.gh-preview-details{ open: true } %summary - Podgląd karty + = t('.summary') %i.fa-solid.fa-chevron-down{ 'aria-hidden': 'true' } %section.gh-panel.gh-preview-panel - %h3 Tak zobaczą to studenci + %h3= t('.students_view') - .gh-segmented-toggle-2.gh-octagon-clip{ role: 'group', 'aria-label': 'Stan karty' } + .gh-segmented-toggle-2.gh-octagon-clip{ role: 'group', 'aria-label': t('.state') } %button{ type: 'button', 'aria-pressed': 'true', value: 'afford', data: { action: 'item-form#state', item_form_target: 'state' } } - Dostępny + = t('.afford') %button{ type: 'button', 'aria-pressed': 'false', value: 'save', data: { action: 'item-form#state', item_form_target: 'state' } } - Zbierasz + = t('.save') %button{ type: 'button', 'aria-pressed': 'false', value: 'sealed', disabled: !card.requires?, data: { action: 'item-form#state', item_form_target: 'state' } } - Zapieczętowany + = t('.sealed') %span.gh-field-hint{ hidden: card.requires?, data: { item_form_target: 'sealHint' } } - Bez wymagań przedmiot nigdy nie będzie zapieczętowany. + = t('.seal_hint') .gh-item-preview-card = render 'card', item: item, card: card, state: :afford, need: need, progress: 0.6, targets: targets, all_feet: true %section.gh-panel.gh-preview-panel - %h3 Miniatura w powiadomieniach + %h3= t('.thumb') .gh-preview-thumb-row = render 'layouts/item_thumb', item: item, inner_data: { item_form_target: 'thumbArt' } %span %b{ data: { item_form_target: 'thumbName' } }= item_name(item) - %span.gh-small Zakup studenta, dziś 10:21 + %span.gh-small= t('.thumb_sample') diff --git a/app/views/items/confirm_destroy.html.haml b/app/views/items/confirm_destroy.html.haml index 372f916c..94c28587 100644 --- a/app/views/items/confirm_destroy.html.haml +++ b/app/views/items/confirm_destroy.html.haml @@ -1,4 +1,4 @@ -- content_for :title, "Usuń przedmiot #{@item.name}" +- content_for :title, t('.page_title', name: @item.name) - if @in_modal = turbo_frame_tag 'modal' do diff --git a/app/views/items/edit.html.haml b/app/views/items/edit.html.haml index 65871598..9643cc11 100644 --- a/app/views/items/edit.html.haml +++ b/app/views/items/edit.html.haml @@ -1,3 +1,3 @@ -- content_for :title, "Edytuj przedmiot - #{@item.name}" +- content_for :title, t('.page_title', name: @item.name) = render 'form', item: @item, shelf: @shelf diff --git a/app/views/items/index.html.haml b/app/views/items/index.html.haml index ca458ea8..1743a1cc 100644 --- a/app/views/items/index.html.haml +++ b/app/views/items/index.html.haml @@ -1,16 +1,14 @@ -- content_for :title, 'Przedmioty' +- content_for :title, t('.title') %section.gh-panel.gh-page-header .gh-page-header-text - %h1.gh-h1 Przedmioty - %p.gh-lead - = "#{@shelf.size} #{gh_plural(@shelf.size, 'przedmiot', 'przedmioty', 'przedmiotów')} w sklepie," - od najtańszego. + %h1.gh-h1= t('.title') + %p.gh-lead= t('.lead', count: @shelf.size) .gh-button-row = link_to new_story_group_item_path(@story_group), class: 'gh-btn' do %i.fa-solid.fa-plus - Nowy przedmiot + = t('.new') - if @shelf.any? .gh-badge-grid--teacher @@ -18,7 +16,5 @@ = render 'grid_card', item: slot.item, card: @shelf.card_for(slot.item), bought: slot.bought - else %section.gh-panel.gh-empty-state - %h2.gh-h2 Sklep jest jeszcze pusty - %p.gh-lead - Dodaj przedmioty, żeby zebrana waluta miała na co iść. Każdy może mieć - wymagania i zniżki, więc sklep otwiera się stopniowo wraz z postępami. + %h2.gh-h2= t('.empty_title') + %p.gh-lead= t('.empty_body') diff --git a/app/views/items/new.html.haml b/app/views/items/new.html.haml index 48413ad2..e439c97d 100644 --- a/app/views/items/new.html.haml +++ b/app/views/items/new.html.haml @@ -1,3 +1,3 @@ -- content_for :title, 'Nowy przedmiot' +- content_for :title, t('.page_title') = render 'form', item: @item, shelf: @shelf diff --git a/app/views/join/_cancel.html.haml b/app/views/join/_cancel.html.haml index 0b46a3f1..b9e318c3 100644 --- a/app/views/join/_cancel.html.haml +++ b/app/views/join/_cancel.html.haml @@ -1,4 +1,4 @@ - if @in_modal - %button.gh-btn.gh-btn--secondary{ type: 'button', data: { action: 'dialog#close' } } Anuluj + %button.gh-btn.gh-btn--secondary{ type: 'button', data: { action: 'dialog#close' } }= t('shared.actions.cancel') - else - = link_to 'Anuluj', home_path, class: 'gh-btn gh-btn--secondary' + = link_to t('shared.actions.cancel'), home_path, class: 'gh-btn gh-btn--secondary' diff --git a/app/views/join/_step_code.html.haml b/app/views/join/_step_code.html.haml index 66147941..b7444d38 100644 --- a/app/views/join/_step_code.html.haml +++ b/app/views/join/_step_code.html.haml @@ -1,9 +1,9 @@ - error = local_assigns[:lookup]&.error - member_of = local_assigns[:lookup]&.member_of -%p.gh-small.mb-1 Krok 1 z 2 -%h2.gh-h2#gh-join-title Dołącz do grupy -%p Wpisz 6-znakowy kod, który podał prowadzący. +%p.gh-small.mb-1= t('.step') +%h2.gh-h2#gh-join-title= t('.title') +%p= t('.hint') = form_with url: lookup_join_index_path, method: :get, @@ -11,7 +11,7 @@ turbo_frame: (@in_modal ? 'modal' : nil), } do = hidden_field_tag :code, code, data: { join_code_target: 'code' } - .gh-join-code-inputs{ role: 'group', 'aria-label': 'Kod zaproszenia' } + .gh-join-code-inputs{ role: 'group', 'aria-label': t('.code_label') } - StoryGroupInvite::CODE_LENGTH.times do |index| %span.gh-join-code-slot %input{type: 'text', @@ -21,7 +21,7 @@ spellcheck: 'false', value: code.to_s[index], autofocus: index.zero?, - 'aria-label': "Znak #{index + 1} z #{StoryGroupInvite::CODE_LENGTH}", + 'aria-label': t('.slot_label', index: index + 1, total: StoryGroupInvite::CODE_LENGTH), data: { join_code_target: 'slot', action: 'input->join-code#type keydown->join-code#key paste->join-code#paste', },} @@ -29,8 +29,8 @@ %p.gh-field-error.pb-2{ role: 'alert' } %i.fa-solid.fa-triangle-exclamation.fa-fw.gh-join-icon{ 'aria-hidden': 'true' } %span - Już należysz do tej grupy. - = link_to 'Otwórz grupę', + = t('.member') + = link_to t('.open_group'), story_group_path(member_of), class: 'gh-link-button', data: { turbo_frame: '_top' } @@ -41,8 +41,8 @@ %p.gh-small.gh-join-hint %i.fa-solid.fa-camera.fa-fw.gh-join-icon{ 'aria-hidden': 'true' } - Masz kod QR? Zeskanuj go aparatem telefonu, a kod wpisze się sam. + = t('.qr_hint') .gh-dialog-button-row = render 'join/cancel' - %button.gh-btn{ type: 'submit', data: { join_code_target: 'submit' } } Dalej + %button.gh-btn{ type: 'submit', data: { join_code_target: 'submit' } }= t('shared.actions.next') diff --git a/app/views/join/_step_done.html.haml b/app/views/join/_step_done.html.haml index e69a9b56..15cd902b 100644 --- a/app/views/join/_step_done.html.haml +++ b/app/views/join/_step_done.html.haml @@ -1,14 +1,12 @@ %span.gh-card-art.gh-joined-group-art{ 'aria-hidden': 'true' } = gh_group_cover(story_group) || gh_monogram(story_group.name) -%h2#gh-join-title.gh-h2= "Dołączono do grupy #{story_group.name}" +%h2#gh-join-title.gh-h2= t('.title', name: story_group.name) %p - = "Prowadzi #{story_group.owner.full_name}." - = succeed '.' do - Jesteś tam jako - %b= membership.display_name - = "Walutą w tej grupie są #{story_group.currency_name}." + = t('.led_by', name: story_group.owner.full_name) + = t('.you_are_html', name: tag.b(membership.display_name)) + = t('.currency', currency: story_group.currency_name) .gh-dialog-button-row - = link_to 'Gotowe', story_group_path(story_group), class: 'gh-btn', + = link_to t('.done'), story_group_path(story_group), class: 'gh-btn', data: { turbo_frame: '_top' } diff --git a/app/views/join/_step_nickname.html.haml b/app/views/join/_step_nickname.html.haml index 1e7c12df..8783aefa 100644 --- a/app/views/join/_step_nickname.html.haml +++ b/app/views/join/_step_nickname.html.haml @@ -1,10 +1,8 @@ - nickname_error = local_assigns[:result]&.nickname_error -%p.gh-small.mb-1 Krok 2 z 2 -%h2.gh-h2#gh-join-title= "Dołączasz do grupy #{story_group.name}" -%p - W tej grupie inni zobaczą Cię pod pseudonimem — także w rankingu. - Możesz go później zmienić w ustawieniach w grupie. +%p.gh-small.mb-1= t('.step') +%h2.gh-h2#gh-join-title= t('.title', name: story_group.name) +%p= t('.hint') = form_with url: join_index_path, method: :post, @@ -12,7 +10,7 @@ = hidden_field_tag :code, code .gh-form-field - = label_tag :nickname, 'Pseudonim w tej grupie' + = label_tag :nickname, t('.nickname') .gh-text-input{ class: ('gh-text-input--invalid' if nickname_error) } = text_field_tag :nickname, params[:nickname], maxlength: 24, @@ -27,11 +25,9 @@ %i.fa-solid.fa-triangle-exclamation{ 'aria-hidden': 'true' } %span= nickname_error - else - %span.gh-field-hint#gh-nick-note - Zostaw puste, jeśli chcesz używać imienia i nazwiska. - Pseudonim możesz ustawić później. + %span.gh-field-hint#gh-nick-note= t('.nickname_hint') -# haml-lint:enable RepeatedId .gh-dialog-button-row = render 'join/cancel' - %button.gh-btn{ type: 'submit' } Dołącz do grupy + %button.gh-btn{ type: 'submit' }= t('.join') diff --git a/app/views/join/create.html.haml b/app/views/join/create.html.haml index bacca5c6..4473eaa3 100644 --- a/app/views/join/create.html.haml +++ b/app/views/join/create.html.haml @@ -1,4 +1,4 @@ -- content_for :title, 'Dołączono do grupy' +- content_for :title, t('.page_title') - if @in_modal = turbo_frame_tag 'modal' do diff --git a/app/views/join/new.html.haml b/app/views/join/new.html.haml index 83337bfe..4ccfc3c4 100644 --- a/app/views/join/new.html.haml +++ b/app/views/join/new.html.haml @@ -1,4 +1,4 @@ -- content_for :title, 'Dołącz do grupy' +- content_for :title, t('.page_title') - if @in_modal = turbo_frame_tag 'modal' do diff --git a/app/views/join/show.html.haml b/app/views/join/show.html.haml index b212209f..a684f32a 100644 --- a/app/views/join/show.html.haml +++ b/app/views/join/show.html.haml @@ -1,4 +1,4 @@ -- content_for :title, 'Dołącz do grupy' +- content_for :title, t('.page_title') - if @in_modal = turbo_frame_tag 'modal' do diff --git a/app/views/layouts/_account_menu.html.haml b/app/views/layouts/_account_menu.html.haml index a3519de2..3dd1e843 100644 --- a/app/views/layouts/_account_menu.html.haml +++ b/app/views/layouts/_account_menu.html.haml @@ -1,5 +1,5 @@ %dialog#gh-account-menu.gh-dialog.gh-dialog--menu.gh-dialog--anchored{ data: { action: 'click->menu#closeOnBackdrop' }, - 'aria-label': 'Konto' } + 'aria-label': t('.label') } .gh-dialog-body.gh-octagon-clip %span.gh-sheet-drag-handle{ 'aria-hidden': 'true' } @@ -14,14 +14,16 @@ %li %button{ type: 'button' } %i.fa-solid.fa-gear - %span Ustawienia konta + %span= t('.account_settings') %li %button{ type: 'button', data: { action: 'theme#toggle' } } %i.fa-solid.fa-moon.gh-theme-icon-moon %i.fa-solid.fa-sun.gh-theme-icon-sun %span{ data: { theme_target: 'label' } } - = gh_theme == 'dark' ? 'Jasny motyw' : 'Ciemny motyw' + = gh_theme == 'dark' ? t('layouts.theme.light') : t('layouts.theme.dark') + %li + = render 'layouts/locale_switcher', menu: true %li = link_to logout_path, data: { turbo_method: :delete } do %i.fa-solid.fa-arrow-right-from-bracket - %span Wyloguj się + %span= t('.logout') diff --git a/app/views/layouts/_deck.html.haml b/app/views/layouts/_deck.html.haml index db4513b2..6ff9de4f 100644 --- a/app/views/layouts/_deck.html.haml +++ b/app/views/layouts/_deck.html.haml @@ -1,6 +1,6 @@ - story_group = chrome.story_group -%section.gh-current-group-card{ 'aria-label': "Grupa #{chrome.name}" } +%section.gh-current-group-card{ 'aria-label': t('.group', name: chrome.name) } .gh-current-group-card-frame.gh-octagon-clip .gh-current-group-card-inner.gh-octagon-clip - cover = gh_group_cover(story_group) @@ -12,13 +12,13 @@ %p.gh-current-group-card-role= chrome.role_label %button.gh-link-button.gh-current-group-card-switch-trigger{ type: 'button', - title: 'Zmień grupę', + title: t('.switch'), data: { action: 'menu#open', menu_id_param: 'gh-group-switcher' } } %i.fa-solid.fa-right-left{ 'aria-hidden': 'true' } - %span Zmień grupę + %span= t('.switch') - %nav.gh-sidebar-nav{ 'aria-label': 'Sekcje grupy' } + %nav.gh-sidebar-nav{ 'aria-label': t('.sections') } - chrome.items.each do |item| - on = item.current?(current_path) = link_to item.path, class: "gh-sidebar-link #{'gh-sidebar-link--active' if on}", diff --git a/app/views/layouts/_flash.html.haml b/app/views/layouts/_flash.html.haml index 41b8df1d..074bc251 100644 --- a/app/views/layouts/_flash.html.haml +++ b/app/views/layouts/_flash.html.haml @@ -9,5 +9,5 @@ %button{ class: 'gh-close absolute right-1 top-1/2 -translate-y-1/2', type: 'button', data: { action: 'flash#dismiss' }, - 'aria-label': 'Zamknij' } + 'aria-label': t('shared.actions.close') } %i.fa-solid.fa-xmark diff --git a/app/views/layouts/_group_switcher.html.haml b/app/views/layouts/_group_switcher.html.haml index 628237b2..c77dd40c 100644 --- a/app/views/layouts/_group_switcher.html.haml +++ b/app/views/layouts/_group_switcher.html.haml @@ -6,7 +6,7 @@ .gh-dialog-body.gh-octagon-clip %span.gh-sheet-drag-handle{ 'aria-hidden': 'true' } .gh-popover-header - %h2#gh-switch-title.gh-h3 Zmień grupę + %h2#gh-switch-title.gh-h3= t('.title') %ul.gh-menu-list - groups.each do |story_group| @@ -16,10 +16,10 @@ = render 'layouts/group_thumb', story_group: story_group, size: nil %span= story_group.name - if here - %span.gh-current-badge Tu jesteś + %span.gh-current-badge= t('.here') .gh-popover-footer = link_to new_join_path, class: 'gh-btn gh-btn--secondary', data: { turbo_frame: 'modal' } do %i.fa-solid.fa-plus - Dołącz do grupy + = t('.join') diff --git a/app/views/layouts/_locale_switcher.html.haml b/app/views/layouts/_locale_switcher.html.haml new file mode 100644 index 00000000..1348de1e --- /dev/null +++ b/app/views/layouts/_locale_switcher.html.haml @@ -0,0 +1,8 @@ +- other_locale = (I18n.available_locales - [I18n.locale]).first +- menu = local_assigns.fetch(:menu, false) + += button_to locale_path, method: :patch, params: { locale: other_locale }, + class: (menu ? nil : 'gh-link-button'), lang: other_locale, 'aria-label': t('.label') do + - if menu + %i.fa-solid.fa-language{ 'aria-hidden': 'true' } + %span= t('.switch_to') diff --git a/app/views/layouts/_more_sheet.html.haml b/app/views/layouts/_more_sheet.html.haml index e5a4e2bc..a5c26eda 100644 --- a/app/views/layouts/_more_sheet.html.haml +++ b/app/views/layouts/_more_sheet.html.haml @@ -2,7 +2,7 @@ 'aria-labelledby': 'gh-more-title' } .gh-dialog-body.gh-octagon-clip %span.gh-sheet-drag-handle{ 'aria-hidden': 'true' } - %h2#gh-more-title.gh-h2 Więcej + %h2#gh-more-title.gh-h2= t('shared.actions.more') - if chrome %ul.gh-menu-list.gh-menu-list--nav @@ -16,24 +16,24 @@ %li = link_to new_story_group_path do %i.fa-solid.fa-plus - %span Utwórz grupę + %span= t('.create_group') - %p.gh-popover-section-label Konto + %p.gh-popover-section-label= t('.account') %ul.gh-menu-list %li %button{ type: 'button' } %i.fa-solid.fa-gear - %span Ustawienia konta + %span= t('.account_settings') %li %button{ type: 'button', data: { action: 'theme#toggle' } } %i.fa-solid.fa-moon.gh-theme-icon-moon %i.fa-solid.fa-sun.gh-theme-icon-sun %span{ data: { theme_target: 'label' } } - = gh_theme == 'dark' ? 'Jasny motyw' : 'Ciemny motyw' + = gh_theme == 'dark' ? t('layouts.theme.light') : t('layouts.theme.dark') %li = link_to logout_path, data: { turbo_method: :delete } do %i.fa-solid.fa-arrow-right-from-bracket - %span Wyloguj się + %span= t('.logout') .gh-button-stack - %button.gh-btn.gh-btn--secondary{ type: 'button', data: { action: 'menu#close' } } Zamknij + %button.gh-btn.gh-btn--secondary{ type: 'button', data: { action: 'menu#close' } }= t('shared.actions.close') diff --git a/app/views/layouts/_theme_toggle.html.haml b/app/views/layouts/_theme_toggle.html.haml index f24023b2..8847f12e 100644 --- a/app/views/layouts/_theme_toggle.html.haml +++ b/app/views/layouts/_theme_toggle.html.haml @@ -1,10 +1,11 @@ .gh-auth-footer.relative.z-10{ data: { controller: 'theme', - theme_light_label_value: 'Jasny motyw', - theme_dark_label_value: 'Ciemny motyw' } } + theme_light_label_value: t('layouts.theme.light'), + theme_dark_label_value: t('layouts.theme.dark') } } %button.gh-link-button{ type: 'button', data: { action: 'theme#toggle', theme_target: 'label' }, - 'aria-label': 'Przełącz motyw' } - = gh_theme == 'dark' ? 'Jasny motyw' : 'Ciemny motyw' + 'aria-label': t('layouts.theme.toggle') } + = gh_theme == 'dark' ? t('layouts.theme.light') : t('layouts.theme.dark') + = render 'layouts/locale_switcher' %span - Pomoc: - %button.gh-link-button{ type: 'button' } kontakt + = t('.help') + %button.gh-link-button{ type: 'button' }= t('.contact') diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index c234997d..dc019e8f 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -1,5 +1,5 @@ !!! -%html{ lang: 'pl', data: { gh_theme: gh_theme } } +%html{ lang: I18n.locale, data: { gh_theme: gh_theme } } %head %meta{ charset: 'utf-8' }/ %title= content_for(:title) || 'Gamification Hub' @@ -29,9 +29,11 @@ = yield = render 'layouts/theme_toggle' - else - .gh-app-shell{data: { controller: 'theme menu sidebar', - theme_light_label_value: 'Jasny motyw', - theme_dark_label_value: 'Ciemny motyw', }} + .gh-app-shell{data: { controller: 'theme menu sidebar', + theme_light_label_value: t('layouts.theme.light'), + theme_dark_label_value: t('layouts.theme.dark'), + sidebar_expand_label_value: t('layouts.sidebar.expand'), + sidebar_collapse_label_value: t('layouts.sidebar.collapse'), }} = render Shared::HeaderComponent.new(user: @current_user, group_chrome: gh_group_chrome,) diff --git a/app/views/layouts/mailer.html.haml b/app/views/layouts/mailer.html.haml index 0ec6e9a4..4f8ee924 100644 --- a/app/views/layouts/mailer.html.haml +++ b/app/views/layouts/mailer.html.haml @@ -1,5 +1,5 @@ !!! -%html +%html{ lang: I18n.locale } %head %meta{content: "text/html; charset=utf-8", "http-equiv" => "Content-Type"}/ :css diff --git a/app/views/notifications/_notification.html.haml b/app/views/notifications/_notification.html.haml index 9e194ea1..17e931a2 100644 --- a/app/views/notifications/_notification.html.haml +++ b/app/views/notifications/_notification.html.haml @@ -8,7 +8,7 @@ = render 'layouts/item_thumb', item: notification.item %span.gh-notification-row-text %b= student.full_name - %span= "Zakup: #{notification.item.name}" - %small= "#{story_group.name}, #{gh_when(notification.created_at)}" + %span= t('.purchase', name: notification.item.name) + %small= t('.meta', group: story_group.name, when: gh_when(notification.created_at)) - if unread - %span.gh-notification-unread-dot.gh-octagon-clip{ 'aria-label': 'nowe' } + %span.gh-notification-unread-dot.gh-octagon-clip{ 'aria-label': t('.unread') } diff --git a/app/views/notifications/_panel.html.haml b/app/views/notifications/_panel.html.haml index 93ec495d..201c1948 100644 --- a/app/views/notifications/_panel.html.haml +++ b/app/views/notifications/_panel.html.haml @@ -1,19 +1,19 @@ .gh-popover-header - %h2.gh-h3 Powiadomienia + %h2.gh-h3= t('.title') - if unread.any? = button_to mark_as_read_notifications_path, class: 'gh-link-button' do %i.fa-solid.fa-check-double - Oznacz wszystkie + = t('.mark_all') - if notifications.any? - if unread.any? - %p.gh-popover-section-label Nowe + %p.gh-popover-section-label= t('.new') %ul.gh-notification-list = render partial: 'notifications/notification', collection: unread, as: :notification - if read.any? - %p.gh-popover-section-label Wcześniej + %p.gh-popover-section-label= t('.earlier') %ul.gh-notification-list = render partial: 'notifications/notification', collection: read, as: :notification - else - %p.gh-small Brak powiadomień. + %p.gh-small= t('.empty') diff --git a/app/views/organizations/_admin_form.html.haml b/app/views/organizations/_admin_form.html.haml index c6a407fa..ab817c9f 100644 --- a/app/views/organizations/_admin_form.html.haml +++ b/app/views/organizations/_admin_form.html.haml @@ -1,17 +1,15 @@ = form_with model: user, url: add_admin_organization_path(@organization), data: { turbo_frame: (@in_modal ? 'modal' : nil) } do |form| - %h2.gh-h2 Dodaj administratora - %p - Wyślemy zaproszenie na podany adres. Po pierwszym logowaniu administrator - uzupełni swoje dane. + %h2.gh-h2= t('.title') + %p= t('.hint') - = render 'organizations/field', form: form, record: user, attribute: :email, label: 'Adres e-mail', + = render 'organizations/field', form: form, record: user, attribute: :email, label: t('.email'), input: :email_field, - options: { placeholder: 'jan.nowak@example.com', autofocus: true } + options: { placeholder: t('.email_placeholder'), autofocus: true } - if (error = user.errors[:organization].first) %p.gh-field-error{ role: 'alert' } %i.fa-solid.fa-triangle-exclamation{ 'aria-hidden': 'true' } %span= error = render 'organizations/dialog_actions', cancel_path: organization_path(@organization), - submit_label: 'Wyślij zaproszenie' + submit_label: t('.submit') diff --git a/app/views/organizations/_admins.html.haml b/app/views/organizations/_admins.html.haml index 08da384c..e2c602f7 100644 --- a/app/views/organizations/_admins.html.haml +++ b/app/views/organizations/_admins.html.haml @@ -1,5 +1,5 @@ %section.gh-panel.gh-organizations-panel - %h2.gh-h3.gh-organizations-panel-title Administratorzy + %h2.gh-h3.gh-organizations-panel-title= t('.title') - if admins.any? - admins.each do |admin| @@ -7,9 +7,9 @@ .gh-organization-member-row{ id: dom_id(admin, :admin) } = render 'organizations/person', user: admin %span.gh-teacher-role - Administrator + = t('.role') - if admin.first_login? - %small Zaproszenie wysłane + %small= t('.invited') - if manage_admins %span %span.gh-organization-member-actions @@ -18,9 +18,9 @@ name: name = link_to confirm_destroy_organization_admin_path(admin.organization, admin), class: 'gh-icon-button', - title: 'Usuń', - 'aria-label': "Usuń: #{name}", + title: t('.remove'), + 'aria-label': t('.remove_label', name: name), data: { turbo_frame: 'modal' } do %i.fa-solid.fa-trash{ 'aria-hidden': 'true' } - else - %p.gh-explainer-note Organizacja nie ma jeszcze administratora. + %p.gh-explainer-note= t('.none') diff --git a/app/views/organizations/_confirm_destroy.html.haml b/app/views/organizations/_confirm_destroy.html.haml index b3745e47..c9b6aff2 100644 --- a/app/views/organizations/_confirm_destroy.html.haml +++ b/app/views/organizations/_confirm_destroy.html.haml @@ -1,9 +1,9 @@ - members = organization.users.count -%h2.gh-h2 Usunąć organizację na zawsze? +%h2.gh-h2= t('.title') %p - = "#{members} #{gh_plural(members, 'konto zostanie usunięte', 'konta zostaną usunięte', 'kont zostanie usuniętych')}" - razem z organizacją, w tym konta administratorów. + = t('.accounts', count: members) + = t('.accounts_tail') = form_with url: organization_path(organization), method: :delete, data: { controller: 'confirm-name', @@ -12,7 +12,7 @@ .gh-form-field = label_tag :confirm do - Wpisz nazwę organizacji, żeby potwierdzić: + = t('.confirm_label') %b= organization.name .gh-text-input{ class: ('gh-text-input--invalid' if @confirm_failed) } = text_field_tag :confirm, nil, autocomplete: 'off', autofocus: true, @@ -22,14 +22,14 @@ - if @confirm_failed %p#gh-del-organization-error.gh-field-error{ role: 'alert' } %i.fa-solid.fa-triangle-exclamation{ 'aria-hidden': 'true' } - %span Wpisana nazwa nie zgadza się z nazwą organizacji. Organizacja nie została usunięta. + %span= t('.mismatch') .gh-dialog-button-row - if @in_modal - %button.gh-btn.gh-btn--secondary{ type: 'button', data: { action: 'dialog#close' } } Anuluj + %button.gh-btn.gh-btn--secondary{ type: 'button', data: { action: 'dialog#close' } }= t('organizations.dialog_actions.cancel') - else - = link_to 'Anuluj', organization_path(organization), class: 'gh-btn gh-btn--secondary' + = link_to t('organizations.dialog_actions.cancel'), organization_path(organization), class: 'gh-btn gh-btn--secondary' %button.gh-btn{ type: 'submit', disabled: true, data: { confirm_name_target: 'submit' } } - Usuń organizację + = t('.remove') diff --git a/app/views/organizations/_dialog_actions.html.haml b/app/views/organizations/_dialog_actions.html.haml index f12a7058..73739c52 100644 --- a/app/views/organizations/_dialog_actions.html.haml +++ b/app/views/organizations/_dialog_actions.html.haml @@ -2,7 +2,7 @@ -# back to `cancel_path`. .gh-dialog-button-row - if @in_modal - %button.gh-btn.gh-btn--secondary{ type: 'button', data: { action: 'dialog#close' } } Anuluj + %button.gh-btn.gh-btn--secondary{ type: 'button', data: { action: 'dialog#close' } }= t('.cancel') - else - = link_to 'Anuluj', cancel_path, class: 'gh-btn gh-btn--secondary' + = link_to t('.cancel'), cancel_path, class: 'gh-btn gh-btn--secondary' %button.gh-btn{ type: 'submit' }= submit_label diff --git a/app/views/organizations/_form.html.haml b/app/views/organizations/_form.html.haml index 755772c5..d1c0a125 100644 --- a/app/views/organizations/_form.html.haml +++ b/app/views/organizations/_form.html.haml @@ -1,19 +1,17 @@ - editing = organization.persisted? = form_with model: organization, data: { turbo_frame: (@in_modal ? 'modal' : nil) } do |form| - %h2.gh-h2= editing ? 'Edytuj organizację' : 'Nowa organizacja' + %h2.gh-h2= editing ? t('.edit_title') : t('.new_title') - if organization.usos? - %p - To organizacja powiązana z USOS: trafia do niej każdy, kto zaloguje się - przez USOS. Nie ma limitu członków i nie można jej usunąć. + %p= t('.usos_note') - = render 'organizations/field', form: form, record: organization, attribute: :name, label: 'Nazwa', + = render 'organizations/field', form: form, record: organization, attribute: :name, label: t('.name'), options: { maxlength: 100, autofocus: true } - unless organization.usos? = render 'organizations/field', form: form, record: organization, attribute: :max_members, - label: 'Limit członków', input: :number_field, + label: t('.max_members'), input: :number_field, options: { min: 1, step: 1 } = render 'organizations/dialog_actions', cancel_path: (editing ? organization_path(organization) : organizations_path), - submit_label: (editing ? 'Zapisz' : 'Utwórz organizację') + submit_label: (editing ? t('.save') : t('.create')) diff --git a/app/views/organizations/_member_row.html.haml b/app/views/organizations/_member_row.html.haml index b55b9652..d2de26df 100644 --- a/app/views/organizations/_member_row.html.haml +++ b/app/views/organizations/_member_row.html.haml @@ -7,25 +7,25 @@ = render 'organizations/person', user: user %span.gh-teacher-role - %span.gh-mobile-field-label Rola + %span.gh-mobile-field-label= t('.role') = user_role_label(user.role) - if user.first_login? - %small Zaproszenie wysłane + %small= t('.invited') %span - %span.gh-mobile-field-label Numer indeksu + %span.gh-mobile-field-label= t('.index_number') %span.gh-small= user.university_number.presence || '—' %span.gh-organization-member-actions - if user.first_login? = render 'organizations/resend_invitation_button', organization: @organization, user: user, name: name = link_to edit_organization_user_path(@organization, user), - class: 'gh-icon-button', title: 'Edytuj', - 'aria-label': "Edytuj: #{name}", + class: 'gh-icon-button', title: t('.edit'), + 'aria-label': t('.edit_label', name: name), data: { turbo_frame: 'modal' } do %i.fa-solid.fa-pen{ 'aria-hidden': 'true' } = link_to confirm_destroy_organization_user_path(@organization, user), - class: 'gh-icon-button', title: 'Usuń', - 'aria-label': "Usuń: #{name}", + class: 'gh-icon-button', title: t('.remove'), + 'aria-label': t('.remove_label', name: name), data: { turbo_frame: 'modal' } do %i.fa-solid.fa-trash{ 'aria-hidden': 'true' } diff --git a/app/views/organizations/_members.html.haml b/app/views/organizations/_members.html.haml index 754afff5..fb5ac4b9 100644 --- a/app/views/organizations/_members.html.haml +++ b/app/views/organizations/_members.html.haml @@ -1,39 +1,37 @@ - if users.any? %div{ data: { controller: 'list-search', list_search_total_value: users.size, - list_search_of_value: 'z', } } + list_search_of_value: t('.of'), } } .gh-student-list-toolbar %label.gh-search-input %i.fa-solid.fa-magnifying-glass{ 'aria-hidden': 'true' } %input{ type: 'search', - placeholder: 'Szukaj po nazwisku lub e-mailu', - 'aria-label': 'Szukaj użytkownika', + placeholder: t('.search_placeholder'), + 'aria-label': t('.search_label'), data: { list_search_target: 'input', action: 'input->list-search#filter' }, } %span.gh-select-input - = select_tag :role, options_for_select(user_role_options), include_blank: 'Wszystkie role', - 'aria-label': 'Filtruj po roli', + = select_tag :role, options_for_select(user_role_options), include_blank: t('.all_roles'), + 'aria-label': t('.filter_label'), data: { list_search_target: 'filter', action: 'change->list-search#filter' } %i.fa-solid.fa-chevron-down{ 'aria-hidden': 'true' } %span.gh-card-meta{ data: { list_search_target: 'count' } } - = "#{users.size} z #{users.size}" + = "#{users.size} #{t('.of')} #{users.size}" %section.gh-panel.gh-organizations-panel .gh-organization-member-row.gh-organization-member-row--header{ 'aria-hidden': 'true' } - %span Użytkownik - %span Rola - %span Numer indeksu - %span.gh-ledger-cell--right Akcje + %span= t('.user') + %span= t('.role') + %span= t('.index_number') + %span.gh-ledger-cell--right= t('.actions') = render partial: 'organizations/member_row', collection: users, as: :user %section.gh-panel.gh-empty-state{ hidden: true, data: { list_search_target: 'empty' } } %h2.gh-h2 - Brak użytkowników pasujących do + = t('.no_match') %span{ data: { list_search_target: 'emptyQuery' } } - %p.gh-lead Wyczyść wyszukiwanie, zmień rolę albo sprawdź pisownię. + %p.gh-lead= t('.no_match_hint') - else %section.gh-panel.gh-empty-state - %h2.gh-h2 Nie ma jeszcze użytkowników - %p.gh-lead - Dodaj studentów i nauczycieli — dostaną zaproszenie e-mailem. Konta - zakładane przez logowanie z uczelni pojawią się tu same. + %h2.gh-h2= t('.empty_title') + %p.gh-lead= t('.empty_lead') diff --git a/app/views/organizations/_owned_groups_notice.html.haml b/app/views/organizations/_owned_groups_notice.html.haml index 45246382..a9c05237 100644 --- a/app/views/organizations/_owned_groups_notice.html.haml +++ b/app/views/organizations/_owned_groups_notice.html.haml @@ -2,9 +2,6 @@ - if owned.any? %p.gh-explainer-note - - if owned.one? - Usunięta zostanie też grupa prowadzona przez to konto: - - else - Usunięte zostaną też grupy prowadzone przez to konto: - %b= owned.to_sentence(words_connector: ', ', two_words_connector: ' i ', last_word_connector: ' i ') - — ze wszystkimi studentami, walutą, odznakami i arkuszami ocen. + = t('.lead', count: owned.size) + %b= owned.to_sentence + = t('.rest') diff --git a/app/views/organizations/_resend_invitation_button.html.haml b/app/views/organizations/_resend_invitation_button.html.haml index 00a8a42b..8cea0b07 100644 --- a/app/views/organizations/_resend_invitation_button.html.haml +++ b/app/views/organizations/_resend_invitation_button.html.haml @@ -1,5 +1,5 @@ = button_to organization_user_invitation_path(organization, user), class: 'gh-icon-button', - title: 'Wyślij zaproszenie ponownie', - 'aria-label': "Wyślij zaproszenie ponownie: #{name}" do + title: t('.title'), + 'aria-label': t('.label', name: name) do %i.fa-solid.fa-paper-plane{ 'aria-hidden': 'true' } diff --git a/app/views/organizations/_user_form.html.haml b/app/views/organizations/_user_form.html.haml index 264ba150..944d1dcf 100644 --- a/app/views/organizations/_user_form.html.haml +++ b/app/views/organizations/_user_form.html.haml @@ -2,18 +2,18 @@ - url = editing ? organization_user_path(@organization, user) : add_user_organization_path(@organization) = form_with model: user, url: url, data: { turbo_frame: (@in_modal ? 'modal' : nil) } do |form| - %h2.gh-h2= editing ? 'Edytuj użytkownika' : 'Dodaj użytkownika' + %h2.gh-h2= editing ? t('.edit_title') : t('.new_title') - unless editing - %p Wyślemy zaproszenie na podany adres. Konto zajmie jedno miejsce w organizacji. + %p= t('.hint') - = render 'organizations/field', form: form, record: user, attribute: :email, label: 'Adres e-mail', + = render 'organizations/field', form: form, record: user, attribute: :email, label: t('.email'), input: :email_field, - options: { placeholder: 'jan.nowak@example.com', autofocus: !editing } - = render 'organizations/field', form: form, record: user, attribute: :full_name, label: 'Imię i nazwisko', + options: { placeholder: t('.email_placeholder'), autofocus: !editing } + = render 'organizations/field', form: form, record: user, attribute: :full_name, label: t('.full_name'), options: { maxlength: 80 } .gh-form-field - = form.label :role, 'Rola' + = form.label :role, t('.role') %span.gh-select-input = form.select :role, user_role_options %i.fa-solid.fa-chevron-down{ 'aria-hidden': 'true' } @@ -24,8 +24,8 @@ .gh-form-row-2col = render 'organizations/field', form: form, record: user, attribute: :university_number, - label: 'Numer indeksu (opcjonalnie)', options: { maxlength: 20 } - = render 'organizations/field', form: form, record: user, attribute: :usos_id, label: 'ID USOS (opcjonalnie)' + label: t('.university_number'), options: { maxlength: 20 } + = render 'organizations/field', form: form, record: user, attribute: :usos_id, label: t('.usos_id') - if (error = user.errors[:organization].first) %p.gh-field-error{ role: 'alert' } @@ -33,4 +33,4 @@ %span= error = render 'organizations/dialog_actions', cancel_path: organization_path(@organization), - submit_label: (editing ? 'Zapisz' : 'Wyślij zaproszenie') + submit_label: (editing ? t('.submit_edit') : t('.submit_new')) diff --git a/app/views/organizations/admins/_confirm_destroy.html.haml b/app/views/organizations/admins/_confirm_destroy.html.haml index 64581a43..b0010623 100644 --- a/app/views/organizations/admins/_confirm_destroy.html.haml +++ b/app/views/organizations/admins/_confirm_destroy.html.haml @@ -1,18 +1,18 @@ - name = admin.full_name.presence || admin.email -%h2.gh-h2= "Usunąć administratora #{name}?" +%h2.gh-h2= t('.title', name: name) %p - Konto zostanie usunięte na stałe i zwolni się jedno miejsce w organizacji. + = t('.body') - if admin.first_login? - Wysłane zaproszenie przestanie działać. + = t('.pending') = render 'organizations/owned_groups_notice', user: admin .gh-dialog-button-row - if @in_modal - %button.gh-btn.gh-btn--secondary{ type: 'button', data: { action: 'dialog#close' }, autofocus: true } Anuluj + %button.gh-btn.gh-btn--secondary{ type: 'button', data: { action: 'dialog#close' }, autofocus: true }= t('organizations.dialog_actions.cancel') - else - = link_to 'Anuluj', organization_path(@organization), class: 'gh-btn gh-btn--secondary' + = link_to t('organizations.dialog_actions.cancel'), organization_path(@organization), class: 'gh-btn gh-btn--secondary' - = button_to 'Usuń konto', organization_admin_path(@organization, admin), + = button_to t('.remove'), organization_admin_path(@organization, admin), method: :delete, class: 'gh-btn', form: { data: { turbo_frame: (@in_modal ? 'modal' : nil) } } diff --git a/app/views/organizations/admins/confirm_destroy.html.haml b/app/views/organizations/admins/confirm_destroy.html.haml index a5b867b5..d827f3ac 100644 --- a/app/views/organizations/admins/confirm_destroy.html.haml +++ b/app/views/organizations/admins/confirm_destroy.html.haml @@ -1,4 +1,4 @@ -- content_for :title, 'Usunąć administratora?' +- content_for :title, t('.page_title') - if @in_modal = turbo_frame_tag 'modal' do diff --git a/app/views/organizations/confirm_destroy.html.haml b/app/views/organizations/confirm_destroy.html.haml index 733da5df..f15255bc 100644 --- a/app/views/organizations/confirm_destroy.html.haml +++ b/app/views/organizations/confirm_destroy.html.haml @@ -1,4 +1,4 @@ -- content_for :title, "Usuń organizację #{@organization.name}" +- content_for :title, t('.page_title', name: @organization.name) - if @in_modal = turbo_frame_tag 'modal' do diff --git a/app/views/organizations/edit.html.haml b/app/views/organizations/edit.html.haml index fe7ac1e5..f85e264f 100644 --- a/app/views/organizations/edit.html.haml +++ b/app/views/organizations/edit.html.haml @@ -1,4 +1,4 @@ -- content_for :title, "Edytuj organizację — #{@organization.name}" +- content_for :title, t('.page_title', name: @organization.name) - if @in_modal = turbo_frame_tag 'modal' do diff --git a/app/views/organizations/index.html.haml b/app/views/organizations/index.html.haml index 8104e8cd..cc0cf86d 100644 --- a/app/views/organizations/index.html.haml +++ b/app/views/organizations/index.html.haml @@ -1,37 +1,33 @@ -- content_for :title, 'Organizacje' +- content_for :title, t('.title') %section.gh-panel.gh-page-header .gh-page-header-text - %h1.gh-h1 Organizacje - %p.gh-lead - Uczelnie korzystające z aplikacji. Konta logujące się przez USOS trafiają - do organizacji #{Organization::USOS_NAME}. + %h1.gh-h1= t('.title') + %p.gh-lead= t('.lead', usos: Organization::USOS_NAME) - if policy(:organization).create? = link_to new_organization_path, class: 'gh-btn', data: { turbo_frame: 'modal' } do %i.fa-solid.fa-plus{ 'aria-hidden': 'true' } - Utwórz organizację + = t('.create') - if @organizations.any? %section.gh-panel.gh-organizations-panel .gh-organization-row.gh-organization-row--header{ 'aria-hidden': 'true' } - %span Organizacja - %span Członkowie - %span Administratorzy + %span= t('.organization') + %span= t('.members') + %span= t('.admins') - @organizations.each do |organization| - members = @member_counts.fetch(organization.id, 0) = link_to organization_path(organization), class: 'gh-organization-row gh-organization-row--link' do %b.gh-organization-name= organization.name %span - %span.gh-mobile-field-label Członkowie + %span.gh-mobile-field-label= t('.members') %span.gh-numeric-cell= "#{members} / #{organization.max_members || '-'}" %span - %span.gh-mobile-field-label Administratorzy + %span.gh-mobile-field-label= t('.admins') %span.gh-numeric-cell= @admin_counts.fetch(organization.id, 0) - else %section.gh-panel.gh-empty-state - %h2.gh-h2 Nie ma jeszcze żadnej organizacji - %p.gh-lead - Utwórz organizację, a potem dodaj jej administratora — - on zaprosi resztę osób. + %h2.gh-h2= t('.empty_title') + %p.gh-lead= t('.empty_lead') diff --git a/app/views/organizations/new.html.haml b/app/views/organizations/new.html.haml index 220b39ed..5f50fb53 100644 --- a/app/views/organizations/new.html.haml +++ b/app/views/organizations/new.html.haml @@ -1,4 +1,4 @@ -- content_for :title, 'Nowa organizacja' +- content_for :title, t('.page_title') - if @in_modal = turbo_frame_tag 'modal' do diff --git a/app/views/organizations/new_admin.html.haml b/app/views/organizations/new_admin.html.haml index 9115f358..3e3edce6 100644 --- a/app/views/organizations/new_admin.html.haml +++ b/app/views/organizations/new_admin.html.haml @@ -1,4 +1,4 @@ -- content_for :title, 'Dodaj administratora organizacji' +- content_for :title, t('.page_title') - if @in_modal = turbo_frame_tag 'modal' do diff --git a/app/views/organizations/new_user.html.haml b/app/views/organizations/new_user.html.haml index 4f3c2c6e..4766f94f 100644 --- a/app/views/organizations/new_user.html.haml +++ b/app/views/organizations/new_user.html.haml @@ -1,4 +1,4 @@ -- content_for :title, 'Dodaj użytkownika organizacji' +- content_for :title, t('.page_title') - if @in_modal = turbo_frame_tag 'modal' do diff --git a/app/views/organizations/show.html.haml b/app/views/organizations/show.html.haml index eb893f87..d4a902ff 100644 --- a/app/views/organizations/show.html.haml +++ b/app/views/organizations/show.html.haml @@ -6,7 +6,7 @@ .gh-page-header-text - if @current_user.global_admin? %p.gh-breadcrumbs - = link_to 'Organizacje', organizations_path + = link_to t('.breadcrumb'), organizations_path %i.fa-solid.fa-chevron-right{ 'aria-hidden': 'true' } %span= @organization.name %h1.gh-h1= @organization.name @@ -17,15 +17,15 @@ = link_to edit_organization_path(@organization), class: 'gh-btn gh-btn--secondary', data: { turbo_frame: 'modal' } do %i.fa-solid.fa-pen{ 'aria-hidden': 'true' } - Edytuj + = t('.edit') - if policy(@organization).new_admin? = link_to new_admin_organization_path(@organization), class: 'gh-btn', data: { turbo_frame: 'modal' } do %i.fa-solid.fa-user-shield{ 'aria-hidden': 'true' } - Dodaj administratora + = t('.add_admin') - if manage_users = link_to new_user_organization_path(@organization), class: 'gh-btn', data: { turbo_frame: 'modal' } do %i.fa-solid.fa-user-plus{ 'aria-hidden': 'true' } - Dodaj użytkownika + = t('.add_user') = render 'admins', admins: @admins, manage_admins: policy(@organization).manage_admins? @@ -34,10 +34,10 @@ - if policy(@organization).destroy? %section.gh-panel.gh-danger-zone - %h2.gh-h3 Usuń organizację - %p.gh-small Razem z organizacją znikną wszystkie jej konta. Tej operacji nie można cofnąć. + %h2.gh-h3= t('.danger_title') + %p.gh-small= t('.danger_body') .gh-button-row = link_to confirm_destroy_organization_path(@organization), class: 'gh-btn gh-btn--secondary', data: { turbo_frame: 'modal' } do %i.fa-solid.fa-trash{ 'aria-hidden': 'true' } - Usuń organizację + = t('.danger_button') diff --git a/app/views/organizations/users/_confirm_destroy.html.haml b/app/views/organizations/users/_confirm_destroy.html.haml index cdb70ad7..abc29d51 100644 --- a/app/views/organizations/users/_confirm_destroy.html.haml +++ b/app/views/organizations/users/_confirm_destroy.html.haml @@ -1,15 +1,13 @@ -%h2.gh-h2= "Usunąć konto #{user.full_name}?" -%p - Konto zostanie usunięte na stałe, razem z członkostwem w grupach, walutą, - odznakami i przedmiotami. Zwolni się jedno miejsce w organizacji. +%h2.gh-h2= t('.title', name: user.full_name) +%p= t('.body') = render 'organizations/owned_groups_notice', user: user .gh-dialog-button-row - if @in_modal - %button.gh-btn.gh-btn--secondary{ type: 'button', data: { action: 'dialog#close' }, autofocus: true } Anuluj + %button.gh-btn.gh-btn--secondary{ type: 'button', data: { action: 'dialog#close' }, autofocus: true }= t('organizations.dialog_actions.cancel') - else - = link_to 'Anuluj', organization_path(@organization), class: 'gh-btn gh-btn--secondary' + = link_to t('organizations.dialog_actions.cancel'), organization_path(@organization), class: 'gh-btn gh-btn--secondary' - = button_to 'Usuń konto', organization_user_path(@organization, user), + = button_to t('.remove'), organization_user_path(@organization, user), method: :delete, class: 'gh-btn', form: { data: { turbo_frame: (@in_modal ? 'modal' : nil) } } diff --git a/app/views/organizations/users/confirm_destroy.html.haml b/app/views/organizations/users/confirm_destroy.html.haml index d83d4842..a0781f9b 100644 --- a/app/views/organizations/users/confirm_destroy.html.haml +++ b/app/views/organizations/users/confirm_destroy.html.haml @@ -1,4 +1,4 @@ -- content_for :title, "Usunąć konto #{@user.full_name}?" +- content_for :title, t('.page_title', name: @user.full_name) - if @in_modal = turbo_frame_tag 'modal' do diff --git a/app/views/organizations/users/edit.html.haml b/app/views/organizations/users/edit.html.haml index d3b3ab4e..46f909fe 100644 --- a/app/views/organizations/users/edit.html.haml +++ b/app/views/organizations/users/edit.html.haml @@ -1,4 +1,4 @@ -- content_for :title, "Edytuj użytkownika — #{@user.full_name}" +- content_for :title, t('.page_title', name: @user.full_name) - if @in_modal = turbo_frame_tag 'modal' do diff --git a/app/views/passwordless_mailer/token_email.html.haml b/app/views/passwordless_mailer/token_email.html.haml index a9885e83..f4d83431 100644 --- a/app/views/passwordless_mailer/token_email.html.haml +++ b/app/views/passwordless_mailer/token_email.html.haml @@ -6,21 +6,21 @@ %td{ style: 'padding:32px;font-family:Helvetica,Arial,sans-serif;' } %p{ style: 'margin:0 0 28px;font:800 18px/1 Helvetica,Arial,sans-serif;color:#221A2B;' } Gamification Hub - %h1{ style: 'margin:0 0 16px;font-size:22px;color:#221A2B;' } Zaloguj się + %h1{ style: 'margin:0 0 16px;font-size:22px;color:#221A2B;' }= t('.title') %p{ style: 'margin:0 0 12px;font-size:15px;line-height:1.5;color:#584C64;' } - = "Cześć, #{@first_name}!" + = t('.greeting', name: @first_name) %p{ style: 'margin:0 0 24px;font-size:15px;line-height:1.5;color:#584C64;' } - Kliknij przycisk poniżej, aby zalogować się. Hasło nie jest potrzebne. + = t('.body') %table{ cellpadding: 0, cellspacing: 0, border: 0 } %tr %td{ style: 'background:#FF7A1A;border-bottom:3px solid #C45A10;', align: 'center' } - %a{ href: @token_link, style: 'display:inline-block;padding:14px 28px;font:700 15px Helvetica,Arial,sans-serif;color:#221A2B;text-decoration:none;' } Zaloguj się + %a{ href: @token_link, style: 'display:inline-block;padding:14px 28px;font:700 15px Helvetica,Arial,sans-serif;color:#221A2B;text-decoration:none;' }= t('.button') %p{ style: 'margin:24px 0 0;font-size:13px;line-height:1.5;color:#85798F;' } - = "Link jest ważny przez #{gh_minutes(@expires_in_minutes)}. Jeśli przycisk nie działa, skopiuj ten adres do przeglądarki:" + = t('.fallback', minutes: gh_minutes(@expires_in_minutes)) %p{ style: 'margin:8px 0 0;font-size:13px;word-break:break-all;' } %a{ href: @token_link, style: 'color:#584C64;' }= @token_link %p{ style: 'margin:28px 0 0;padding-top:16px;border-top:1px solid #E4DCEA;font-size:12.5px;color:#85798F;' } - Jeśli to nie Ty próbowałeś się zalogować, możesz zignorować tę wiadomość. + = t('.ignore') diff --git a/app/views/passwordless_mailer/token_email.text.haml b/app/views/passwordless_mailer/token_email.text.haml index b22d6b2a..74ad0019 100644 --- a/app/views/passwordless_mailer/token_email.text.haml +++ b/app/views/passwordless_mailer/token_email.text.haml @@ -1,11 +1,11 @@ Gamification Hub -= "Cześć, #{@first_name}!" += t('.greeting', name: @first_name) -Aby zalogować się, otwórz poniższy link. Hasło nie jest potrzebne. += t('.text_body') = @token_link -= "Link jest ważny przez #{gh_minutes(@expires_in_minutes)}." += t('.valid_for', minutes: gh_minutes(@expires_in_minutes)) -Jeśli to nie Ty próbowałeś się zalogować, możesz zignorować tę wiadomość. += t('.ignore') diff --git a/app/views/ranking/_board.html.haml b/app/views/ranking/_board.html.haml index 00cb5b77..5b631e32 100644 --- a/app/views/ranking/_board.html.haml +++ b/app/views/ranking/_board.html.haml @@ -1,32 +1,32 @@ -%section.gh-panel.gh-ranking-table{ role: 'table', 'aria-label': 'Ranking' } +%section.gh-panel.gh-ranking-table{ role: 'table', 'aria-label': t('.table_label') } .gh-ranking-row.gh-ranking-row--header{ role: 'row' } - %span{ role: 'columnheader' } Miejsce - %span{ role: 'columnheader' }= reveal_names ? 'Pseudonim i student' : 'Pseudonim' - %span{ role: 'columnheader' } Ranga - %span.gh-ranking-score{ role: 'columnheader' } Zebrane + %span{ role: 'columnheader' }= t('.place') + %span{ role: 'columnheader' }= reveal_names ? t('.nickname_and_student') : t('.nickname') + %span{ role: 'columnheader' }= t('.rank') + %span.gh-ranking-score{ role: 'columnheader' }= t('.collected') - board.visible_rows.each do |row| - if row.gap? .gh-ranking-row.gh-ranking-row--hidden-gap{ role: 'row' } %span.gh-ranking-gap-label{ role: 'cell' } … %span.gh-ranking-name{ role: 'cell' } - %span.gh-empty-note Pozostałe miejsca są ukryte + %span.gh-empty-note= t('.hidden_rows') - else .gh-ranking-row{ class: ('gh-ranking-row--me' if row.mine), role: 'row' } %span{ role: 'cell' } %span.gh-position-badge.gh-octagon-clip{ class: ("gh-position-badge--p#{row.place}" if row.place <= 3), - 'aria-label': "Miejsce #{row.place}" }= row.place + 'aria-label': t('.place_label', place: row.place) }= row.place %span.gh-ranking-name{ role: 'cell' } %b = row.student.display_name - if row.mine - %span.gh-you-badge.gh-octagon-clip Ty + %span.gh-you-badge.gh-octagon-clip= t('.you') - if reveal_names - if row.student.nickname.present? %small= row.student.full_name - else - %small.gh-empty-note Bez pseudonimu + %small.gh-empty-note= t('.no_nickname') %span.gh-ranking-rank-cell{ role: 'cell' }= row.rank&.name || '—' %span.gh-ranking-score{ role: 'cell' }= row.student.total_currency diff --git a/app/views/ranking/_confirm_mode.html.haml b/app/views/ranking/_confirm_mode.html.haml index ace43d7f..dbfb323c 100644 --- a/app/views/ranking/_confirm_mode.html.haml +++ b/app/views/ranking/_confirm_mode.html.haml @@ -1,25 +1,21 @@ - full = mode == 'full' - if full - %h2#gh-rk-title.gh-h2 Pokazać pełny ranking? - %p - Każdy student zobaczy miejsca i pseudonimy wszystkich, także osób na końcu - listy. + %h2#gh-rk-title.gh-h2= t('.full_title') + %p= t('.full_body') - else - %h2#gh-rk-title.gh-h2 Ograniczyć ranking do podium? - %p - Studentom zostaną pierwsze 3 miejsca i ich własne. Nikt nie zobaczy, kto - jest na końcu listy. + %h2#gh-rk-title.gh-h2= t('.podium_title') + %p= t('.podium_body') .gh-dialog-button-row - if @in_modal %button.gh-btn.gh-btn--secondary{ type: 'button', data: { action: 'dialog#close' }, autofocus: true } - = full ? 'Zostaw podium' : 'Anuluj' + = full ? t('.keep_podium') : t('shared.actions.cancel') - else - = link_to (full ? 'Zostaw podium' : 'Anuluj'), story_group_ranking_path(story_group), + = link_to (full ? t('.keep_podium') : t('shared.actions.cancel')), story_group_ranking_path(story_group), class: 'gh-btn gh-btn--secondary' - = button_to (full ? 'Pokaż pełny ranking' : 'Pokaż tylko podium'), + = button_to (full ? t('.show_full') : t('.show_podium')), story_group_ranking_path(story_group), method: :patch, class: 'gh-btn', params: { story_group: { ranking_mode: mode } }, diff --git a/app/views/ranking/_confirm_visibility.html.haml b/app/views/ranking/_confirm_visibility.html.haml index 1cf6c783..bad94bbd 100644 --- a/app/views/ranking/_confirm_visibility.html.haml +++ b/app/views/ranking/_confirm_visibility.html.haml @@ -1,47 +1,41 @@ - - if enabled - %h2#gh-rk-title.gh-h2 Pokazać ranking studentom? - %p Studenci zobaczą ranking od razu. Wybierz, ile mają widzieć. + %h2#gh-rk-title.gh-h2= t('.show_title') + %p= t('.show_body') = form_with model: story_group, url: story_group_ranking_path(story_group), method: :patch, data: { turbo_frame: (@in_modal ? 'modal' : nil) } do |f| = f.hidden_field :ranking_enabled, value: true - .gh-choice-grid.gh-ranking-mode-options{ role: 'radiogroup', 'aria-label': 'Co widzą studenci' } + .gh-choice-grid.gh-ranking-mode-options{ role: 'radiogroup', 'aria-label': t('.modes_label') } %label.gh-choice-card = f.radio_button :ranking_mode, 'podium_and_own', checked: mode == 'podium_and_own', class: 'sr-only' - %b= StoryGroup::RANKING_MODE_LABELS.fetch('podium_and_own') - %span - Wszyscy widzą pierwsze 3 miejsca. Każdy widzi też swoje miejsce, ale - nie cudze. + %b= StoryGroup.ranking_mode_label('podium_and_own') + %span= t('.podium_body') %label.gh-choice-card = f.radio_button :ranking_mode, 'full', checked: mode == 'full', class: 'sr-only' - %b= StoryGroup::RANKING_MODE_LABELS.fetch('full') - %span - Wszyscy widzą miejsca i pseudonimy wszystkich, także na końcu listy. + %b= StoryGroup.ranking_mode_label('full') + %span= t('.full_body') .gh-dialog-button-row - if @in_modal - %button.gh-btn.gh-btn--secondary{ type: 'button', data: { action: 'dialog#close' } } Anuluj + %button.gh-btn.gh-btn--secondary{ type: 'button', data: { action: 'dialog#close' } }= t('shared.actions.cancel') - else - = link_to 'Anuluj', story_group_ranking_path(story_group), class: 'gh-btn gh-btn--secondary' + = link_to t('shared.actions.cancel'), story_group_ranking_path(story_group), class: 'gh-btn gh-btn--secondary' - %button.gh-btn{ type: 'submit', autofocus: true } Pokaż ranking + %button.gh-btn{ type: 'submit', autofocus: true }= t('.show') - else - %h2#gh-rk-title.gh-h2 Ukryć ranking przed studentami? - %p - Studenci przestaną widzieć miejsca i pseudonimy. Ty nadal widzisz cały - ranking, a zebrana waluta i rangi nie zmieniają się. + %h2#gh-rk-title.gh-h2= t('.hide_title') + %p= t('.hide_body') .gh-dialog-button-row - if @in_modal %button.gh-btn.gh-btn--secondary{ type: 'button', data: { action: 'dialog#close' }, autofocus: true } - Zostaw widoczny + = t('.keep_visible') - else - = link_to 'Zostaw widoczny', story_group_ranking_path(story_group), class: 'gh-btn gh-btn--secondary' + = link_to t('.keep_visible'), story_group_ranking_path(story_group), class: 'gh-btn gh-btn--secondary' - = button_to 'Ukryj ranking', story_group_ranking_path(story_group), + = button_to t('.hide'), story_group_ranking_path(story_group), method: :patch, class: 'gh-btn', params: { story_group: { ranking_enabled: false } }, form: { data: { turbo_frame: (@in_modal ? 'modal' : nil) } } diff --git a/app/views/ranking/_student_board.html.haml b/app/views/ranking/_student_board.html.haml index be63b9f3..c75e9fcc 100644 --- a/app/views/ranking/_student_board.html.haml +++ b/app/views/ranking/_student_board.html.haml @@ -1,32 +1,32 @@ - story_group = board.story_group %section.gh-panel.gh-page-header.gh-page-header--stacked - %h1.gh-h1 Ranking + %h1.gh-h1= t('.title') %p.gh-lead - if !board.visible? - Prowadzący ukrył ranking na razie. + = t('.hidden_lead') - elsif story_group.ranking_podium_and_own? - Prowadzący pokazuje tylko podium. Swoje miejsce widzisz tylko Ty. + = t('.podium_lead') - else - = "Kolejność według łącznie zebranej waluty (#{story_group.currency_name})." + = t('.full_lead', currency: story_group.currency_name) .gh-my-ranking-stats - if board.own_place %div - %b= "#{board.own_place}." - %small= "Twoje miejsce z #{board.participants_count}" + %b= t('.own_place', place: board.own_place) + %small= t('.own_place_label', count: board.participants_count) %div %b= board.own_row.student.total_currency - %small Zebrane łącznie + %small= t('.collected') .gh-nickname-row - Twój pseudonim w tej grupie: - %b= board.membership.nickname.presence || 'brak' + = t('.nickname') + %b= board.membership.nickname.presence || t('.no_nickname') = link_to nickname_story_group_membership_path(story_group), class: 'gh-link-button', data: { turbo_frame: 'modal' } do %i.fa-solid.fa-pen{ 'aria-hidden': 'true' } - Zmień + = t('.change') - if board.visible? = render 'board', board: board, reveal_names: false @@ -34,7 +34,5 @@ %section.gh-panel.gh-empty-state .gh-empty-state-icon.gh-octagon-clip %i.fa-solid.fa-eye-slash{ 'aria-hidden': 'true' } - %h2.gh-h2 Ranking jest teraz ukryty - %p.gh-lead - Zbieraj dalej walutę. Gdy prowadzący włączy ranking, zobaczysz tu swoje - miejsce. + %h2.gh-h2= t('.hidden_title') + %p.gh-lead= t('.hidden_body') diff --git a/app/views/ranking/_teacher_board.html.haml b/app/views/ranking/_teacher_board.html.haml index 37d811c9..4a0d2833 100644 --- a/app/views/ranking/_teacher_board.html.haml +++ b/app/views/ranking/_teacher_board.html.haml @@ -2,10 +2,8 @@ %section.gh-panel.gh-page-header .gh-page-header-text - %h1.gh-h1 Ranking - %p.gh-lead - = "Kolejność według łącznie zebranej waluty (#{story_group.currency_name})." - Studenci widzą tylko pseudonimy, Ty widzisz też imiona i nazwiska. + %h1.gh-h1= t('.title') + %p.gh-lead= t('.lead', currency: story_group.currency_name) .gh-ranking-controls .gh-toggle-switch-row.gh-switch-row--inline @@ -15,11 +13,12 @@ 'aria-checked': board.visible?.to_s, 'aria-labelledby': 'gh-rk-vis', data: { turbo_frame: 'modal' } %span - %b#gh-rk-vis Widoczny dla studentów + %b#gh-rk-vis= t('.visible') - if board.visible? - .gh-segmented-toggle-2.gh-octagon-clip{ role: 'group', 'aria-label': 'Co widzą studenci' } - - StoryGroup::RANKING_MODE_LABELS.each do |mode, label| + .gh-segmented-toggle-2.gh-octagon-clip{ role: 'group', 'aria-label': t('.modes_label') } + - StoryGroup.ranking_modes.each_key do |mode| + - label = StoryGroup.ranking_mode_label(mode) - if story_group.ranking_mode == mode %span{ 'aria-pressed': 'true' }= label - else @@ -30,27 +29,24 @@ %p.gh-info-banner.gh-ranking-visibility-note %i.fa-solid.fa-eye-slash{ 'aria-hidden': 'true' } %span - %b Ranking jest ukryty przed studentami. - Ty nadal go widzisz. + %b= t('.hidden_note') + = t('.hidden_note_tail') - elsif story_group.ranking_podium_and_own? %p.gh-info-banner.gh-ranking-visibility-note %i.fa-solid.fa-eye-slash{ 'aria-hidden': 'true' } %span - %b Studenci widzą tylko podium i swoje miejsce. - Nikt nie zobaczy, kto jest na końcu listy. Ty widzisz wszystkich. + %b= t('.podium_note') + = t('.podium_note_tail') - if board.any? = render 'board', board: board, reveal_names: true - %p.gh-small.gh-ranking-footnote - Remis daje to samo miejsce. - Studenci bez pseudonimu są widoczni pod imieniem i nazwiskiem. + %p.gh-small.gh-ranking-footnote= t('.footnote') - else %section.gh-panel.gh-empty-state - %h2.gh-h2 Nikt jeszcze nie dołączył do grupy - %p.gh-lead - Ranking ułoży się sam, gdy studenci dołączą i zaczną zbierać walutę. + %h2.gh-h2= t('.empty_title') + %p.gh-lead= t('.empty_body') .gh-button-row = link_to story_group_invites_path(story_group), class: 'gh-btn' do %i.fa-solid.fa-qrcode - Zaproszenia + = t('.invites') diff --git a/app/views/ranking/confirm_mode.html.haml b/app/views/ranking/confirm_mode.html.haml index 1333c647..7fddbbea 100644 --- a/app/views/ranking/confirm_mode.html.haml +++ b/app/views/ranking/confirm_mode.html.haml @@ -1,4 +1,4 @@ -- content_for :title, "Ranking - #{StoryGroup::RANKING_MODE_LABELS.fetch(@mode)}" +- content_for :title, t('.page_title', mode: StoryGroup.ranking_mode_label(@mode)) - if @in_modal = turbo_frame_tag 'modal' do diff --git a/app/views/ranking/confirm_visibility.html.haml b/app/views/ranking/confirm_visibility.html.haml index 680f6533..5e4ba16b 100644 --- a/app/views/ranking/confirm_visibility.html.haml +++ b/app/views/ranking/confirm_visibility.html.haml @@ -1,4 +1,4 @@ -- content_for :title, @enabled ? 'Pokaż ranking' : 'Ukryj ranking' +- content_for :title, @enabled ? t('.show_page_title') : t('.hide_page_title') - if @in_modal = turbo_frame_tag 'modal' do diff --git a/app/views/ranking/show.html.haml b/app/views/ranking/show.html.haml index e4dab000..28bec0c5 100644 --- a/app/views/ranking/show.html.haml +++ b/app/views/ranking/show.html.haml @@ -1,4 +1,4 @@ -- content_for :title, "Ranking - #{@story_group.name}" +- content_for :title, t('.page_title', group: @story_group.name) - if @board.teacher? = render 'teacher_board', board: @board diff --git a/app/views/ranks/_confirm_destroy.html.haml b/app/views/ranks/_confirm_destroy.html.haml index d2156e4f..b24fac63 100644 --- a/app/views/ranks/_confirm_destroy.html.haml +++ b/app/views/ranks/_confirm_destroy.html.haml @@ -1,25 +1,21 @@ -%h2#gh-del-title.gh-h2= "Usunąć rangę „#{rank.name}”?" -%p Studenci z tą rangą spadną do rangi niżej. +%h2#gh-del-title.gh-h2= t('.title', name: rank.name) +%p= t('.body') - if dependent_items.any? - - one = dependent_items.one? %p.gh-warning-note{ role: 'alert' } %i.fa-solid.fa-triangle-exclamation{ 'aria-hidden': 'true' } - %span - = "#{one ? 'Przedmiot' : 'Przedmioty'} #{gh_and_list(dependent_items.map(&:name))}" - = one ? 'wymaga tej rangi.' : 'wymagają tej rangi.' - = "Zanim ją usuniesz, przypisz #{one ? 'mu' : 'im'} inną rangę." + %span= t('.blocked', count: dependent_items.size, items: gh_and_list(dependent_items.map(&:name))) .gh-dialog-button-row - if @in_modal %button.gh-btn.gh-btn--secondary{ type: 'button', data: { action: 'dialog#close' }, autofocus: true } - Anuluj + = t('shared.actions.cancel') - else - = link_to 'Anuluj', edit_story_group_rank_path(@story_group, rank), class: 'gh-btn gh-btn--secondary' + = link_to t('shared.actions.cancel'), edit_story_group_rank_path(@story_group, rank), class: 'gh-btn gh-btn--secondary' - if dependent_items.any? - %button.gh-btn{ type: 'button', disabled: true } Usuń rangę + %button.gh-btn{ type: 'button', disabled: true }= t('.remove') - else - = button_to 'Usuń rangę', story_group_rank_path(@story_group, rank), + = button_to t('.remove'), story_group_rank_path(@story_group, rank), method: :delete, class: 'gh-btn', form: { data: { turbo_frame: (@in_modal ? 'modal' : nil) } } diff --git a/app/views/ranks/_form.html.haml b/app/views/ranks/_form.html.haml index c0ea337d..a3139d89 100644 --- a/app/views/ranks/_form.html.haml +++ b/app/views/ranks/_form.html.haml @@ -3,15 +3,15 @@ %section.gh-panel.gh-page-header.gh-page-header--stacked %p.gh-breadcrumbs - = link_to 'Rangi', story_group_ranks_path(@story_group) + = link_to t('.breadcrumb'), story_group_ranks_path(@story_group) %i.fa-solid.fa-chevron-right{ 'aria-hidden': 'true' } - %span= editing ? (rank.name_was.presence || rank.name) : 'Nowa ranga' - %h1.gh-h1= editing ? 'Edytuj rangę' : 'Nowa ranga' - %p.gh-lead - Podgląd obok pokazuje, gdzie ranga trafi na drabince i ilu studentów obejmie. + %span= editing ? (rank.name_was.presence || rank.name) : t('.new_title') + %h1.gh-h1= editing ? t('.edit_title') : t('.new_title') + %p.gh-lead= t('.lead') - rungs_json = ladder.rungs.map { |rung| { id: rung.rank.id, name: rung.rank.name, min: rung.rank.required_currency_value.to_i, disc: rung.rank.discount.to_i } }.to_json -- form_data = { controller: 'rank-form', action: 'gh:image-crop->rank-form#art', rank_form_rungs_value: rungs_json, rank_form_totals_value: ladder.student_totals.to_json, rank_form_editing_id_value: rank.id.to_i } +- i18n_json = t('ranks.helpers').slice(:required, :gives_discount, :discount, :starting, :no_discount, :name_placeholder, :new_rank, :from_short).merge(t('.js')).to_json +- form_data = { controller: 'rank-form', action: 'gh:image-crop->rank-form#art', rank_form_rungs_value: rungs_json, rank_form_totals_value: ladder.student_totals.to_json, rank_form_editing_id_value: rank.id.to_i, rank_form_i18n_value: i18n_json } = form_with model: [@story_group, rank], data: form_data do |f| @@ -22,19 +22,18 @@ %p.gh-info-banner %i.fa-solid.fa-users{ 'aria-hidden': 'true' } %span - %b - = "#{holders} #{gh_plural(holders, 'student ma', 'studentów ma', 'studentów ma')} tę rangę." - Zmiana progu może przesunąć studentów w górę lub w dół. Podgląd obok pokazuje skutki. + %b= t('.holders', count: holders) + = t('.holders_note') .gh-form-section - %h2.gh-h3 Karta rangi - %p.gh-field-hint Tak ranga wygląda na stronie grupy i w rankingu. + %h2.gh-h3= t('.card') + %p.gh-field-hint= t('.card_hint') .gh-form-field - = f.label :name, 'Nazwa' + = f.label :name, t('shared.actions.name') .gh-text-input{ class: ('gh-text-input--invalid' if error.call(:name)) } = f.text_field :name, maxlength: 40, autocomplete: 'off', - placeholder: 'np. Admirał Floty', + placeholder: t('.name_placeholder'), 'aria-invalid': (error.call(:name) ? 'true' : nil), 'aria-describedby': (error.call(:name) ? 'gh-rank-name-error' : nil), data: { rank_form_target: 'name', action: 'rank-form#refresh' } @@ -48,13 +47,12 @@ refresh_action: 'rank-form#refresh' .gh-form-section - %h2.gh-h3 Próg i nagroda - %p.gh-field-hint - Ranga zależy od łącznie zebranej waluty, więc wydawanie jej nie obniża rangi. + %h2.gh-h3= t('.threshold_section') + %p.gh-field-hint= t('.threshold_hint') .gh-form-row-2col .gh-form-field - = f.label :required_currency_value, 'Wymagane zebrane' + = f.label :required_currency_value, t('.threshold') .gh-price-field-row .gh-text-input.gh-text-input--numeric{ class: ('gh-text-input--invalid' if error.call(:required_currency_value)) } = f.number_field :required_currency_value, min: 0, step: 1, @@ -64,7 +62,7 @@ = render 'layouts/currency_token', story_group: @story_group, size: 32 .gh-form-field - = f.label :discount, 'Zniżka w sklepie' + = f.label :discount, t('.discount') .gh-price-field-row .gh-text-input.gh-text-input--numeric{ class: ('gh-text-input--invalid' if error.call(:discount)) } = f.number_field :discount, min: 0, max: 100, step: 1, @@ -87,23 +85,21 @@ - if editing .gh-form-section - %h2.gh-h3 Usuwanie - %p.gh-field-hint - Studenci z tą rangą spadną do rangi niżej. Przedmioty, które jej wymagają, - trzeba będzie najpierw przypisać do innej rangi. + %h2.gh-h3= t('shared.actions.removal') + %p.gh-field-hint= t('.removal_hint') .gh-button-row = link_to confirm_destroy_story_group_rank_path(@story_group, rank), class: 'gh-btn gh-btn--secondary', data: { turbo_frame: 'modal' } do %i.fa-solid.fa-trash{ 'aria-hidden': 'true' } - Usuń rangę + = t('.remove') - %aside.gh-preview-column{ 'aria-label': 'Podgląd' } + %aside.gh-preview-column{ 'aria-label': t('shared.actions.preview') } = render 'preview', rank: rank, ladder: ladder, editing: editing .gh-panel.gh-form-actions-bar - = link_to 'Anuluj', story_group_ranks_path(@story_group), class: 'gh-link-button' + = link_to t('shared.actions.cancel'), story_group_ranks_path(@story_group), class: 'gh-link-button' %span.gh-form-actions-spacer %button.gh-btn{ type: 'submit' } - unless editing %i.fa-solid.fa-plus{ 'aria-hidden': 'true' } - = editing ? 'Zapisz zmiany' : 'Dodaj rangę' + = editing ? t('shared.actions.save_changes') : t('.create') diff --git a/app/views/ranks/_preview.html.haml b/app/views/ranks/_preview.html.haml index d6d7e4d5..c71790c6 100644 --- a/app/views/ranks/_preview.html.haml +++ b/app/views/ranks/_preview.html.haml @@ -4,11 +4,11 @@ %details.gh-preview-details{ open: true } %summary - Podgląd rangi + = t('.summary') %i.fa-solid.fa-chevron-down{ 'aria-hidden': 'true' } %section.gh-panel.gh-preview-panel - %h3 Karta rangi + %h3= t('.card') .gh-preview-flip-card %article.gh-card.gh-card--rank .gh-card-frame @@ -16,7 +16,7 @@ .gh-card-header %h4.gh-card-name{ class: ('gh-placeholder-text' if rank.name.blank?), data: { rank_form_target: 'cardName' } } - = rank.name.presence || 'Nazwa rangi' + = rank.name.presence || t('ranks.helpers.name_placeholder') .gh-card-art{ data: { rank_form_target: 'cardArt' } } - if rank.upload? = image_tag rank.icon, alt: '' @@ -25,7 +25,7 @@ %p.gh-rule-text{ data: { rank_form_target: 'cardRules' } }= rank_reward_line(rank) %section.gh-panel.gh-preview-panel - %h3 Drabinka rang + %h3= t('.ladder') %ol.gh-rank-form-ladder{ data: { rank_form_target: 'ladder' } } - entries.each do |entry, draft| %li.gh-rank-form-rung{ class: ('gh-rank-form-rung--draft' if draft), @@ -36,10 +36,10 @@ %span %b %span{ data: (draft ? { rank_form_target: 'draftName' } : {}) } - = entry.name.presence || 'Nowa ranga' + = entry.name.presence || t('ranks.helpers.new_rank') - if draft - %small (ta ranga) + %small= t('.this_rank') %small{ data: (draft ? { rank_form_target: 'draftTerms' } : {}) } = rank_ladder_terms(entry) %span.gh-rank-form-rung-value{ data: (draft ? { rank_form_target: 'draftValue' } : {}) } - = "od #{entry.required_currency_value.to_i}" + = t('ranks.helpers.from_short', min: entry.required_currency_value.to_i) diff --git a/app/views/ranks/_student_ladder.html.haml b/app/views/ranks/_student_ladder.html.haml index 6e628c5e..f809ee97 100644 --- a/app/views/ranks/_student_ladder.html.haml +++ b/app/views/ranks/_student_ladder.html.haml @@ -1,8 +1,7 @@ %section.gh-panel.gh-page-header .gh-page-header-text - %h1.gh-h1 Rangi - %p.gh-lead - Ranga rośnie razem z łącznie zebraną walutą. Wydawanie jej nie obniża rangi. + %h1.gh-h1= t('.title') + %p.gh-lead= t('.lead') - if ladder.any? %ol.gh-panel.gh-student-ladder-list @@ -13,36 +12,34 @@ %b = rung.rank.name - if rung.current? - %span.gh-you-badge.gh-octagon-clip Twoja ranga + %span.gh-you-badge.gh-octagon-clip= t('.yours') %small.gh-small= rank_summary_sentences(rung.rank) %div - if rung.done? %span.gh-rank-status.gh-rank-status--achieved %i.fa-solid.fa-check{ 'aria-hidden': 'true' } - Zdobyta + = t('.achieved') - elsif rung.progressing? - collected, needed = rung.progress %span.gh-rank-status - if rung.next? - = "#{collected} z #{needed} do pierwszej rangi" + = t('.to_first', collected: collected, needed: needed) - else - = "#{ladder.collected} z #{rung.next_threshold} do następnej" + = t('.to_next', collected: ladder.collected, needed: rung.next_threshold) .gh-progress-bar.gh-octagon-clip{ role: 'progressbar', 'aria-valuemin': 0, 'aria-valuemax': needed, 'aria-valuenow': collected, - 'aria-label': "Postęp do rangi wyżej niż #{rung.rank.name}" } + 'aria-label': t('.progress_label', name: rung.rank.name) } %i.gh-octagon-clip{ style: "--gh-p: #{gh_percent(collected, needed)}%" } - elsif rung.current? - %span.gh-rank-status.gh-rank-status--achieved Najwyższa ranga + %span.gh-rank-status.gh-rank-status--achieved= t('.top') - else %span.gh-rank-status %i.fa-solid.fa-lock{ 'aria-hidden': 'true' } - = "Brakuje #{rung.remaining}" + = t('.missing', count: rung.remaining) - else %section.gh-panel.gh-empty-state - %h2.gh-h2 W tej grupie nie ma jeszcze rang - %p.gh-lead - Prowadzący ich nie dodał. Zbieraj walutę — kiedy rangi się pojawią, zaliczy - się wszystko, co zebrałeś do tej pory. + %h2.gh-h2= t('.empty_title') + %p.gh-lead= t('.empty_body') diff --git a/app/views/ranks/_teacher_ladder.html.haml b/app/views/ranks/_teacher_ladder.html.haml index 4092b86b..3e55639a 100644 --- a/app/views/ranks/_teacher_ladder.html.haml +++ b/app/views/ranks/_teacher_ladder.html.haml @@ -1,14 +1,12 @@ %section.gh-panel.gh-page-header .gh-page-header-text - %h1.gh-h1 Rangi - %p.gh-lead - = "#{ladder.size} #{gh_plural(ladder.size, 'ranga', 'rangi', 'rang')}." - Ranga zależy od łącznie zebranej waluty, więc wydawanie jej nie obniża rangi. + %h1.gh-h1= t('.title') + %p.gh-lead= t('.lead', count: ladder.size) .gh-button-row = link_to new_story_group_rank_path(@story_group), class: 'gh-btn' do %i.fa-solid.fa-plus - Nowa ranga + = t('.new') - if ladder.any? %section.gh-panel.gh-list-panel @@ -16,7 +14,7 @@ - ladder.descending.each_with_index do |rung, position| - if position.positive? %li.gh-rank-gap{ 'aria-hidden': 'true' } - = "↑ #{rung.gap_to_next} zebranych do awansu" + = t('.gap', count: rung.gap_to_next) %li.gh-rank-row = render 'rank_thumb', rank: rung.rank @@ -27,26 +25,24 @@ - required_by = ladder.dependent_items(rung.rank) - if required_by.any? %small.gh-rank-row-requirement - = "Wymagana do zakupu: #{required_by.map(&:name).join(', ')}" + = t('.required_by', items: required_by.map(&:name).join(', ')) .gh-rank-row-stats %b= rung.holders - = gh_plural(rung.holders, 'student', 'studentów', 'studentów') + = t('.holders', count: rung.holders) - if ladder.total_students.positive? .gh-progress-bar.gh-octagon-clip{ role: 'progressbar', 'aria-valuemin': 0, 'aria-valuemax': ladder.total_students, 'aria-valuenow': rung.holders, - 'aria-label': "Studenci z rangą #{rung.rank.name}" } + 'aria-label': t('.progress_label', name: rung.rank.name) } %i.gh-octagon-clip{ style: "--gh-p: #{gh_percent(rung.holders, ladder.total_students)}%" } = link_to edit_story_group_rank_path(@story_group, rung.rank), class: 'gh-btn gh-btn--secondary gh-btn--small' do %i.fa-solid.fa-pen - Edytuj + = t('shared.actions.edit') - else %section.gh-panel.gh-empty-state - %h2.gh-h2 Nie ma jeszcze żadnych rang - %p.gh-lead - Dodaj rangi, żeby studenci widzieli, do czego dążą. Ranga zależy od łącznie - zebranej waluty, a każda może dawać zniżkę w sklepie. + %h2.gh-h2= t('.empty_title') + %p.gh-lead= t('.empty_body') diff --git a/app/views/ranks/confirm_destroy.html.haml b/app/views/ranks/confirm_destroy.html.haml index ed89b182..abcd9cd8 100644 --- a/app/views/ranks/confirm_destroy.html.haml +++ b/app/views/ranks/confirm_destroy.html.haml @@ -1,4 +1,4 @@ -- content_for :title, "Usuń rangę #{@rank.name}" +- content_for :title, t('.page_title', name: @rank.name) - if @in_modal = turbo_frame_tag 'modal' do diff --git a/app/views/ranks/edit.html.haml b/app/views/ranks/edit.html.haml index c3f29608..53ac8cec 100644 --- a/app/views/ranks/edit.html.haml +++ b/app/views/ranks/edit.html.haml @@ -1,3 +1,3 @@ -- content_for :title, "Edytuj rangę - #{@rank.name}" +- content_for :title, t('.page_title', name: @rank.name) = render 'form', rank: @rank, ladder: @ladder diff --git a/app/views/ranks/index.html.haml b/app/views/ranks/index.html.haml index d07fe499..83478dba 100644 --- a/app/views/ranks/index.html.haml +++ b/app/views/ranks/index.html.haml @@ -1,4 +1,4 @@ -- content_for :title, 'Rangi' +- content_for :title, t('.title') - if @ladder.membership = render 'student_ladder', ladder: @ladder diff --git a/app/views/ranks/new.html.haml b/app/views/ranks/new.html.haml index 617e052f..37ce92ec 100644 --- a/app/views/ranks/new.html.haml +++ b/app/views/ranks/new.html.haml @@ -1,3 +1,3 @@ -- content_for :title, 'Nowa ranga' +- content_for :title, t('.page_title') = render 'form', rank: @rank, ladder: @ladder diff --git a/app/views/sessions/new.html.haml b/app/views/sessions/new.html.haml index 86cac741..e9cc69ea 100644 --- a/app/views/sessions/new.html.haml +++ b/app/views/sessions/new.html.haml @@ -1,19 +1,19 @@ -- content_for :title, 'Zaloguj się' +- content_for :title, t('.title') -%h1.gh-h1 Zaloguj się -%p.gh-lead Wybierz sposób, z którego korzystasz na swojej uczelni lub w szkole. +%h1.gh-h1= t('.title') +%p.gh-lead= t('.lead') .gh-button-stack = button_to '/auth/uam_usos', class: 'gh-btn', data: { turbo: false } do %i.fa-solid.fa-graduation-cap - Zaloguj się przez USOS - %p.gh-signed-in-as Studenci i nauczyciele uczelni + = t('.usos') + %p.gh-signed-in-as= t('.usos_hint') - .gh-auth-divider albo + .gh-auth-divider= t('.or') = link_to new_auth_passwordless_path, class: 'gh-btn gh-btn--secondary' do %i.fa-solid.fa-envelope - Zaloguj się e-mailem - %p.gh-signed-in-as Uczniowie szkół i osoby dodane przez administratora + = t('.email') + %p.gh-signed-in-as= t('.email_hint') = render 'shared/register_prompt' diff --git a/app/views/shared/_category_row.html.haml b/app/views/shared/_category_row.html.haml index 217e065a..7704cdee 100644 --- a/app/views/shared/_category_row.html.haml +++ b/app/views/shared/_category_row.html.haml @@ -7,14 +7,14 @@ data: { sheet_form_target: 'row', gh_locked: locked.to_s } } = f.hidden_field :position, data: { sheet_form_target: 'position' } - %span.gh-category-drag-handle{ title: 'Przeciągnij, aby zmienić kolejność', 'aria-hidden': 'true' } + %span.gh-category-drag-handle{ title: t('.drag'), 'aria-hidden': 'true' } %i.fa-solid.fa-grip-vertical .gh-category-row-body .gh-text-input{ class: ('gh-text-input--invalid' if error.call(:didactic_description)) } = f.text_field :didactic_description, autocomplete: 'off', - placeholder: 'Za co, np. Pomoc innym', - 'aria-label': "Kategoria #{number}: za co", + placeholder: t('.name_placeholder'), + 'aria-label': t('shared.sheet_form.js.name_label', number: number), 'aria-invalid': (error.call(:didactic_description) ? 'true' : nil), data: { sheet_form_target: 'name', action: 'sheet-form#refresh' } - if error.call(:didactic_description) @@ -25,33 +25,32 @@ %details.gh-story-disclosure{ open: category.story_description.present? } %summary.gh-category-story-toggle.gh-link-button %i.fa-solid.fa-align-left{ 'aria-hidden': 'true' } - Dodaj opis fabularny + = t('.add_story') .gh-text-input.gh-category-story-textarea = f.text_area :story_description, rows: 1, - placeholder: 'Opis fabularny, np. Holowanie uszkodzonego statku', - 'aria-label': "Opis fabularny kategorii #{number}" + placeholder: t('.story_placeholder'), + 'aria-label': t('shared.sheet_form.js.story_label', number: number) - if sheet && category.persisted? && category.sheet_only? - %span.gh-tag.gh-tag--sheet-only Tylko w tym arkuszu + %span.gh-tag.gh-tag--sheet-only= t('.sheet_only') - if sheet %span.gh-tag.gh-tag--hidden-column{ hidden: !category.hidden?, data: { sheet_form_target: 'hiddenTag' } } - Ukryta w tabeli ocen + = t('.hidden_column') - if locked - - awards = category.awards_count %p.gh-category-lock-note %i.fa-solid.fa-lock{ 'aria-hidden': 'true' } %span - = "Przyznano #{awards} #{gh_plural(awards, 'nagrodę', 'nagrody', 'nagród')}." - Kolumny nie można usunąć, można ją ukryć. + = t('.awarded', count: category.awards_count) + = t('.locked_note') %span{ hidden: (category.reward == category.reward_was), data: { sheet_form_target: 'rewardNote' } } - Nowa wartość obejmie tylko przyszłe nagrody. + = t('.reward_note') .gh-category-row-price .gh-text-input.gh-text-input--numeric{ class: ('gh-text-input--invalid' if error.call(:reward)) } = f.number_field :reward, min: 1, step: 1, - 'aria-label': "Nagroda za kategorię #{number}", + 'aria-label': t('shared.sheet_form.js.reward_label', number: number), 'aria-invalid': (error.call(:reward) ? 'true' : nil), data: { sheet_form_target: 'reward', action: 'sheet-form#refresh', gh_awarded: (locked ? category.reward_was : nil) } @@ -62,21 +61,23 @@ %span= error.call(:reward) .gh-category-row-actions - %button.gh-icon-button{ type: 'button', 'aria-label': 'Przesuń wyżej', + %button.gh-icon-button{ type: 'button', 'aria-label': t('.move_up'), data: { action: 'sheet-form#moveUp', sheet_form_target: 'up' } } %i.fa-solid.fa-chevron-up{ 'aria-hidden': 'true' } - %button.gh-icon-button{ type: 'button', 'aria-label': 'Przesuń niżej', + %button.gh-icon-button{ type: 'button', 'aria-label': t('.move_down'), data: { action: 'sheet-form#moveDown', sheet_form_target: 'down' } } %i.fa-solid.fa-chevron-down{ 'aria-hidden': 'true' } - if locked - %label.gh-icon-button.gh-icon-toggle{ title: (category.hidden? ? 'Pokaż' : 'Ukryj') + ' w tabeli ocen' } + - toggle = category.hidden? ? t('.show') : t('.hide') + %label.gh-icon-button.gh-icon-toggle{ title: toggle } = f.check_box :hidden, { data: { sheet_form_target: 'hide', action: 'sheet-form#refresh' }, - 'aria-label': (category.hidden? ? 'Pokaż w tabeli ocen' : 'Ukryj w tabeli ocen') } + 'aria-label': toggle } %i.fa-solid{ class: (category.hidden? ? 'fa-eye' : 'fa-eye-slash'), 'aria-hidden': 'true', data: { sheet_form_target: 'hideIcon' } } - else - %label.gh-icon-button.gh-icon-toggle{ title: 'Usuń' } + %label.gh-icon-button.gh-icon-toggle{ title: t('shared.actions.remove') } = f.check_box :_destroy, { data: { sheet_form_target: 'destroy', action: 'sheet-form#remove' }, - 'aria-label': "Usuń kategorię #{number}" } + 'aria-label': t(sheet ? 'shared.sheet_form.js.destroy_column' : 'shared.sheet_form.js.destroy_category', + number: number) } %i.fa-solid.fa-trash{ 'aria-hidden': 'true' } diff --git a/app/views/shared/_grid_preview.html.haml b/app/views/shared/_grid_preview.html.haml index eddd20e6..55fb42dc 100644 --- a/app/views/shared/_grid_preview.html.haml +++ b/app/views/shared/_grid_preview.html.haml @@ -1,18 +1,18 @@ -- sample = local_assigns[:sample_names].presence || ['Adam Pawłowski', 'Barbara Kowalewska'] +- sample = local_assigns[:sample_names].presence || t('shared.sheet_form.sample_names') %details.gh-preview-details{ open: true } %summary - Podgląd tabeli ocen + = t('.summary') %i.fa-solid.fa-chevron-down{ 'aria-hidden': 'true' } %section.gh-panel.gh-preview-panel - %h3.gh-h3 Nagłówek tabeli ocen + %h3.gh-h3= t('.header') .gh-grading-table-preview %table.gh-grading-table %thead %tr{ data: { sheet_form_target: 'previewHead' } } - %th.gh-grading-th-student{ scope: 'col' } Student + %th.gh-grading-th-student{ scope: 'col' }= t('.student') - categories.each do |category| %th{ scope: 'col' } .gh-grading-column-head @@ -28,7 +28,7 @@ %td %span.gh-grading-cell.gh-grading-cell--sm - %span.gh-field-hint Najedź na nagłówek w prawdziwej tabeli, żeby zobaczyć opis fabularny. + %span.gh-field-hint= t('.hint') %template{ data: { sheet_form_target: 'previewHeadCell' } } %th{ scope: 'col' } diff --git a/app/views/shared/_image_field.html.haml b/app/views/shared/_image_field.html.haml index 3e0b3e25..8ff19dc1 100644 --- a/app/views/shared/_image_field.html.haml +++ b/app/views/shared/_image_field.html.haml @@ -2,13 +2,7 @@ - glyph_attribute = local_assigns.fetch(:glyph_attribute, :icon_glyph) - registry = local_assigns.fetch(:registry, Glyphs) - tile = local_assigns.fetch(:tile, :glyph) -- defaults = { label: 'Grafika', - presets: 'Gotowe grafiki', - own: 'Twoja grafika', - upload: 'Albo wgraj własną grafikę', - hint: 'JPG lub PNG, najlepiej co najmniej 400 px.', - crop: 'Przytnij grafikę', - cropped: 'Przesuń i przeskaluj zaznaczenie — podgląd obok zmienia się na bieżąco. Zapisujemy kadr w kształcie karty, więc wypełni ją w całości. Na listach zobaczysz go w kwadratowej miniaturze, też w całości.', } +- defaults = t('shared.image_field').slice(:label, :presets, :own, :upload, :hint, :crop, :cropped) - copy = defaults.merge(local_assigns.fetch(:copy, {})) - field_id = ->(suffix) { "gh-#{kind}-art-#{suffix}" } @@ -19,7 +13,8 @@ .gh-form-field{ data: { controller: 'image-crop', image_crop_aspect_value: local_assigns.fetch(:aspect, 1.6), - image_crop_width_value: 1024, } } + image_crop_width_value: 1024, + image_crop_alt_value: copy[:own], } } %p.gh-form-label= copy[:label] .gh-art-preset-grid{ class: "gh-art-preset-grid--#{kind}", role: 'radiogroup', 'aria-label': copy[:presets], @@ -29,7 +24,7 @@ %label.gh-art-preset-tile{ class: "gh-art-preset-tile--#{kind}" } = form.radio_button glyph_attribute, key, class: 'sr-only', id: field_id.call(key), - 'aria-label': "#{copy[:label]}: #{registry.label_for(key)}", + 'aria-label': t('.preset_label', label: copy[:label], name: registry.label_for(key)), data: { action: ['image-crop#pickPreset', refresh].compact.join(' ') } %i.gh-octagon-clip - case tile @@ -63,7 +58,7 @@ %b= copy[:upload] %small= copy[:hint] %label.gh-btn.gh-btn--secondary.gh-btn--small - Wybierz plik + = t('.choose_file') = form.file_field attribute, class: 'sr-only', accept: 'image/png,image/jpeg,image/gif', data: { image_crop_target: 'input', action: 'image-crop#choose' } diff --git a/app/views/shared/_invitation_email.html.haml b/app/views/shared/_invitation_email.html.haml index 664ba2df..de93e341 100644 --- a/app/views/shared/_invitation_email.html.haml +++ b/app/views/shared/_invitation_email.html.haml @@ -9,20 +9,17 @@ %td.gh-invitation-email-body %p.gh-invitation-email-brand Gamification Hub - %h1.gh-invitation-email-title Zaproszenie + %h1.gh-invitation-email-title= t('.title') %p.gh-invitation-email-lead= lead - %p.gh-invitation-email-text - Kliknij przycisk poniżej, aby dokończyć zakładanie konta i uzupełnić swoje dane. + %p.gh-invitation-email-text= t('.text') %table{ cellpadding: 0, cellspacing: 0, border: 0 } %tr %td.gh-invitation-email-button{ align: 'center' } - %a.gh-invitation-email-button-link{ href: token_link } Dokończ zakładanie konta + %a.gh-invitation-email-button-link{ href: token_link }= t('.button') - %p.gh-invitation-email-note - Link jest ważny przez 7 dni. Jeśli przycisk nie działa, skopiuj ten adres do przeglądarki: + %p.gh-invitation-email-note= t('.note') %p.gh-invitation-email-url %a{ href: token_link }= token_link - %p.gh-invitation-email-footer - Jeśli nie spodziewałeś się tego zaproszenia, możesz zignorować tę wiadomość. + %p.gh-invitation-email-footer= t('.footer') diff --git a/app/views/shared/_invitation_email.text.haml b/app/views/shared/_invitation_email.text.haml index e9924020..d65a4526 100644 --- a/app/views/shared/_invitation_email.text.haml +++ b/app/views/shared/_invitation_email.text.haml @@ -2,10 +2,10 @@ Gamification Hub = lead -Aby dokończyć zakładanie konta i uzupełnić swoje dane, otwórz poniższy link. += t('.text_plain') = token_link -Link jest ważny przez 7 dni. += t('.valid_for') -Jeśli nie spodziewałeś się tego zaproszenia, możesz zignorować tę wiadomość. += t('.footer') diff --git a/app/views/shared/_register_prompt.html.haml b/app/views/shared/_register_prompt.html.haml index 0d8c51e4..3398e57a 100644 --- a/app/views/shared/_register_prompt.html.haml +++ b/app/views/shared/_register_prompt.html.haml @@ -1,3 +1,3 @@ %p.gh-auth-alt-action - Nie masz konta? - %button.gh-link-button{ type: 'button' } Zarejestruj się + = t('.no_account') + %button.gh-link-button{ type: 'button' }= t('.register') diff --git a/app/views/shared/_sheet_form.html.haml b/app/views/shared/_sheet_form.html.haml index 93abc57f..1ba6a2a5 100644 --- a/app/views/shared/_sheet_form.html.haml +++ b/app/views/shared/_sheet_form.html.haml @@ -8,29 +8,28 @@ - sheet_names = local_assigns[:sheet_names] || [] - awards_count = local_assigns[:awards_count].to_i -- sample_names = @story_group.student_memberships.with_user.first(2).map(&:display_name).presence || ['Adam Pawłowski', 'Barbara Kowalewska'] +- sample_names = @story_group.student_memberships.with_user.first(2).map(&:display_name).presence || t('.sample_names') -- headings = { template_new: 'Nowy szablon arkusza', template_edit: 'Edytuj szablon arkusza', sheet: 'Ustawienia arkusza' } +- headings = { template_new: t('.template_new_title'), template_edit: t('.template_edit_title'), + sheet: t('.sheet_title') } - heading = headings[variant] - crumb_tail = sheet ? record.name : (record.base_name_was.presence || record.base_name) -- crumb_tail = 'Nowy szablon' if variant == :template_new +- crumb_tail = t('.new_template') if variant == :template_new - form_data = { controller: 'sheet-form', - sheet_form_columns_value: %w[kolumna kolumny kolumn].to_json, - sheet_form_currency_value: @story_group.currency_name.to_s, - sheet_form_category_label_value: (sheet ? 'kolumnę' : 'kategorię'), + sheet_form_i18n_value: t('.js').to_json, + sheet_form_sheet_value: sheet, sheet_form_editing_value: editing.to_s } %div{ data: form_data } %section.gh-panel.gh-page-header.gh-page-header--stacked %p.gh-breadcrumbs - = link_to 'Arkusze ocen', story_group_activity_groups_path(@story_group), + = link_to t('.breadcrumb'), story_group_activity_groups_path(@story_group), data: { action: 'sheet-form#leave' } %i.fa-solid.fa-chevron-right{ 'aria-hidden': 'true' } %span= crumb_tail %h1.gh-h1= heading - %p.gh-lead - = sheet ? 'Zmiany dotyczą tylko tego arkusza.' : 'Podgląd obok pokazuje nagłówek tabeli ocen.' + %p.gh-lead= sheet ? t('.sheet_lead') : t('.template_lead') = form_with model: [@story_group, record], data: { sheet_form_target: 'form' } do |f| .gh-form-shell @@ -39,18 +38,17 @@ %p.gh-info-banner %i.fa-solid.fa-table{ 'aria-hidden': 'true' } %span - %b Zmiany dotyczą tylko arkuszy utworzonych od teraz. + %b= t('.template_changes') - if sheet_names.many? - = "#{sheet_names.first} – #{sheet_names.last} zostają bez zmian." + = t('.sheets_range_unchanged', first: sheet_names.first, last: sheet_names.last) - else - = "#{sheet_names.first} zostaje bez zmian." + = t('.sheet_unchanged', name: sheet_names.first) - if sheet && awards_count.positive? %p.gh-info-banner %i.fa-solid.fa-lock{ 'aria-hidden': 'true' } %span - %b= "W tym arkuszu przyznano już #{awards_count} #{gh_plural(awards_count, 'nagrodę', 'nagrody', 'nagród')}." - Zmiany kolumn dotyczą tylko przyszłych nagród. - = "Szablon #{record.activity_group_template.base_name} się nie zmienia." + %b= t('.awarded', count: awards_count) + = t('.awarded_note', name: record.activity_group_template.base_name) - if record.errors[:base].any? %p.gh-field-error{ role: 'alert' } @@ -59,10 +57,10 @@ .gh-form-section .gh-form-field.mt-0 - = f.label name_attr, sheet ? 'Nazwa arkusza' : 'Nazwa szablonu' + = f.label name_attr, sheet ? t('.sheet_name') : t('.template_name') .gh-text-input{ class: ('gh-text-input--invalid' if error.call(name_attr)) } = f.text_field name_attr, maxlength: 100, autocomplete: 'off', - placeholder: (sheet ? 'np. Laboratoria 5 (14.10)' : 'np. Laboratoria'), + placeholder: (sheet ? t('.sheet_name_placeholder') : t('.template_name_placeholder')), 'aria-invalid': (error.call(name_attr) ? 'true' : nil), 'aria-describedby': (error.call(name_attr) ? 'gh-sheet-name-error' : nil), data: { sheet_form_target: 'title', action: 'sheet-form#refresh' } @@ -72,13 +70,11 @@ %span= error.call(name_attr) - unless sheet %span.gh-field-hint - Nowe arkusze dostaną nazwę szablonu z kolejnym numerem, np. - = "#{record.base_name.presence || 'Laboratoria'} 6." + = t('.template_name_hint', example: record.base_name.presence || t('.name_example')) .gh-form-section - %h2.gh-h3= sheet ? 'Kolumny w tabeli ocen' : 'Kategorie nagród' - %p.gh-field-hint - Każda kategoria to kolumna w tabeli ocen. Kolejność tutaj to kolejność kolumn. + %h2.gh-h3= sheet ? t('.sheet_columns') : t('.template_categories') + %p.gh-field-hint= t('.columns_hint') %ul.gh-category-list{ data: { controller: 'sortable-rows', sheet_form_target: 'list', action: 'sortable-rows:reordered->sheet-form#refresh' } } @@ -88,14 +84,15 @@ %button.gh-btn.gh-btn--secondary.gh-add-category-btn{ type: 'button', data: { action: 'sheet-form#add' } } %i.fa-solid.fa-plus{ 'aria-hidden': 'true' } - = sheet ? 'Dodaj kolumnę' : 'Dodaj kategorię' + = sheet ? t('.add_column') : t('.add_category') %p.gh-explainer-note - %span{ data: { sheet_form_target: 'summaryCount' } } - = "#{visible.size} #{gh_plural(visible.size, 'kolumna', 'kolumny', 'kolumn')}" - w tabeli ocen. Student może zdobyć do - %b{ data: { sheet_form_target: 'summaryMax' } }= visible.sum { |category| category.reward.to_i } - = "#{@story_group.currency_name} za arkusz." + = gh_fill(t('.summary'), + count: tag.span(t('.js.columns', count: visible.size), + data: { sheet_form_target: 'summaryCount' }), + max: tag.b(visible.sum { |category| category.reward.to_i }, + data: { sheet_form_target: 'summaryMax' }), + currency: @story_group.currency_name) %template{ data: { sheet_form_target: 'blank' } } = f.fields_for cats_attr, categories.klass.new(reward: 1), child_index: 'NEW_RECORD' do |cf| @@ -104,35 +101,34 @@ - if editing .gh-form-section - %h2.gh-h3 Usuwanie - %p.gh-field-hint - = sheet ? 'Arkusz zniknie z listy, a przyznane nagrody zostają u studentów i w ich historii.' : 'Szablon zniknie z listy, a utworzone z niego arkusze zostają.' + %h2.gh-h3= t('shared.actions.removal') + %p.gh-field-hint= sheet ? t('.sheet_removal_hint') : t('.template_removal_hint') .gh-button-row - path = sheet ? confirm_destroy_story_group_activity_group_path(@story_group, record) : confirm_destroy_story_group_activity_group_template_path(@story_group, record) = link_to path, class: 'gh-btn gh-btn--secondary', data: { turbo_frame: 'modal' } do %i.fa-solid.fa-trash{ 'aria-hidden': 'true' } - = sheet ? 'Usuń arkusz' : 'Usuń szablon' + = sheet ? t('.remove_sheet') : t('.remove_template') - %aside.gh-preview-column{ 'aria-label': 'Podgląd' } + %aside.gh-preview-column{ 'aria-label': t('shared.actions.preview') } = render 'shared/grid_preview', categories: visible, sample_names: sample_names .gh-panel.gh-form-actions-bar - = link_to 'Anuluj', story_group_activity_groups_path(@story_group), class: 'gh-link-button', + = link_to t('shared.actions.cancel'), story_group_activity_groups_path(@story_group), class: 'gh-link-button', data: { action: 'sheet-form#leave' } %span.gh-form-actions-spacer - if editing - %span.gh-small{ data: { sheet_form_target: 'dirty' } } Brak zmian + %span.gh-small{ data: { sheet_form_target: 'dirty' } }= t('.no_changes') %button.gh-btn{ type: 'submit', data: { sheet_form_target: 'submit' } } - unless editing %i.fa-solid.fa-plus{ 'aria-hidden': 'true' } - = editing ? 'Zapisz zmiany' : 'Utwórz szablon' + = editing ? t('shared.actions.save_changes') : t('.create') %dialog.gh-dialog{ data: { sheet_form_target: 'discard', action: 'close->sheet-form#closeDiscard click->sheet-form#closeOnBackdrop' } } .gh-dialog-body.gh-octagon-clip - %h2.gh-h2 Odrzucić zmiany? - %p Niezapisane zmiany zostaną utracone. + %h2.gh-h2= t('.discard_title') + %p= t('.discard_body') .gh-dialog-button-row %button.gh-btn.gh-btn--secondary{ type: 'button', data: { action: 'sheet-form#closeDiscard' }, autofocus: true } - Wróć do edycji - %button.gh-btn{ type: 'button', data: { action: 'sheet-form#discard' } } Odrzuć zmiany + = t('shared.actions.back_to_editing') + %button.gh-btn{ type: 'button', data: { action: 'sheet-form#discard' } }= t('.discard') diff --git a/app/views/shop/_buy_button.html.haml b/app/views/shop/_buy_button.html.haml index 7ab73449..9b2dec04 100644 --- a/app/views/shop/_buy_button.html.haml +++ b/app/views/shop/_buy_button.html.haml @@ -1,4 +1,4 @@ = link_to confirm_buy_story_group_shop_path(@story_group, item), class: 'gh-btn', data: { turbo_frame: 'modal' }, - 'aria-label': "Kup #{item.name} za #{price}" do - = "Kup za #{price}" + 'aria-label': t('.label', name: item.name, price: price) do + = t('.buy', price: price) diff --git a/app/views/shop/_confirm_buy.html.haml b/app/views/shop/_confirm_buy.html.haml index 07010e07..1e98e1ff 100644 --- a/app/views/shop/_confirm_buy.html.haml +++ b/app/views/shop/_confirm_buy.html.haml @@ -1,36 +1,36 @@ - price = offer.price - left = @student.current_currency.to_i - price -%h2#gh-buy-title.gh-h2= "Kupić „#{item.name}”?" +%h2#gh-buy-title.gh-h2= t('.title', name: item.name) .gh-purchase-summary = render 'layouts/item_thumb', item: item %dl.gh-purchase-details - %dt Cena + %dt= t('.price') %dd - if offer.discounted? %s= offer.list_price %span.gh-buy-v= price = render 'layouts/currency_token', story_group: @story_group, size: 22 - %dt Zostanie Ci + %dt= t('.left') %dd %span.gh-buy-v= left = render 'layouts/currency_token', story_group: @story_group, size: 22 - if offer.discounted? - %p.gh-small.gh-note-row= "Zniżka #{offer.discount_label}." + %p.gh-small.gh-note-row= t('.discount', label: offer.discount_label) %p.gh-small.gh-note-row %i.fa-solid.fa-bell{ 'aria-hidden': 'true' } - %span Prowadzący dostanie powiadomienie o zakupie. + %span= t('.notice') .gh-dialog-button-row - if @in_modal %button.gh-btn.gh-btn--secondary{ type: 'button', data: { action: 'dialog#close' }, autofocus: true } - Anuluj + = t('shared.actions.cancel') - else - = link_to 'Anuluj', story_group_shop_index_path(@story_group), class: 'gh-btn gh-btn--secondary' + = link_to t('shared.actions.cancel'), story_group_shop_index_path(@story_group), class: 'gh-btn gh-btn--secondary' - = button_to "Kup za #{price}", buy_story_group_shop_path(@story_group, item), + = button_to t('.buy', price: price), buy_story_group_shop_path(@story_group, item), method: :post, class: 'gh-btn', form: { data: { turbo_frame: '_top' } } diff --git a/app/views/shop/confirm_buy.html.haml b/app/views/shop/confirm_buy.html.haml index c7df3eca..7203ad10 100644 --- a/app/views/shop/confirm_buy.html.haml +++ b/app/views/shop/confirm_buy.html.haml @@ -1,4 +1,4 @@ -- content_for :title, "Kup #{@item.name}" +- content_for :title, t('.page_title', name: @item.name) - if @in_modal = turbo_frame_tag 'modal' do diff --git a/app/views/shop/index.html.haml b/app/views/shop/index.html.haml index 741ddb94..fa65772b 100644 --- a/app/views/shop/index.html.haml +++ b/app/views/shop/index.html.haml @@ -1,8 +1,8 @@ -- content_for :title, 'Sklep' +- content_for :title, t('.title') %section.gh-panel.gh-page-header .gh-page-header-text - %h1.gh-h1 Sklep + %h1.gh-h1= t('.title') %p.gh-lead = shop_balance_sentence(@shop.balance) = shop_discount_sentence(@shop.discount_badges) @@ -11,11 +11,11 @@ = render 'layouts/currency_token', story_group: @story_group, size: 32 %div %b= @shop.balance - %small do wydania + %small= t('.to_spend') %span.gh-wallet-divider{ 'aria-hidden': 'true' } %div %b= @shop.total - %small zebrane łącznie + %small= t('.collected') - if @shop.any? - @shop.zones.each do |zone| @@ -28,7 +28,5 @@ = render 'shop/card', offer: offer, state: zone.key - else %section.gh-panel.gh-empty-state - %h2.gh-h2 Sklep jest jeszcze pusty - %p.gh-lead - Prowadzący nie dodał jeszcze przedmiotów. Zbieraj walutę — przyda się, - kiedy sklep się otworzy. + %h2.gh-h2= t('.empty_title') + %p.gh-lead= t('.empty_body') diff --git a/app/views/story_group_invites/_cancel.html.haml b/app/views/story_group_invites/_cancel.html.haml index cb598c7e..0dc8c08a 100644 --- a/app/views/story_group_invites/_cancel.html.haml +++ b/app/views/story_group_invites/_cancel.html.haml @@ -1,4 +1,4 @@ - if @in_modal - %button.gh-btn.gh-btn--secondary{ type: 'button', data: { action: 'dialog#close' } } Anuluj + %button.gh-btn.gh-btn--secondary{ type: 'button', data: { action: 'dialog#close' } }= t('shared.actions.cancel') - else - = link_to 'Anuluj', story_group_invites_path(@story_group), class: 'gh-btn gh-btn--secondary' + = link_to t('shared.actions.cancel'), story_group_invites_path(@story_group), class: 'gh-btn gh-btn--secondary' diff --git a/app/views/story_group_invites/_code.html.haml b/app/views/story_group_invites/_code.html.haml index 7dbe13a9..3a34da84 100644 --- a/app/views/story_group_invites/_code.html.haml +++ b/app/views/story_group_invites/_code.html.haml @@ -1,10 +1,10 @@ -%h2#gh-code-title.gh-h2= "Dołącz do grupy #{@story_group.name}" -%p Wpisz kod w aplikacji („Dołącz do grupy”) albo zeskanuj kod QR. +%h2#gh-code-title.gh-h2= t('.title', name: @story_group.name) +%p= t('.hint') = render 'qr_stat', invite: invite .gh-dialog-button-row - if @in_modal - %button.gh-btn{ type: 'button', data: { action: 'dialog#close' }, autofocus: true } Zamknij + %button.gh-btn{ type: 'button', data: { action: 'dialog#close' }, autofocus: true }= t('shared.actions.close') - else - = link_to 'Zamknij', story_group_invites_path(@story_group), class: 'gh-btn' + = link_to t('shared.actions.close'), story_group_invites_path(@story_group), class: 'gh-btn' diff --git a/app/views/story_group_invites/_confirm_destroy.html.haml b/app/views/story_group_invites/_confirm_destroy.html.haml index c73c2be2..87ea690c 100644 --- a/app/views/story_group_invites/_confirm_destroy.html.haml +++ b/app/views/story_group_invites/_confirm_destroy.html.haml @@ -1,18 +1,17 @@ - joined = invite.uses.to_i -%h2#gh-del-title.gh-h2= "Usunąć zaproszenie #{invite.code}?" +%h2#gh-del-title.gh-h2= t('.title', code: invite.code) %p - Kod przestanie działać od razu. + = t('.body') - if joined.positive? - = "#{joined} #{gh_plural(joined, 'osoba, która dołączyła', 'osoby, które dołączyły', 'osób, które dołączyły')}" - = 'z tym kodem, zostaje w grupie.' + = t('.joined', count: joined) .gh-dialog-button-row - if @in_modal - %button.gh-btn.gh-btn--secondary{ type: 'button', data: { action: 'dialog#close' }, autofocus: true } Anuluj + %button.gh-btn.gh-btn--secondary{ type: 'button', data: { action: 'dialog#close' }, autofocus: true }= t('shared.actions.cancel') - else - = link_to 'Anuluj', story_group_invites_path(@story_group), class: 'gh-btn gh-btn--secondary' + = link_to t('shared.actions.cancel'), story_group_invites_path(@story_group), class: 'gh-btn gh-btn--secondary' - = button_to 'Usuń zaproszenie', story_group_invite_path(@story_group, invite), + = button_to t('.remove'), story_group_invite_path(@story_group, invite), method: :delete, class: 'gh-btn', form: { data: { turbo_frame: (@in_modal ? 'modal' : nil) } } diff --git a/app/views/story_group_invites/_copy_button.html.haml b/app/views/story_group_invites/_copy_button.html.haml index 2c42f4d4..91a5d1c4 100644 --- a/app/views/story_group_invites/_copy_button.html.haml +++ b/app/views/story_group_invites/_copy_button.html.haml @@ -1,5 +1,5 @@ - labelled = local_assigns.fetch(:labelled, true) -- title = "Kopiuj kod #{invite.code}" +- title = t('.title', code: invite.code) %button{ type: 'button', class: labelled ? 'gh-btn gh-btn--secondary gh-copy-action' : 'gh-icon-button', @@ -9,8 +9,8 @@ action: 'clipboard#copy', clipboard_title: title, clipboard_text_value: invite.code, - clipboard_done_value: 'Skopiowano', - clipboard_message_value: "Skopiowano kod #{invite.code}." } } + clipboard_done_value: t('.done'), + clipboard_message_value: t('.message', code: invite.code) } } %i.fa-solid.fa-copy{ 'aria-hidden': 'true', data: { clipboard_target: 'icon' } } - if labelled - %span{ data: { clipboard_target: 'label' } } Kopiuj kod + %span{ data: { clipboard_target: 'label' } }= t('.label') diff --git a/app/views/story_group_invites/_form.html.haml b/app/views/story_group_invites/_form.html.haml index 499dfa19..c2f1b6ca 100644 --- a/app/views/story_group_invites/_form.html.haml +++ b/app/views/story_group_invites/_form.html.haml @@ -5,33 +5,23 @@ - scope = ->(field) { "story_group_invite[#{field}]" } %h2#gh-invite-title.gh-h2 - = editing ? "Edytuj zaproszenie #{invite.code}" : 'Nowe zaproszenie' -%p - - if editing - Kod zostaje ten sam. Zmieniasz tylko ograniczenia. - - else - Oba ograniczenia są opcjonalne. + = editing ? t('.edit_title', code: invite.code) : t('.new_title') +%p= editing ? t('.edit_hint') : t('.new_hint') = form_with url: (editing ? story_group_invite_path(@story_group, invite) : story_group_invites_path(@story_group)), method: (editing ? :patch : :post), data: { controller: 'invite-form', turbo_frame: (@in_modal ? 'modal' : nil), - invite_form_summary_prefix_value: 'Kod będzie działał', - invite_form_limit_prefix_value: 'dla', - invite_form_no_limit_value: 'bez limitu osób', - invite_form_expiry_prefix_value: 'do', - invite_form_no_expiry_value: 'i bez daty ważności', - invite_form_people_forms_value: %w[osoby osób osób].to_json, - invite_form_unit_forms_value: %w[osoba osoby osób].to_json } do + invite_form_i18n_value: invite_form_wording.to_json } do - unless editing .gh-invite-presets %button.gh-purchase-filter-chip{ type: 'button', data: { preset: 'today', action: 'invite-form#preset' } } - Na dzisiejsze zajęcia + = t('.today') %button.gh-purchase-filter-chip{ type: 'button', data: { preset: 'none', action: 'invite-form#preset' } } - Bez ograniczeń + = t('.no_limits') .gh-invite-limits .gh-toggle-switch-row @@ -41,13 +31,13 @@ data: { invite_form_target: 'limitSwitch', action: 'invite-form#refresh' } %span %b - = label_tag 'gh-limit-switch', 'Limit użyć' - %span.gh-field-hint Kod przestanie działać, gdy dołączy tyle osób. + = label_tag 'gh-limit-switch', t('.limit') + %span.gh-field-hint= t('.limit_hint') .gh-limit-fields-row .gh-text-input.gh-text-input--numeric{ class: ('gh-text-input--invalid' if limit_error) } = number_field_tag scope.call(:max_uses), form.max_uses, min: 1, step: 1, - id: 'gh-max-uses', 'aria-label': 'Limit użyć', + id: 'gh-max-uses', 'aria-label': t('.limit'), 'aria-invalid': limit_error ? 'true' : nil, 'aria-describedby': (limit_error ? 'gh-limit-error' : nil), data: { invite_form_target: 'maxUses', action: 'invite-form#refresh' } @@ -66,20 +56,20 @@ data: { invite_form_target: 'expirySwitch', action: 'invite-form#refresh' } %span %b - = label_tag 'gh-expiry-switch', 'Data ważności' - %span.gh-field-hint Po tej chwili kod przestanie działać. + = label_tag 'gh-expiry-switch', t('.expiry') + %span.gh-field-hint= t('.expiry_hint') .gh-limit-fields-row .gh-text-input{ class: ('gh-text-input--invalid' if expiry_error) } = date_field_tag scope.call(:expires_on), form.expires_on, - id: 'gh-expires-on', 'aria-label': 'Data', + id: 'gh-expires-on', 'aria-label': t('.date'), 'aria-invalid': expiry_error ? 'true' : nil, 'aria-describedby': (expiry_error ? 'gh-expiry-error' : nil), data: { invite_form_target: 'date', today: Time.zone.today.to_fs(:iso8601), action: 'invite-form#refresh' } .gh-text-input = time_field_tag scope.call(:expires_time), form.expires_time, - id: 'gh-expires-time', 'aria-label': 'Godzina', + id: 'gh-expires-time', 'aria-label': t('.time'), data: { invite_form_target: 'time', end_of_day: InviteForm::DEFAULT_TIME, action: 'invite-form#refresh' } @@ -92,4 +82,4 @@ .gh-dialog-button-row = render 'cancel' - %button.gh-btn{ type: 'submit' }= editing ? 'Zapisz zmiany' : 'Utwórz zaproszenie' + %button.gh-btn{ type: 'submit' }= editing ? t('shared.actions.save_changes') : t('.create') diff --git a/app/views/story_group_invites/_invite_row.html.haml b/app/views/story_group_invites/_invite_row.html.haml index 6d0fdf5e..015a8508 100644 --- a/app/views/story_group_invites/_invite_row.html.haml +++ b/app/views/story_group_invites/_invite_row.html.haml @@ -7,13 +7,13 @@ %span.gh-pill.gh-octagon-clip{ class: invite.active? ? 'gh-pill--earn' : 'gh-pill--corr' }= invite.status_label %span.gh-invite-usage - = "Użycia: #{invite_uses_label(invite)}" + = t('.uses', uses: invite_uses_label(invite)) - if invite.max_uses .gh-progress-bar.gh-octagon-clip{ role: 'progressbar', 'aria-valuemin': 0, 'aria-valuemax': invite.max_uses, 'aria-valuenow': invite.uses, - 'aria-label': "Wykorzystanie kodu #{invite.code}" } + 'aria-label': t('.usage_label', code: invite.code) } %i.gh-octagon-clip{ style: "--gh-p: #{gh_percent(invite.uses, invite.max_uses)}%" } %span.gh-invite-expiry= invite_expiry_label(invite) @@ -23,16 +23,16 @@ = link_to story_group_invite_path(@story_group, invite), class: 'gh-btn gh-btn--small', data: { turbo_frame: 'modal' } do %i.fa-solid.fa-qrcode - Pokaż + = t('.show') = render 'copy_button', invite: invite, labelled: false = link_to edit_story_group_invite_path(@story_group, invite), class: 'gh-icon-button', - title: 'Edytuj', 'aria-label': "Edytuj zaproszenie #{invite.code}", + title: t('shared.actions.edit'), 'aria-label': t('.edit_label', code: invite.code), data: { turbo_frame: 'modal' } do %i.fa-solid.fa-pen = link_to confirm_destroy_story_group_invite_path(@story_group, invite), class: 'gh-icon-button', - title: 'Usuń', 'aria-label': "Usuń zaproszenie #{invite.code}", + title: t('.remove'), 'aria-label': t('.remove_label', code: invite.code), data: { turbo_frame: 'modal' } do %i.fa-solid.fa-trash diff --git a/app/views/story_group_invites/_quick.html.haml b/app/views/story_group_invites/_quick.html.haml index d5c1a2b7..9db03f63 100644 --- a/app/views/story_group_invites/_quick.html.haml +++ b/app/views/story_group_invites/_quick.html.haml @@ -1,10 +1,10 @@ -%h2.gh-h2#gh-quick-title= "Dołącz do grupy #{@story_group.name}" -%p Wpisz kod w aplikacji („Dołącz do grupy”) albo zeskanuj kod QR. +%h2.gh-h2#gh-quick-title= t('.title', name: @story_group.name) +%p= t('.hint') - if active.any? .gh-form-field{ data: { controller: 'invite-picker' } } %label{ for: 'gh-quick-select' } - = "Zaproszenie (aktywne: #{active.size}, najnowsze na górze)" + = t('.picker', count: active.size) %span.gh-select-input %select#gh-quick-select{data: { invite_picker_target: 'select', action: 'change->invite-picker#pick', }} @@ -12,26 +12,24 @@ %option{ value: invite.id }= invite_option_label(invite) %i.fa-solid.fa-chevron-down{ 'aria-hidden': 'true' } - %p.gh-small - Wygasłe i wyczerpane zaproszenia są ukryte. Wszystkie znajdziesz w - zakładce Zaproszenia. + %p.gh-small= t('.hidden_note') - active.each_with_index do |invite, index| .gh-quick-block{hidden: index.positive?, data: { invite_picker_target: 'block', invite_picker_for: invite.id },} = render 'qr_stat', invite: invite - else - %p.gh-explainer-note Brak aktywnych zaproszeń. Utwórz nowe, żeby studenci mogli dołączyć. + %p.gh-explainer-note= t('.empty') .gh-button-row.gh-quick-invite-cta = link_to new_story_group_invite_path(@story_group), class: 'gh-btn', data: { turbo_frame: 'modal' } do %i.fa-solid.fa-plus{ 'aria-hidden': 'true' } - Nowe zaproszenie + = t('.new') .gh-dialog-button-row - = link_to 'Zarządzaj zaproszeniami', story_group_invites_path(@story_group), + = link_to t('.manage'), story_group_invites_path(@story_group), class: 'gh-btn gh-btn--secondary', data: { turbo_frame: '_top' } - if @in_modal - %button.gh-btn{ type: 'button', data: { action: 'dialog#close' }, autofocus: true } Zamknij + %button.gh-btn{ type: 'button', data: { action: 'dialog#close' }, autofocus: true }= t('shared.actions.close') - else - = link_to 'Zamknij', story_group_invites_path(@story_group), class: 'gh-btn' + = link_to t('shared.actions.close'), story_group_invites_path(@story_group), class: 'gh-btn' diff --git a/app/views/story_group_invites/confirm_destroy.html.haml b/app/views/story_group_invites/confirm_destroy.html.haml index c2ec927b..8a0edee8 100644 --- a/app/views/story_group_invites/confirm_destroy.html.haml +++ b/app/views/story_group_invites/confirm_destroy.html.haml @@ -1,4 +1,4 @@ -- content_for :title, "Usunąć zaproszenie #{@invite.code}?" +- content_for :title, t('.page_title', code: @invite.code) - if @in_modal = turbo_frame_tag 'modal' do diff --git a/app/views/story_group_invites/edit.html.haml b/app/views/story_group_invites/edit.html.haml index a3598911..7a98ee3b 100644 --- a/app/views/story_group_invites/edit.html.haml +++ b/app/views/story_group_invites/edit.html.haml @@ -1,4 +1,4 @@ -- content_for :title, "Edytuj zaproszenie #{@invite.code}" +- content_for :title, t('.page_title', code: @invite.code) - if @in_modal = turbo_frame_tag 'modal' do diff --git a/app/views/story_group_invites/index.html.haml b/app/views/story_group_invites/index.html.haml index 3f2b6dc1..5f4113a2 100644 --- a/app/views/story_group_invites/index.html.haml +++ b/app/views/story_group_invites/index.html.haml @@ -1,33 +1,31 @@ -- content_for :title, 'Zaproszenia' +- content_for :title, t('.title') %section.gh-panel.gh-page-header .gh-page-header-text - %h1.gh-h1 Zaproszenia - %p.gh-lead - Kod ma 6 znaków bez mylących liter (bez O, 0, I, 1 i L). - Studenci wpisują go w „Dołącz do grupy” albo skanują kod QR. + %h1.gh-h1= t('.title') + %p.gh-lead= t('.lead') = link_to new_story_group_invite_path(@story_group), class: 'gh-btn', data: { turbo_frame: 'modal' } do %i.fa-solid.fa-plus - Nowe zaproszenie + = t('.new') %section.gh-panel.gh-invite-list .gh-invite-row.gh-invite-row--header{ 'aria-hidden': 'true' } - %span Kod - %span Status - %span Użycia - %span Ważność + %span= t('.code') + %span= t('.status') + %span= t('.uses') + %span= t('.validity') %span - if @active.any? = render partial: 'invite_row', collection: @active, as: :invite, locals: { fresh_id: @fresh_invite_id } - else - %p.gh-explainer-note Brak aktywnych zaproszeń. Utwórz nowe, żeby studenci mogli dołączyć. + %p.gh-explainer-note= t('.empty') - if @inactive.any? %details.gh-inactive-invites - %summary= "Nieaktywne (#{@inactive.size}): wygasłe i wyczerpane" + %summary= t('.inactive', count: @inactive.size) %section.gh-panel.gh-invite-list = render partial: 'invite_row', collection: @inactive, as: :invite diff --git a/app/views/story_group_invites/new.html.haml b/app/views/story_group_invites/new.html.haml index 21a4baa9..e82515e4 100644 --- a/app/views/story_group_invites/new.html.haml +++ b/app/views/story_group_invites/new.html.haml @@ -1,4 +1,4 @@ -- content_for :title, 'Nowe zaproszenie' +- content_for :title, t('.page_title') - if @in_modal = turbo_frame_tag 'modal' do diff --git a/app/views/story_group_invites/quick.html.haml b/app/views/story_group_invites/quick.html.haml index 4c9a25fa..6327f720 100644 --- a/app/views/story_group_invites/quick.html.haml +++ b/app/views/story_group_invites/quick.html.haml @@ -1,4 +1,4 @@ -- content_for :title, 'Kod dla studentów' +- content_for :title, t('.page_title') - if @in_modal = turbo_frame_tag 'modal' do diff --git a/app/views/story_group_invites/show.html.haml b/app/views/story_group_invites/show.html.haml index 7091a453..5444c2c3 100644 --- a/app/views/story_group_invites/show.html.haml +++ b/app/views/story_group_invites/show.html.haml @@ -1,4 +1,4 @@ -- content_for :title, "Zaproszenie #{@invite.code}" +- content_for :title, t('.page_title', code: @invite.code) - if @in_modal = turbo_frame_tag 'modal' do diff --git a/app/views/story_group_memberships/_confirm_leave.html.haml b/app/views/story_group_memberships/_confirm_leave.html.haml index 137d7d70..9f123433 100644 --- a/app/views/story_group_memberships/_confirm_leave.html.haml +++ b/app/views/story_group_memberships/_confirm_leave.html.haml @@ -1,30 +1,22 @@ - story_group = membership.story_group -- badges = membership.students_badges.count -- items = membership.students_items.count -- balance = membership.current_currency.to_i -%h2#gh-leave-title.gh-h2= "Opuścić grupę #{story_group.name}?" -%p - = succeed ':' do - Stracisz dostęp do sklepu, rankingu i swojej historii w tej grupie - = "#{balance} #{story_group.currency_name}," - = "#{badges} #{gh_plural(badges, 'odznaka', 'odznaki', 'odznak')}" - = "i #{items} #{gh_plural(items, 'przedmiot', 'przedmioty', 'przedmiotów')}." +%h2#gh-leave-title.gh-h2= t('.title', name: story_group.name) +- badges = t('.badges', count: membership.students_badges.count) +- items = t('.items', count: membership.students_items.count) +%p= t('.lose', balance: membership.current_currency.to_i, currency: story_group.currency_name, badges:, items:) %p.gh-warning-note{ role: 'alert' } %i.fa-solid.fa-triangle-exclamation{ 'aria-hidden': 'true' } - %span - Tego nie da się cofnąć. Jeśli wrócisz do tej grupy z nowym kodem od - prowadzącego, zaczniesz od zera. + %span= t('.warning') .gh-dialog-button-row - if @in_modal %button.gh-btn.gh-btn--secondary{ type: 'button', data: { action: 'dialog#close' }, autofocus: true } - Zostań w grupie + = t('.stay') - else - = link_to 'Zostań w grupie', edit_story_group_membership_path(story_group), + = link_to t('.stay'), edit_story_group_membership_path(story_group), class: 'gh-btn gh-btn--secondary' - = button_to 'Opuść grupę', story_group_membership_path(story_group), + = button_to t('.leave'), story_group_membership_path(story_group), method: :delete, class: 'gh-btn', form: { data: { turbo_frame: (@in_modal ? 'modal' : nil) } } diff --git a/app/views/story_group_memberships/_nickname.html.haml b/app/views/story_group_memberships/_nickname.html.haml index 5b9cbb0e..a5926197 100644 --- a/app/views/story_group_memberships/_nickname.html.haml +++ b/app/views/story_group_memberships/_nickname.html.haml @@ -1,7 +1,5 @@ -%h2#gh-nick-title.gh-h2= "Pseudonim w grupie #{membership.story_group.name}" -%p - Widoczny w rankingu tylko w tej grupie. W innych grupach możesz mieć inny. - Prowadzący widzi też Twoje imię i nazwisko. +%h2#gh-nick-title.gh-h2= t('.title', name: membership.story_group.name) +%p= t('.hint') = form_with model: membership, url: story_group_membership_path(membership.story_group), method: :patch, data: { turbo_frame: (@in_modal ? 'modal' : nil) } do |f| @@ -10,9 +8,9 @@ .gh-dialog-button-row - if @in_modal - %button.gh-btn.gh-btn--secondary{ type: 'button', data: { action: 'dialog#close' } } Anuluj + %button.gh-btn.gh-btn--secondary{ type: 'button', data: { action: 'dialog#close' } }= t('shared.actions.cancel') - else - = link_to 'Anuluj', story_group_ranking_path(membership.story_group), + = link_to t('shared.actions.cancel'), story_group_ranking_path(membership.story_group), class: 'gh-btn gh-btn--secondary' - %button.gh-btn{ type: 'submit' } Zapisz pseudonim + %button.gh-btn{ type: 'submit' }= t('.save') diff --git a/app/views/story_group_memberships/_nickname_field.html.haml b/app/views/story_group_memberships/_nickname_field.html.haml index 24c916e0..4e5dac89 100644 --- a/app/views/story_group_memberships/_nickname_field.html.haml +++ b/app/views/story_group_memberships/_nickname_field.html.haml @@ -2,7 +2,7 @@ - autofocus = local_assigns.fetch(:autofocus, false) .gh-form-field - = f.label :nickname, 'Twój pseudonim' + = f.label :nickname, t('.label') .gh-nickname-field-row .gh-text-input{ class: ('gh-text-input--invalid' if error) } = f.text_field :nickname, maxlength: 24, autocomplete: 'off', autofocus: autofocus, @@ -14,4 +14,4 @@ %p#gh-nickname-error.gh-field-error{ role: 'alert' } %i.fa-solid.fa-triangle-exclamation{ 'aria-hidden': 'true' } %span= error - %span.gh-field-hint Do 24 znaków. Zostaw puste, żeby pokazywać imię i nazwisko. + %span.gh-field-hint= t('.hint') diff --git a/app/views/story_group_memberships/confirm_leave.html.haml b/app/views/story_group_memberships/confirm_leave.html.haml index 58ddde0d..e2b91adc 100644 --- a/app/views/story_group_memberships/confirm_leave.html.haml +++ b/app/views/story_group_memberships/confirm_leave.html.haml @@ -1,4 +1,4 @@ -- content_for :title, "Opuść grupę #{@story_group.name}" +- content_for :title, t('.page_title', name: @story_group.name) - if @in_modal = turbo_frame_tag 'modal' do diff --git a/app/views/story_group_memberships/edit.html.haml b/app/views/story_group_memberships/edit.html.haml index 70006dfc..e192b8f6 100644 --- a/app/views/story_group_memberships/edit.html.haml +++ b/app/views/story_group_memberships/edit.html.haml @@ -1,42 +1,40 @@ -- content_for :title, "Ustawienia w grupie - #{@story_group.name}" +- content_for :title, t('.page_title', name: @story_group.name) %section.gh-panel.gh-page-header %p.gh-breadcrumbs = link_to @story_group.name, story_group_path(@story_group) %i.fa-solid.fa-chevron-right{ 'aria-hidden': 'true' } - %span Ustawienia w grupie - %h1.gh-h1 Ustawienia w grupie - %p.gh-lead= "Te ustawienia dotyczą tylko grupy #{@story_group.name}." + %span= t('.title') + %h1.gh-h1= t('.title') + %p.gh-lead= t('.lead', name: @story_group.name) %section.gh-panel.gh-settings-section - %h2.gh-h3 Pseudonim - %p.gh-field-hint Widoczny w rankingu tylko w tej grupie. W innych grupach możesz mieć inny. + %h2.gh-h3= t('.nickname') + %p.gh-field-hint= t('.nickname_hint') = form_with model: @membership, url: story_group_membership_path(@story_group), method: :patch do |f| - = render 'nickname_field', f: f, membership: @membership, submit_label: 'Zapisz' + = render 'nickname_field', f: f, membership: @membership, submit_label: t('shared.actions.save') %section.gh-panel.gh-settings-section - %h2.gh-h3 Co widzi prowadzący - %p.gh-field-hint Pozostali studenci widzą tylko Twój pseudonim, i to tylko w rankingu. + %h2.gh-h3= t('.teacher_sees') + %p.gh-field-hint= t('.teacher_sees_hint') %ul.gh-visible-fields-list %li - %span Imię i nazwisko + %span= t('.full_name') %b= @membership.full_name %li - %span E-mail + %span= t('.email') %b= @membership.email %li - %span Numer indeksu - %b= @membership.university_number.presence || 'Brak' + %span= t('.university_number') + %b= @membership.university_number.presence || t('.none') %section.gh-panel.gh-danger-zone - %h2.gh-h3 Opuść grupę - %p.gh-small - Stracisz dostęp do grupy, a Twoja waluta, odznaki i przedmioty w niej - przepadną. Żeby wrócić, potrzebujesz nowego kodu od prowadzącego. + %h2.gh-h3= t('.leave_title') + %p.gh-small= t('.leave_body') .gh-button-row = link_to confirm_leave_story_group_membership_path(@story_group), class: 'gh-btn gh-btn--secondary', data: { turbo_frame: 'modal' } do %i.fa-solid.fa-right-from-bracket{ 'aria-hidden': 'true' } - Opuść grupę + = t('.leave') diff --git a/app/views/story_group_memberships/nickname.html.haml b/app/views/story_group_memberships/nickname.html.haml index 543e6c6e..405ef30e 100644 --- a/app/views/story_group_memberships/nickname.html.haml +++ b/app/views/story_group_memberships/nickname.html.haml @@ -1,4 +1,4 @@ -- content_for :title, "Pseudonim w grupie #{@story_group.name}" +- content_for :title, t('.page_title', name: @story_group.name) - if @in_modal = turbo_frame_tag 'modal' do diff --git a/app/views/story_groups/_confirm_destroy.html.haml b/app/views/story_groups/_confirm_destroy.html.haml index 36dc6553..f3a81162 100644 --- a/app/views/story_groups/_confirm_destroy.html.haml +++ b/app/views/story_groups/_confirm_destroy.html.haml @@ -1,12 +1,12 @@ - students = story_group.students.count - teachers = story_group.teachers.count + 1 -%h2#gh-del-group-title.gh-h2 Usunąć grupę na zawsze? +%h2#gh-del-group-title.gh-h2= t('.title') %p - = "#{students} #{gh_plural(students, 'student', 'studenci', 'studentów')}" - = "i #{teachers} #{gh_plural(teachers, 'nauczyciel', 'nauczycieli', 'nauczycieli')}" - = "#{gh_plural(students + teachers, 'straci', 'stracą', 'straci')} dostęp." - Historia, przedmioty i ranking znikną. + = t('.lose_access', count: students + teachers, + students: t('.students', count: students), + teachers: t('.teachers', count: teachers)) + = t('.gone') = form_with url: story_group_path(story_group), method: :delete, data: { controller: 'confirm-name', @@ -15,7 +15,7 @@ .gh-form-field = label_tag :confirm do - Wpisz nazwę grupy, żeby potwierdzić: + = t('.confirm') %b= story_group.name .gh-text-input{ class: ('gh-text-input--invalid' if @confirm_failed) } = text_field_tag :confirm, nil, autocomplete: 'off', autofocus: true, @@ -25,14 +25,14 @@ - if @confirm_failed %p#gh-del-group-error.gh-field-error{ role: 'alert' } %i.fa-solid.fa-triangle-exclamation{ 'aria-hidden': 'true' } - %span Wpisana nazwa nie zgadza się z nazwą grupy. Grupa nie została usunięta. + %span= t('.mismatch') .gh-dialog-button-row - if @in_modal - %button.gh-btn.gh-btn--secondary{ type: 'button', data: { action: 'dialog#close' } } Anuluj + %button.gh-btn.gh-btn--secondary{ type: 'button', data: { action: 'dialog#close' } }= t('shared.actions.cancel') - else - = link_to 'Anuluj', edit_story_group_path(story_group), class: 'gh-btn gh-btn--secondary' + = link_to t('shared.actions.cancel'), edit_story_group_path(story_group), class: 'gh-btn gh-btn--secondary' %button.gh-btn{ type: 'submit', disabled: true, data: { confirm_name_target: 'submit' } } - Usuń grupę + = t('.remove') diff --git a/app/views/story_groups/_currency_mini.html.haml b/app/views/story_groups/_currency_mini.html.haml index 6aa40ed4..e2ad6621 100644 --- a/app/views/story_groups/_currency_mini.html.haml +++ b/app/views/story_groups/_currency_mini.html.haml @@ -5,7 +5,7 @@ %span{ data: { group_wizard_target: 'markLetter' } } %span %b 19 - %small do wydania + %small= t('.to_spend') %span.gh-currency-preview-purse %span.gh-currency-token.gh-octagon-clip{ style: '--gh-s: 50px', 'aria-hidden': 'true' } diff --git a/app/views/story_groups/_group_card.html.haml b/app/views/story_groups/_group_card.html.haml index d34a5a22..1281e93c 100644 --- a/app/views/story_groups/_group_card.html.haml +++ b/app/views/story_groups/_group_card.html.haml @@ -13,5 +13,5 @@ %span{ class: "gh-tag gh-role-tag gh-tag--#{group.role}" }= group.role_label %p.gh-group-index-meta - %span= "#{group.students_count} #{gh_plural(group.students_count, 'student', 'studentów', 'studentów')}" + %span= t('.students', count: group.students_count) %span= group.meta diff --git a/app/views/story_groups/_preset_review.html.haml b/app/views/story_groups/_preset_review.html.haml index f722213c..8a73850f 100644 --- a/app/views/story_groups/_preset_review.html.haml +++ b/app/views/story_groups/_preset_review.html.haml @@ -1,12 +1,12 @@ - preset = @preset -- currency = @currency_name.presence || 'waluty' +-# Not t('.x'): the render layout: blocks below run inside preset_row and would read its keys. +- tr = ->(key, **vars) { t("story_groups.preset_review.#{key}", **vars) } +- currency = @currency_name.presence || tr.call(:currency_fallback) = turbo_frame_tag 'starter_pack' do .gh-form-section.gh-form-section--first - %h2.gh-h3= "Zestaw „#{preset.label}” dla #{preset.classes} zajęć" - %p.gh-field-hint - Usuń, czego nie potrzebujesz, i popraw liczby. Nazwy i opisy zmienisz po - utworzeniu grupy. + %h2.gh-h3= tr.call(:title, pack: preset.label, classes: preset.classes) + %p.gh-field-hint= tr.call(:hint) = hidden_field_tag 'setup[pack]', preset.pack, id: nil = hidden_field_tag 'setup[classes]', preset.classes, id: nil @@ -14,34 +14,34 @@ .gh-preset-review-grid %section.gh-preset-review-section %h3.gh-h3 - Rangi + = tr.call(:ranks) %span.gh-showcase-zone-count.gh-octagon-clip= preset.ranks.size - preset.ranks.each do |rank| = render layout: 'preset_row', locals: { zone: 'ranks', index: rank.index, icon: 'fa-ranking-star', name: rank.name, - note: (rank.discount.positive? ? "−#{rank.discount}% w sklepie" : 'Ranga startowa') } do + note: (rank.discount.positive? ? tr.call(:shop_discount, discount: rank.discount) : tr.call(:starting_rank)) } do - if rank.index.zero? - %span.gh-preset-row-value od 0 + %span.gh-preset-row-value= tr.call(:from_zero) = hidden_field_tag "setup[ranks][#{rank.index}][value]", rank.threshold, id: nil - else %span.gh-preset-row-value - od + = tr.call(:from) .gh-text-input.gh-input--compact = number_field_tag "setup[ranks][#{rank.index}][value]", rank.threshold, - min: 0, step: 1, id: nil, 'aria-label': "#{rank.name}: próg" + min: 0, step: 1, id: nil, 'aria-label': tr.call(:threshold_label, name: rank.name) %section.gh-preset-review-section %h3.gh-h3 - Odznaki + = tr.call(:badges) %span.gh-showcase-zone-count.gh-octagon-clip= preset.badges.size - preset.badges.each do |badge| = render layout: 'preset_row', locals: { zone: 'badges', index: badge.index, icon: 'fa-award', name: badge.name, note: badge.didactic_description } do - %span.gh-preset-row-value= "−#{badge.discount}% w sklepie" + %span.gh-preset-row-value= tr.call(:shop_discount, discount: badge.discount) %section.gh-preset-review-section %h3.gh-h3 - Przedmioty w sklepie + = tr.call(:items) %span.gh-showcase-zone-count.gh-octagon-clip= preset.items.size - preset.items.each do |item| = render layout: 'preset_row', locals: { zone: 'items', index: item.index, icon: 'fa-bag-shopping', @@ -49,15 +49,15 @@ %span.gh-preset-row-value .gh-text-input.gh-input--compact = number_field_tag "setup[items][#{item.index}][value]", item.price, - min: 1, step: 1, id: nil, 'aria-label': "#{item.name}: cena w #{currency}" + min: 1, step: 1, id: nil, 'aria-label': tr.call(:price_label, name: item.name, currency: currency) %span.gh-currency-token.gh-octagon-clip{ style: '--gh-s: 20px', 'aria-hidden': 'true' } %span.gh-octagon-clip{ data: { group_wizard_target: 'markBox' } } %span{ data: { group_wizard_target: 'markLetter' } } %section.gh-preset-review-section %h3.gh-h3 - Kategorie zajęć - %small.gh-page-kicker= "(szablon „#{StarterPack::TEMPLATE_NAME}”)" + = tr.call(:categories) + %small.gh-page-kicker= tr.call(:template, name: StarterPack.template_name) %span.gh-showcase-zone-count.gh-octagon-clip= preset.categories.size - preset.categories.each do |category| = render layout: 'preset_row', locals: { zone: 'cats', index: category.index, icon: 'fa-table-cells', @@ -66,7 +66,7 @@ .gh-text-input.gh-input--compact = number_field_tag "setup[cats][#{category.index}][value]", category.reward, min: 1, step: 1, id: nil, - 'aria-label': "#{category.didactic_description}: nagroda w #{currency}" + 'aria-label': tr.call(:reward_label, name: category.didactic_description, currency: currency) %span.gh-currency-token.gh-octagon-clip{ style: '--gh-s: 20px', 'aria-hidden': 'true' } %span.gh-octagon-clip{ data: { group_wizard_target: 'markBox' } } %span{ data: { group_wizard_target: 'markLetter' } } diff --git a/app/views/story_groups/_preset_row.html.haml b/app/views/story_groups/_preset_row.html.haml index b432ee99..f94f5c7c 100644 --- a/app/views/story_groups/_preset_row.html.haml +++ b/app/views/story_groups/_preset_row.html.haml @@ -1,6 +1,8 @@ - row_id = "gh-preset-#{zone}-#{index}" -.gh-preset-row{ data: { controller: 'preset-row' } } +.gh-preset-row{ data: { controller: 'preset-row', preset_row_name_value: name, + preset_row_remove_value: t('.remove', name: '%{name}'), + preset_row_restore_value: t('.restore', name: '%{name}') } } %i.fa-solid{ class: icon, 'aria-hidden': 'true' } %span.gh-preset-row-name @@ -15,5 +17,5 @@ %label.gh-preset-row-remove{ for: row_id } = check_box_tag "setup[#{zone}][#{index}][keep]", '1', true, id: row_id, class: 'sr-only', data: { preset_row_target: 'keep', action: 'preset-row#toggle' } - %span.sr-only{ data: { preset_row_target: 'label' } }= "Usuń: #{name}" + %span.sr-only{ data: { preset_row_target: 'label' } }= t('.remove', name: name) %i.fa-solid.fa-xmark{ 'aria-hidden': 'true', data: { preset_row_target: 'icon' } } diff --git a/app/views/story_groups/_settings.html.haml b/app/views/story_groups/_settings.html.haml index 87f200e5..691e035c 100644 --- a/app/views/story_groups/_settings.html.haml +++ b/app/views/story_groups/_settings.html.haml @@ -1,21 +1,19 @@ - error = ->(attribute) { story_group.errors[attribute].first } %section.gh-panel.gh-page-header - %h1.gh-h1 Ustawienia grupy - %p.gh-lead - Zmienią je wszyscy prowadzący tę grupę. Tylko właściciel może usunąć grupę. - Ranking ustawiasz w zakładce Ranking. + %h1.gh-h1= t('.title') + %p.gh-lead= t('.lead') = form_with model: story_group do |f| %section.gh-panel.gh-settings-section .gh-form-section.gh-form-section--first - %h2.gh-h3 Grupa i opowieść + %h2.gh-h3= t('.group_section') .gh-form-field - = f.label :name, 'Nazwa grupy' + = f.label :name, t('.name') .gh-text-input{ class: ('gh-text-input--invalid' if error.call(:name)) } = f.text_field :name, maxlength: 40, autocomplete: 'off', - placeholder: 'np. Kosmiczne króliki', + placeholder: t('.name_placeholder'), 'aria-invalid': (error.call(:name) ? 'true' : nil), 'aria-describedby': (error.call(:name) ? 'gh-group-name-error' : nil) - if error.call(:name) @@ -24,7 +22,7 @@ %span= error.call(:name) .gh-form-field - = f.label :description, 'Opowieść' + = f.label :description, t('.story') .gh-text-input{ class: ('gh-text-input--invalid' if error.call(:description)) } = f.text_area :description, rows: 3, maxlength: 1024, 'aria-invalid': (error.call(:description) ? 'true' : nil), @@ -33,53 +31,49 @@ %p#gh-group-story-error.gh-field-error{ role: 'alert' } %i.fa-solid.fa-triangle-exclamation{ 'aria-hidden': 'true' } %span= error.call(:description) - %span.gh-field-hint Widzą ją wszyscy w grupie, na stronie głównej grupy. + %span.gh-field-hint= t('.story_hint') = render 'shared/image_field', form: f, record: story_group, presets: GroupArt::KEYS, registry: GroupArt, kind: 'group', tile: :art, - copy: { label: 'Grafika grupy', - hint: 'JPG lub PNG, najlepiej co najmniej 1200 px szerokości.' } + copy: { label: t('.art_label'), + hint: t('.art_hint') } .gh-form-section - %h2.gh-h3 Waluta - %p.gh-field-hint Nowa nazwa pojawi się wszędzie, także w historii studentów. + %h2.gh-h3= t('.currency_section') + %p.gh-field-hint= t('.currency_hint') .gh-form-field - = f.label :currency_name, 'Nazwa waluty' + = f.label :currency_name, t('.currency_name') .gh-text-input{ class: ('gh-text-input--invalid' if error.call(:currency_name)) } = f.text_field :currency_name, maxlength: 40, autocomplete: 'off', - placeholder: 'np. Marchewek', + placeholder: t('.currency_placeholder'), 'aria-invalid': (error.call(:currency_name) ? 'true' : nil), 'aria-describedby': (error.call(:currency_name) ? 'gh-group-currency-error' : nil) - if error.call(:currency_name) %p#gh-group-currency-error.gh-field-error{ role: 'alert' } %i.fa-solid.fa-triangle-exclamation{ 'aria-hidden': 'true' } %span= error.call(:currency_name) - %span.gh-field-hint Podaj ją w formie, która pasuje do liczby — np. „Marchewek”. + %span.gh-field-hint= t('.currency_name_hint') = render 'shared/image_field', form: f, record: story_group, attribute: :currency_icon, glyph_attribute: :currency_icon_glyph, presets: CurrencyIcons::KEYS, registry: CurrencyIcons, kind: 'currency', tile: :currency, aspect: 1, - copy: { label: 'Ikona waluty', presets: 'Gotowe ikony', - own: 'Twoja ikona', upload: 'Albo wgraj własną ikonę', - hint: 'Najlepiej PNG z przezroczystym tłem, co najmniej 128 px.', - crop: 'Przytnij ikonę', - cropped: 'Przesuń i przeskaluj zaznaczenie. Zapisujemy kwadratowy kadr, bo ikona siada na okrągłym liczmanie.' } + copy: t('story_groups.currency_icon') .gh-form-section - %h2.gh-h3 Życia + %h2.gh-h3= t('.lives_section') .gh-form-field{ data: { controller: 'lives-stepper', lives_stepper_max_value: 10 } } - %p#gh-default-lives-label.gh-form-label Życia na start + %p#gh-default-lives-label.gh-form-label= t('.lives') .gh-number-stepper{ role: 'group', 'aria-labelledby': 'gh-default-lives-label' } - %button.gh-number-stepper-button.gh-octagon-clip{ type: 'button', 'aria-label': 'Mniej', + %button.gh-number-stepper-button.gh-octagon-clip{ type: 'button', 'aria-label': t('shared.actions.fewer'), disabled: story_group.default_lives.to_i.zero?, data: { lives_stepper_target: 'down', action: 'lives-stepper#down' } } %i.fa-solid.fa-minus{ 'aria-hidden': 'true' } %output{ 'aria-live': 'polite', data: { lives_stepper_target: 'output' } }= story_group.default_lives - %button.gh-number-stepper-button.gh-octagon-clip{ type: 'button', 'aria-label': 'Więcej', + %button.gh-number-stepper-button.gh-octagon-clip{ type: 'button', 'aria-label': t('shared.actions.more'), disabled: story_group.default_lives.to_i >= 10, data: { lives_stepper_target: 'up', action: 'lives-stepper#up' } } @@ -92,23 +86,19 @@ %i.fa-solid.fa-triangle-exclamation{ 'aria-hidden': 'true' } %span= error.call(:default_lives) - %span.gh-field-hint - Dotyczy tylko studentów, którzy dołączą od teraz. Obecnym zmieniasz - życia na liście studentów. + %span.gh-field-hint= t('.lives_hint') .gh-panel.gh-form-actions-bar - = link_to 'Wróć do grupy', story_group_path(story_group), class: 'gh-link-button' + = link_to t('.back'), story_group_path(story_group), class: 'gh-link-button' %span.gh-form-actions-spacer - %button.gh-btn{ type: 'submit' } Zapisz zmiany + %button.gh-btn{ type: 'submit' }= t('shared.actions.save_changes') - if policy(story_group).destroy? %section.gh-panel.gh-danger-zone - %h2.gh-h3 Usuń grupę - %p.gh-small - Usunięcie zabiera wszystkim dostęp do grupy, jej sklepu, rankingu - i historii. Tego nie da się cofnąć. + %h2.gh-h3= t('.delete_title') + %p.gh-small= t('.delete_body') .gh-button-row = link_to confirm_destroy_story_group_path(story_group), class: 'gh-btn gh-btn--secondary', data: { turbo_frame: 'modal' } do %i.fa-solid.fa-trash{ 'aria-hidden': 'true' } - Usuń grupę + = t('.delete') diff --git a/app/views/story_groups/_student_show.html.haml b/app/views/story_groups/_student_show.html.haml index 32b5cec7..4cf56864 100644 --- a/app/views/story_groups/_student_show.html.haml +++ b/app/views/story_groups/_student_show.html.haml @@ -11,12 +11,12 @@ %h1.gh-h1= story_group.name - if story_group.description.present? %p.gh-group-description-text= story_group.description - %p.gh-small.gh-group-description-card-action Prowadzi #{story_group.owner.full_name} + %p.gh-small.gh-group-description-card-action= t('.led_by', name: story_group.owner.full_name) - %section.gh-card.gh-card--rank.gh-currency-balance-card{ 'aria-label': "Twoje #{story_group.currency_name}" } + %section.gh-card.gh-card--rank.gh-currency-balance-card{ 'aria-label': t('.balance_label', currency: story_group.currency_name) } .gh-card-frame .gh-card-inset - %p.gh-page-kicker Do wydania + %p.gh-page-kicker= t('.to_spend') .gh-big-number-row = render 'layouts/currency_token', story_group: story_group, size: 44 %span.gh-tabular-nums= overview.balance @@ -24,10 +24,10 @@ %dl.gh-currency-balance-stats %div - %dt Zebrane łącznie + %dt= t('.collected') %dd= overview.total %div - %dt Życia + %dt= t('.lives') %dd %span.gh-lives-badge{ class: ('gh-lives-badge--zero' if overview.lives.zero?) } %i.fa-solid.fa-heart{ 'aria-hidden': 'true' } @@ -35,14 +35,14 @@ = link_to story_group_shop_index_path(story_group), class: 'gh-btn' do %i.fa-solid.fa-bag-shopping{ 'aria-hidden': 'true' } - Otwórz sklep + = t('.open_shop') %section.gh-card.gh-card--rank.gh-rank-card .gh-card-frame .gh-card-inset .gh-rank-card-top - %p.gh-page-kicker Twoja ranga - %h2.gh-rank-card-name= overview.rank&.name || 'Brak rangi' + %p.gh-page-kicker= t('.your_rank') + %h2.gh-rank-card-name= overview.rank&.name || t('.no_rank') - if overview.rank .gh-card-art @@ -52,21 +52,19 @@ - progress, span = overview.rank_progress .gh-rank-progress .gh-rank-progress-text - %span #{overview.total} z #{overview.next_rank.required_currency_value} + %span= t('.progress', total: overview.total, needed: overview.next_rank.required_currency_value) %span= overview.next_rank.name .gh-progress-bar.gh-octagon-clip{ role: 'progressbar', 'aria-valuemin': 0, 'aria-valuemax': span, 'aria-valuenow': progress, - 'aria-label': 'Postęp do następnej rangi', } + 'aria-label': t('.progress_label'), } %i.gh-octagon-clip{ style: "--gh-p: #{gh_percent(progress, span)}%" } %p.gh-small= overview_next_rank_line(overview.missing_to_next_rank, overview.next_rank) - elsif overview.rank - %p.gh-small #{overview.total} zebranych. To najwyższa ranga w tej grupie. + %p.gh-small= t('.top_rank', total: overview.total) - else - %p.gh-small - Prowadzący nie dodał jeszcze rang. Waluta, którą zbierasz, liczy się - mimo to — rangi przyznają się same, gdy się pojawią. + %p.gh-small= t('.no_ranks') - if overview.ranking_place %p.gh-small.gh-rank-card-footer @@ -76,8 +74,8 @@ - if shelf.any? %section.gh-showcase-zone.gh-showcase-zone--badges %h2.gh-showcase-zone-label - %span Odznaki - %span.gh-showcase-zone-count.gh-octagon-clip #{shelf.earned_count} z #{shelf.size} + %span= t('.badges') + %span.gh-showcase-zone-count.gh-octagon-clip= t('.badges_count', earned: shelf.earned_count, total: shelf.size) .gh-card-grid.gh-card-grid--compact - shelf.slots.each do |slot| = render 'badges/flip_card', badge: slot.badge, @@ -87,7 +85,7 @@ %section.gh-showcase-zone.gh-showcase-zone--hand %h2.gh-showcase-zone-label - %span Twoje przedmioty + %span= t('.items') %span.gh-showcase-zone-count.gh-octagon-clip= overview.items_count .gh-item-hand - count = overview.items.size @@ -97,10 +95,10 @@ .gh-panel.gh-empty-slot-panel %p= overview_hand_line(overview.affordable) - = link_to 'Przejdź do sklepu', story_group_shop_index_path(story_group), class: 'gh-btn gh-btn--secondary' + = link_to t('.go_to_shop'), story_group_shop_index_path(story_group), class: 'gh-btn gh-btn--secondary' %section.gh-panel.gh-overview-ledger-panel{ 'aria-labelledby': 'gh-ledger-title' } - %h2#gh-ledger-title.gh-h2 Ostatnie zmiany + %h2#gh-ledger-title.gh-h2= t('.recent') - if overview.entries.any? %ul.gh-ledger-list - overview.entries.each do |entry| @@ -108,13 +106,11 @@ %span.gh-activity-feed-row-marker{ class: "gh-activity-feed-row-marker--#{entry.tone}", 'aria-hidden': 'true' } %span.gh-activity-feed-row-text = entry.title - %small #{entry.label}, #{gh_when(entry.occurred_at)} + %small= t('.entry_meta', label: entry.label, when: gh_when(entry.occurred_at)) %span.gh-activity-feed-row-amount{ class: "gh-activity-feed-row-amount--#{entry.tone}" }= entry.signed_amount - = link_to 'Pokaż całą historię', + = link_to t('.full_history'), story_group_student_currency_transactions_path(story_group, overview.student), class: 'gh-link-button' - else - %p.gh-explainer-note - Pierwsze wpisy pojawią się po zajęciach. Prowadzący przyznaje walutę za - obecność, punktualność i aktywność. + %p.gh-explainer-note= t('.no_entries') diff --git a/app/views/story_groups/_teacher_show.html.haml b/app/views/story_groups/_teacher_show.html.haml index 96e34068..734023ac 100644 --- a/app/views/story_groups/_teacher_show.html.haml +++ b/app/views/story_groups/_teacher_show.html.haml @@ -12,16 +12,16 @@ .gh-button-row = link_to story_group_invites_path(story_group), class: 'gh-btn' do %i.fa-solid.fa-qrcode{ 'aria-hidden': 'true' } - Pokaż kod dla studentów + = t('.show_code') = link_to story_group_activity_groups_path(story_group), class: 'gh-btn gh-btn--secondary' do %i.fa-solid.fa-table{ 'aria-hidden': 'true' } - Utwórz arkusz + = t('.create_sheet') - if policy(story_group).update? = link_to edit_story_group_path(story_group), class: 'gh-btn gh-btn--secondary' do %i.fa-solid.fa-gear{ 'aria-hidden': 'true' } - Ustawienia grupy + = t('.settings') -%section.gh-panel.gh-stat-row{ 'aria-label': 'Podsumowanie grupy' } +%section.gh-panel.gh-stat-row{ 'aria-label': t('.summary') } %dl - overview.stat_tiles.each do |tile| .gh-stat-tile @@ -33,7 +33,7 @@ .gh-overview-grid %section.gh-panel.gh-overview-side-panel{ 'aria-labelledby': 'gh-purchases-title' } - %h2#gh-purchases-title.gh-h2 Ostatnie zakupy + %h2#gh-purchases-title.gh-h2= t('.purchases') - if overview.purchases.any? %ul.gh-purchase-list @@ -41,23 +41,21 @@ %li.gh-purchase-row.gh-purchase-row--group-variant = render 'layouts/item_thumb', item: purchase.item %span.gh-purchase-row-main - %b= purchase.item&.name || 'Przedmiot usunięty z oferty' + %b= purchase.item&.name || t('.removed_item') %span= purchase.student_name %span.gh-price-badge %b= purchase.price = render 'layouts/currency_token', story_group: story_group, size: 16 %span.gh-purchase-row-time= gh_when(purchase.time) - = link_to 'Wszystkie zakupy w grupie', home_path(group: story_group.id), class: 'gh-link-button' + = link_to t('.all_purchases'), home_path(group: story_group.id), class: 'gh-link-button' - else - %p.gh-explainer-note - Nikt jeszcze niczego nie kupił. Zakupy pojawią się tutaj same, gdy - studenci zaczną wydawać walutę w sklepie. + %p.gh-explainer-note= t('.no_purchases') .gh-overview-column - if overview.attention.any? %section.gh-panel.gh-overview-side-panel{ 'aria-labelledby': 'gh-attention-title' } - %h2#gh-attention-title.gh-h2 Wymaga uwagi + %h2#gh-attention-title.gh-h2= t('.attention') %ul.gh-attention-list - overview.attention.each do |row| %li @@ -71,12 +69,12 @@ - if overview.recent_sheets.any? %section.gh-panel.gh-overview-side-panel{ 'aria-labelledby': 'gh-sheets-title' } - %h2#gh-sheets-title.gh-h2 Ostatnie arkusze + %h2#gh-sheets-title.gh-h2= t('.recent_sheets') - overview.recent_sheets.each do |sheet| .gh-sheet-summary-row %div %b= sheet.activity_group.name - = link_to 'Otwórz', story_group_activity_groups_path(story_group), class: 'gh-link-button' + = link_to t('.open'), story_group_activity_groups_path(story_group), class: 'gh-link-button' - if sheet.podium.any? %ol.gh-leaderboard-top3 - sheet.podium.each_with_index do |place, index| @@ -86,11 +84,9 @@ %span.gh-leaderboard-score +#{place.points} %p.gh-small= ranking_visibility_note(story_group) - else - %p.gh-small Nikt nie ma jeszcze nagród z tego arkusza. + %p.gh-small= t('.no_rewards') - else %section.gh-panel.gh-overview-side-panel - %h2.gh-h2 Nie masz jeszcze arkuszy ocen - %p.gh-explainer-note - Arkusz to jedne zajęcia: kolumny z nagrodami i lista studentów. - Utwórz pierwszy, żeby zacząć przyznawać walutę. - = link_to 'Utwórz arkusz', story_group_activity_groups_path(story_group), class: 'gh-btn gh-btn--secondary' + %h2.gh-h2= t('.no_sheets_title') + %p.gh-explainer-note= t('.no_sheets_body') + = link_to t('.create_sheet'), story_group_activity_groups_path(story_group), class: 'gh-btn gh-btn--secondary' diff --git a/app/views/story_groups/_wizard.html.haml b/app/views/story_groups/_wizard.html.haml index 40650f97..e63be5d8 100644 --- a/app/views/story_groups/_wizard.html.haml +++ b/app/views/story_groups/_wizard.html.haml @@ -1,22 +1,22 @@ - error = ->(attribute) { story_group.errors[attribute].first } - packs = StarterPack -- steps = ['Grupa i opowieść', 'Waluta i zasady', 'Jak zaczynasz', 'Podsumowanie'] +- steps = t('.steps') .gh-wizard{ data: { controller: 'group-wizard', action: 'gh:image-crop->group-wizard#art', - group_wizard_preset_url_value: preset_preview_story_groups_path } } + group_wizard_preset_url_value: preset_preview_story_groups_path, + group_wizard_i18n_value: t('.js').to_json } } %section.gh-panel.gh-page-header.gh-page-header--stacked %p.gh-breadcrumbs - = link_to 'Grupy', story_groups_path + = link_to t('.breadcrumb'), story_groups_path %i.fa-solid.fa-chevron-right{ 'aria-hidden': 'true' } - %span Nowa grupa - %h1.gh-h1 Nowa grupa - %p.gh-lead - Zajmie to kilka minut. Wszystko zmienisz później w ustawieniach grupy. + %span= t('.title') + %h1.gh-h1= t('.title') + %p.gh-lead= t('.lead') - %nav.gh-wizard-steps{ 'aria-label': 'Kroki' } + %nav.gh-wizard-steps{ 'aria-label': t('.steps_label') } - steps.each_with_index do |label, index| %button.gh-wizard-step-tab{ type: 'button', disabled: index.positive?, 'aria-current': (index.zero? ? 'step' : nil), @@ -42,63 +42,60 @@ %section.gh-panel.gh-form-column %section{ data: { group_wizard_target: 'panel' } } - %h2.sr-only Grupa i opowieść + %h2.sr-only= steps[0] .gh-form-section.gh-form-section--first - %h2.gh-h3 Nazwa i opowieść - %p.gh-field-hint Studenci zobaczą je na liście grup i na stronie grupy. + %h2.gh-h3= t('.name_section') + %p.gh-field-hint= t('.name_hint') .gh-form-field - = f.label :name, 'Nazwa grupy' + = f.label :name, t('.name') .gh-text-input{ class: ('gh-text-input--invalid' if error.call(:name)) } = f.text_field :name, maxlength: 40, autocomplete: 'off', - placeholder: 'np. Zakon Algorytmów', + placeholder: t('.name_placeholder'), 'aria-invalid': (error.call(:name) ? 'true' : nil), 'aria-describedby': (error.call(:name) ? 'gh-wiz-name-error' : nil), data: { group_wizard_target: 'name', action: 'group-wizard#refresh' } %p#gh-wiz-name-error.gh-field-error{ role: 'alert', hidden: error.call(:name).blank?, data: { group_wizard_target: 'nameError' } } %i.fa-solid.fa-triangle-exclamation{ 'aria-hidden': 'true' } - %span= error.call(:name) || 'Podaj nazwę grupy.' + %span= error.call(:name) || t('.name_missing') .gh-form-field = f.label :description do - Opowieść - %small (opcjonalnie) + = t('.story') + %small= t('shared.actions.optional') .gh-text-input{ class: ('gh-text-input--invalid' if error.call(:description)) } = f.text_area :description, rows: 4, maxlength: 1024, - placeholder: 'Dawno, dawno temu…' + placeholder: t('.story_placeholder') - if error.call(:description) %p.gh-field-error{ role: 'alert' } %i.fa-solid.fa-triangle-exclamation{ 'aria-hidden': 'true' } %span= error.call(:description) - %span.gh-field-hint Krótki wstęp fabularny. Zmienisz go w każdej chwili. + %span.gh-field-hint= t('.story_hint') .gh-form-section - %h2.gh-h3 Grafika grupy - %p.gh-field-hint - Pojawia się na karcie grupy, w panelu bocznym i jako tło grupy. + %h2.gh-h3= t('.art_section') + %p.gh-field-hint= t('.art_hint') = render 'shared/image_field', form: f, record: story_group, presets: GroupArt::KEYS, registry: GroupArt, kind: 'group', tile: :art, refresh_action: 'group-wizard#art', - copy: { label: 'Grafika grupy', - hint: 'JPG lub PNG, najlepiej co najmniej 1200 px szerokości.' } + copy: { label: t('story_groups.settings.art_label'), + hint: t('story_groups.settings.art_hint') } %section{ data: { group_wizard_target: 'panel' }, hidden: true } - %h2.sr-only Waluta i zasady + %h2.sr-only= steps[1] .gh-form-section.gh-form-section--first - %h2.gh-h3 Nazwa waluty - %p.gh-field-hint - Czytamy ją po każdej liczbie, więc podaj formę, która pasuje do - liczby mnogiej — np. „Marchewek”. + %h2.gh-h3= t('.currency_section') + %p.gh-field-hint= t('.currency_hint') .gh-form-field - = f.label :currency_name, 'Nazwa waluty' + = f.label :currency_name, t('.currency_name') .gh-text-input{ class: ('gh-text-input--invalid' if error.call(:currency_name)) } = f.text_field :currency_name, maxlength: 40, autocomplete: 'off', - placeholder: 'np. Złotych Monet', + placeholder: t('.currency_placeholder'), 'aria-invalid': (error.call(:currency_name) ? 'true' : nil), 'aria-describedby': (error.call(:currency_name) ? 'gh-wiz-currency-error' : nil), data: { group_wizard_target: 'currency', action: 'group-wizard#refresh' } @@ -106,32 +103,28 @@ hidden: error.call(:currency_name).blank?, data: { group_wizard_target: 'currencyError' } } %i.fa-solid.fa-triangle-exclamation{ 'aria-hidden': 'true' } - %span= error.call(:currency_name) || 'Podaj nazwę waluty.' + %span= error.call(:currency_name) || t('.currency_missing') = render 'shared/image_field', form: f, record: story_group, attribute: :currency_icon, glyph_attribute: :currency_icon_glyph, presets: CurrencyIcons::KEYS, registry: CurrencyIcons, kind: 'currency', tile: :currency, aspect: 1, refresh_action: 'group-wizard#refresh', - copy: { label: 'Ikona waluty', presets: 'Gotowe ikony', - own: 'Twoja ikona', upload: 'Albo wgraj własną ikonę', - hint: 'Najlepiej PNG z przezroczystym tłem, co najmniej 128 px.', - crop: 'Przytnij ikonę', - cropped: 'Przesuń i przeskaluj zaznaczenie. Zapisujemy kwadratowy kadr, bo ikona siada na okrągłym liczmanie.' } + copy: t('story_groups.currency_icon') .gh-form-section - %h2.gh-h3 Życia + %h2.gh-h3= t('.lives_section') .gh-form-field{ data: { controller: 'lives-stepper', lives_stepper_max_value: 10 } } - %p#gh-wiz-lives-label.gh-form-label Życia na start + %p#gh-wiz-lives-label.gh-form-label= t('.lives') .gh-number-stepper{ role: 'group', 'aria-labelledby': 'gh-wiz-lives-label' } - %button.gh-number-stepper-button.gh-octagon-clip{ type: 'button', 'aria-label': 'Mniej żyć', + %button.gh-number-stepper-button.gh-octagon-clip{ type: 'button', 'aria-label': t('.fewer_lives'), disabled: story_group.default_lives.to_i.zero?, data: { lives_stepper_target: 'down', action: 'lives-stepper#down' } } %i.fa-solid.fa-minus{ 'aria-hidden': 'true' } %output{ 'aria-live': 'polite', data: { lives_stepper_target: 'output' } }= story_group.default_lives - %button.gh-number-stepper-button.gh-octagon-clip{ type: 'button', 'aria-label': 'Więcej żyć', + %button.gh-number-stepper-button.gh-octagon-clip{ type: 'button', 'aria-label': t('.more_lives'), disabled: story_group.default_lives.to_i >= 10, data: { lives_stepper_target: 'up', action: 'lives-stepper#up' } } @@ -144,12 +137,10 @@ %i.fa-solid.fa-triangle-exclamation{ 'aria-hidden': 'true' } %span= error.call(:default_lives) - %span.gh-field-hint - Przy 0 życiach student kupi tylko przedmioty oznaczone jako - dostępne przy 0 życiach. + %span.gh-field-hint= t('.lives_hint') .gh-form-section - %h2.gh-h3 Ranking + %h2.gh-h3= t('.ranking_section') .gh-toggle-switch-row = f.check_box :ranking_enabled, class: 'gh-toggle-switch gh-octagon-clip', role: 'switch', @@ -157,60 +148,51 @@ data: { group_wizard_target: 'ranking', action: 'change->group-wizard#refresh' } %span - %b#gh-wiz-ranking-label Pokazuj ranking studentom - %span.gh-field-hint - Domyślnie wyłączony. Możesz go włączyć w każdej chwili - w zakładce Ranking. + %b#gh-wiz-ranking-label= t('.ranking') + %span.gh-field-hint= t('.ranking_hint') .gh-form-field{ hidden: !story_group.ranking_enabled?, data: { group_wizard_target: 'rankingMode' } } - = f.label :ranking_mode, 'Co widzą studenci' + = f.label :ranking_mode, t('.ranking_mode') %span.gh-select-input = f.select :ranking_mode, - StoryGroup.ranking_modes.keys.map { |mode| [StoryGroup::RANKING_MODE_LABELS[mode], mode] }, + StoryGroup.ranking_modes.keys.map { |mode| [StoryGroup.ranking_mode_label(mode), mode] }, {}, data: { action: 'group-wizard#refresh' } %i.fa-solid.fa-chevron-down{ 'aria-hidden': 'true' } - %span.gh-field-hint - „Podium i własne miejsce” pokazuje tylko najlepszą trójkę - i miejsce studenta. „Pełny ranking” pokazuje całą listę. + %span.gh-field-hint= t('.ranking_mode_hint') %section{ data: { group_wizard_target: 'panel' }, hidden: true } - %h2.sr-only Jak zaczynasz + %h2.sr-only= steps[2] .gh-form-section.gh-form-section--first - %h2.gh-h3 Jak chcesz zacząć? - %p.gh-field-hint Obie drogi dają pełną grupę. Wszystko zmienisz później. + %h2.gh-h3= t('.start_section') + %p.gh-field-hint= t('.start_hint') - .gh-choice-grid{ role: 'radiogroup', 'aria-label': 'Sposób startu' } + .gh-choice-grid{ role: 'radiogroup', 'aria-label': t('.start_label') } %label.gh-choice-card = radio_button_tag 'setup[path]', 'quick', false, class: 'sr-only', id: 'gh-wiz-path-quick', data: { group_wizard_target: 'path', action: 'group-wizard#refresh' } - %span.gh-tag.gh-tag--discount Polecane na pierwszą grupę - %b Szybki start - %span - Gotowe rangi, odznaki, przedmioty i kategorie zajęć. - Przejrzysz je i poprawisz przed utworzeniem grupy. + %span.gh-tag.gh-tag--discount= t('.recommended') + %b= t('.quick') + %span= t('.quick_body') %label.gh-choice-card = radio_button_tag 'setup[path]', 'manual', false, class: 'sr-only', id: 'gh-wiz-path-manual', data: { group_wizard_target: 'path', action: 'group-wizard#refresh' } - %b Od zera - %span - Pusta grupa. Rangi, odznaki i przedmioty dodasz później, - krok po kroku. + %b= t('.manual') + %span= t('.manual_body') %p.gh-field-error{ role: 'alert', hidden: true, data: { group_wizard_target: 'pathError' } } %i.fa-solid.fa-triangle-exclamation{ 'aria-hidden': 'true' } - %span Wybierz, jak chcesz zacząć. + %span= t('.path_missing') .gh-form-section{ hidden: true, data: { group_wizard_target: 'quickOnly' } } - %h2.gh-h3 Klimat zestawu - %p.gh-field-hint - Zmienia tylko nazwy rang i odznak. Zasady są wszędzie takie same. + %h2.gh-h3= t('.pack_section') + %p.gh-field-hint= t('.pack_hint') - .gh-choice-grid{ role: 'radiogroup', 'aria-label': 'Klimat' } + .gh-choice-grid{ role: 'radiogroup', 'aria-label': t('.pack_label') } - packs::KEYS.each_with_index do |key, index| %label.gh-choice-card = radio_button_tag 'setup[pack]', key, index.zero?, class: 'sr-only', @@ -218,23 +200,21 @@ data: { group_wizard_target: 'pack', action: 'group-wizard#refresh' } %b= packs.label_for(key) %span= packs.description_for(key) - %em= "#{packs::RANK_NAMES.fetch(key).first(3).join(' → ')} → …" + %em= "#{packs.rank_names(key).first(3).join(' → ')} → …" .gh-form-section{ hidden: true, data: { group_wizard_target: 'quickOnly' } } - %h2.gh-h3 Ile zajęć planujesz? - %p.gh-field-hint - Dopasujemy progi rang i ceny tak, żeby najwyższa ranga była - osiągalna w semestrze. + %h2.gh-h3= t('.classes_section') + %p.gh-field-hint= t('.classes_hint') .gh-form-field - .gh-number-stepper{ role: 'group', 'aria-label': 'Liczba zajęć' } - %button.gh-number-stepper-button.gh-octagon-clip{ type: 'button', 'aria-label': 'Mniej zajęć', + .gh-number-stepper{ role: 'group', 'aria-label': t('.classes_label') } + %button.gh-number-stepper-button.gh-octagon-clip{ type: 'button', 'aria-label': t('.fewer_classes'), data: { group_wizard_target: 'classesDown', action: 'group-wizard#classesDown' } } %i.fa-solid.fa-minus{ 'aria-hidden': 'true' } %output{ 'aria-live': 'polite', data: { group_wizard_target: 'classesOutput' } }= packs::DEFAULT_CLASSES - %button.gh-number-stepper-button.gh-octagon-clip{ type: 'button', 'aria-label': 'Więcej zajęć', + %button.gh-number-stepper-button.gh-octagon-clip{ type: 'button', 'aria-label': t('.more_classes'), data: { group_wizard_target: 'classesUp', action: 'group-wizard#classesUp' } } %i.fa-solid.fa-plus{ 'aria-hidden': 'true' } @@ -243,22 +223,20 @@ data: { group_wizard_target: 'classes' } %section{ data: { group_wizard_target: 'panel' }, hidden: true } - %h2.sr-only Podsumowanie + %h2.sr-only= steps[3] .gh-form-section.gh-form-section--first{ hidden: true, data: { group_wizard_target: 'manualSummary' } } - %h2.gh-h3 Podsumowanie - %p.gh-field-hint - Grupa powstanie pusta. Po utworzeniu zobaczysz listę kroków do - uzupełnienia. + %h2.gh-h3= t('.summary') + %p.gh-field-hint= t('.summary_hint') %dl.gh-summary-list - %dt Nazwa + %dt= t('shared.actions.name') %dd{ data: { group_wizard_target: 'sumName' } } - %dt Waluta + %dt= t('.summary_currency') %dd{ data: { group_wizard_target: 'sumCurrency' } } - %dt Życia na start + %dt= t('.summary_lives') %dd{ data: { group_wizard_target: 'sumLives' } } - %dt Ranking + %dt= t('.summary_ranking') %dd{ data: { group_wizard_target: 'sumRanking' } } = turbo_frame_tag 'starter_pack', @@ -266,10 +244,10 @@ action: 'turbo:frame-load->group-wizard#refresh' }, hidden: true - %aside.gh-preview-column{ 'aria-label': 'Podgląd', data: { group_wizard_target: 'preview' } } + %aside.gh-preview-column{ 'aria-label': t('shared.actions.preview'), data: { group_wizard_target: 'preview' } } %section.gh-panel.gh-preview-panel{ data: { group_wizard_target: 'previewGroup' } } - %h3 Karta na liście grup + %h3= t('.card_preview') %article.gh-card.gh-card--neutral.gh-group-index-card .gh-card-frame .gh-card-inset @@ -277,34 +255,33 @@ %span{ data: { group_wizard_target: 'artMono' } } .gh-card-header %h2.gh-card-name.gh-placeholder-text{ data: { group_wizard_target: 'nameEcho' } } - Nazwa grupy + = t('.js.name_placeholder') %p.gh-group-index-meta - %span Prowadzisz tę grupę + %span= t('.you_lead') - %span.gh-field-hint - Tło za formularzem pokazuje, jak grafika wygląda jako tło grupy. + %span.gh-field-hint= t('.background_hint') %section.gh-panel.gh-preview-panel{ hidden: true, data: { group_wizard_target: 'previewCurrency' } } - %h3 Tak zobaczą walutę studenci + %h3= t('.currency_preview') .gh-currency-preview-pair - %w[light dark].each do |theme| = render 'currency_mini', theme: theme .gh-panel.gh-form-actions-bar - = link_to 'Anuluj', story_groups_path, class: 'gh-btn gh-btn--secondary', + = link_to t('shared.actions.cancel'), story_groups_path, class: 'gh-btn gh-btn--secondary', data: { group_wizard_target: 'cancel' } - %button.gh-btn.gh-btn--secondary{ type: 'button', hidden: true, 'aria-label': 'Wstecz', + %button.gh-btn.gh-btn--secondary{ type: 'button', hidden: true, 'aria-label': t('.back'), data: { group_wizard_target: 'back', action: 'group-wizard#back' } } %i.fa-solid.fa-chevron-left{ 'aria-hidden': 'true' } - %span.gh-form-actions-bar-label Wstecz + %span.gh-form-actions-bar-label= t('.back') %span.gh-form-actions-spacer - %span.gh-page-kicker{ data: { group_wizard_target: 'counter' } } Krok 1 z 4 + %span.gh-page-kicker{ data: { group_wizard_target: 'counter' } }= t('.step_counter', step: 1, total: steps.size) %button.gh-btn{ type: 'button', data: { group_wizard_target: 'next', action: 'group-wizard#next' } } - %span.gh-form-actions-bar-label{ data: { group_wizard_target: 'nextLabel' } } Dalej - %span.gh-form-actions-bar-label--compact{ data: { group_wizard_target: 'nextLabelShort' } } Dalej + %span.gh-form-actions-bar-label{ data: { group_wizard_target: 'nextLabel' } }= t('shared.actions.next') + %span.gh-form-actions-bar-label--compact{ data: { group_wizard_target: 'nextLabelShort' } }= t('shared.actions.next') %i.fa-solid.fa-chevron-right{ 'aria-hidden': 'true', data: { group_wizard_target: 'nextIcon' } } - = f.submit 'Utwórz grupę', class: 'gh-btn', hidden: true, + = f.submit t('.submit'), class: 'gh-btn', hidden: true, data: { group_wizard_target: 'submit' } diff --git a/app/views/story_groups/confirm_destroy.html.haml b/app/views/story_groups/confirm_destroy.html.haml index 54d12d5d..ab3323d7 100644 --- a/app/views/story_groups/confirm_destroy.html.haml +++ b/app/views/story_groups/confirm_destroy.html.haml @@ -1,4 +1,4 @@ -- content_for :title, "Usuń grupę #{@story_group.name}" +- content_for :title, t('.page_title', name: @story_group.name) - if @in_modal = turbo_frame_tag 'modal' do diff --git a/app/views/story_groups/created.html.haml b/app/views/story_groups/created.html.haml index 4f42b3dc..7d933abc 100644 --- a/app/views/story_groups/created.html.haml +++ b/app/views/story_groups/created.html.haml @@ -1,47 +1,30 @@ -- content_for :title, "Grupa #{@story_group.name} jest gotowa" +- content_for :title, t('.page_title', name: @story_group.name) - quick = @counts.values.any?(&:positive?) -- created_list = [gh_plural(@counts[:ranks], 'rangę', 'rangi', 'rang'), - gh_plural(@counts[:badges], 'odznakę', 'odznaki', 'odznak'), - gh_plural(@counts[:items], 'przedmiot', 'przedmioty', 'przedmiotów'), - gh_plural(@counts[:categories], 'kategorię', 'kategorie', 'kategorii')] -- created_parts = %i[ranks badges items categories].each_with_index.map { |key, i| "#{@counts[key]} #{created_list[i]}" } -- created_parts[3] = "#{created_parts[3]} w szablonie arkusza" +- created_parts = %i[ranks badges items categories].map { |key| t(".#{key}", count: @counts[key]) } %section.gh-panel.gh-wizard-success-panel .gh-wizard-success-icon.gh-octagon-clip %i.fa-solid.fa-check{ 'aria-hidden': 'true' } - %h1.gh-h1= "Grupa „#{@story_group.name}” jest gotowa" + %h1.gh-h1= t('.title', name: @story_group.name) %p.gh-lead - if quick - = "Utworzono #{gh_and_list(created_parts)}." + = t('.quick_lead', list: gh_and_list(created_parts)) - else - Grupa jest pusta. Poniżej znajdziesz kolejne kroki. + = t('.empty_lead') %p.gh-explainer-note %i.fa-solid.fa-qrcode{ 'aria-hidden': 'true' } - %span - Kod do dołączenia utworzysz w zakładce - %b Zaproszenia - \, kiedy będziesz go potrzebować. Możesz mieć kilka kodów naraz, - z limitem osób albo datą ważności. + %span= t('.invites_note_html', tab: tag.b(t('.invites_tab'))) - %h2.gh-h3 Co dalej + %h2.gh-h3= t('.next') %ol.gh-next-steps-list - - if quick - %li Zaproś studentów: pokaż im kod albo kod QR. - %li Po pierwszych zajęciach utwórz arkusz ocen i oceń studentów. - %li Zajrzyj do sklepu i dopasuj opisy przedmiotów do swojej opowieści. - - else - %li Dodaj rangi, żeby studenci widzieli, do czego dążą. - %li Dodaj przedmioty do sklepu. - %li Dodaj odznaki za szczególne osiągnięcia. - %li Utwórz szablon arkusza ocen. - %li Zaproś studentów: pokaż im kod albo kod QR. + - (quick ? t('.quick_steps') : t('.empty_steps')).each do |step| + %li= step .gh-button-row - = link_to 'Przejdź do grupy', story_group_path(@story_group), class: 'gh-btn' + = link_to t('.go_to_group'), story_group_path(@story_group), class: 'gh-btn' = link_to new_story_group_invite_path(@story_group), class: 'gh-btn gh-btn--secondary' do %i.fa-solid.fa-qrcode{ 'aria-hidden': 'true' } - Utwórz zaproszenie + = t('.create_invite') diff --git a/app/views/story_groups/edit.html.haml b/app/views/story_groups/edit.html.haml index 688cc398..703f0f6e 100644 --- a/app/views/story_groups/edit.html.haml +++ b/app/views/story_groups/edit.html.haml @@ -1,3 +1,3 @@ -- content_for :title, "Ustawienia grupy - #{@story_group.name}" +- content_for :title, t('.page_title', name: @story_group.name) = render 'settings', story_group: @story_group diff --git a/app/views/story_groups/index.html.haml b/app/views/story_groups/index.html.haml index 56848dc9..670d0d12 100644 --- a/app/views/story_groups/index.html.haml +++ b/app/views/story_groups/index.html.haml @@ -1,25 +1,21 @@ -- content_for :title, 'Grupy' +- content_for :title, t('.title') %section.gh-panel.gh-page-header .gh-page-header-text - %h1.gh-h1 Grupy - %p.gh-lead - - if @current_user.student? - Grupy, w których się uczysz. - - else - Grupy, które prowadzisz, wspierasz albo w których się uczysz. + %h1.gh-h1= t('.title') + %p.gh-lead= @current_user.student? ? t('.student_lead') : t('.teacher_lead') .gh-button-row = link_to new_join_path, class: 'gh-btn gh-btn--secondary', data: { turbo_frame: 'modal' } do %i.fa-solid.fa-plus - Dołącz do grupy + = t('.join') - if policy(:story_group).create? = link_to new_story_group_path, class: 'gh-btn' do %i.fa-solid.fa-plus - Utwórz grupę + = t('.create') - if @listing.tabs.size > 1 - %nav.gh-group-tabs{ 'aria-label': 'Filtr grup' } + %nav.gh-group-tabs{ 'aria-label': t('.filter') } - @listing.tabs.each do |tab| = link_to story_groups_path(filter: tab.key), class: 'gh-group-tab', 'aria-current': (@listing.filter == tab.key).to_s do @@ -32,8 +28,8 @@ %label.gh-search-input.gh-search-input--wide %i.fa-solid.fa-magnifying-glass{ 'aria-hidden': 'true' } %input{ type: 'search', - placeholder: 'Szukaj grupy', - 'aria-label': 'Szukaj grupy', + placeholder: t('.search'), + 'aria-label': t('.search'), data: { group_search_target: 'input', action: 'input->group-search#filter' } } .gh-group-index-grid @@ -42,21 +38,17 @@ %section.gh-panel.gh-empty-state{ hidden: true, data: { group_search_target: 'empty' } } %h2.gh-h2 - Brak grup o nazwie + = t('.no_match') %span{ data: { group_search_target: 'emptyQuery' } } - %p.gh-lead Wyczyść wyszukiwanie albo sprawdź pisownię. + %p.gh-lead= t('.no_match_hint') - else %section.gh-panel.gh-empty-state %h2.gh-h2 - if @listing.filter - Brak grup w tej zakładce + = t('.empty_tab_title') - elsif @current_user.student? - Nie należysz jeszcze do żadnej grupy + = t('.empty_student_title') - else - Nie masz jeszcze żadnej grupy - %p.gh-lead - - if @listing.filter - Zmień zakładkę, żeby zobaczyć pozostałe grupy. - - else - Masz kod od prowadzącego? Dołącz do grupy jako uczestnik. + = t('.empty_title') + %p.gh-lead= @listing.filter ? t('.empty_tab_body') : t('.empty_body') diff --git a/app/views/story_groups/new.html.haml b/app/views/story_groups/new.html.haml index 2688b3c1..2fb60530 100644 --- a/app/views/story_groups/new.html.haml +++ b/app/views/story_groups/new.html.haml @@ -1,3 +1,3 @@ -- content_for :title, 'Nowa grupa' +- content_for :title, t('.page_title') = render 'wizard', story_group: @story_group diff --git a/app/views/students/_confirm_destroy.html.haml b/app/views/students/_confirm_destroy.html.haml index 4afeb0da..64229930 100644 --- a/app/views/students/_confirm_destroy.html.haml +++ b/app/views/students/_confirm_destroy.html.haml @@ -1,29 +1,26 @@ - losses = student_removal_losses(sheet) -%h2#gh-kick-title.gh-h2 Usunąć studenta z grupy? -%p - = "#{student.display_name} straci dostęp do grupy" - i zniknie z list, rankingu i arkuszy ocen. +%h2#gh-kick-title.gh-h2= t('.title') +%p= t('.access_lost', name: student.display_name) - if losses.any? %p.gh-warning-note{ role: 'alert' } %i.fa-solid.fa-triangle-exclamation{ 'aria-hidden': 'true' } %span - = "Przepadną: #{gh_and_list(losses)}." - Tego nie da się cofnąć. + = t('.losses', list: gh_and_list(losses)) + = t('.irreversible') - else - %p.gh-explainer-note Ten student nie ma jeszcze odznak, przedmiotów ani historii waluty. + %p.gh-explainer-note= t('.nothing_to_lose') -%p.gh-small - Jeśli wróci do grupy, dostanie nowe konto uczestnika i zacznie od zera. +%p.gh-small= t('.rejoin_note') .gh-dialog-button-row - if @in_modal %button.gh-btn.gh-btn--secondary{ type: 'button', data: { action: 'dialog#close' }, autofocus: true } - Anuluj + = t('shared.actions.cancel') - else - = link_to 'Anuluj', story_group_student_path(@story_group, student), class: 'gh-btn gh-btn--secondary' + = link_to t('shared.actions.cancel'), story_group_student_path(@story_group, student), class: 'gh-btn gh-btn--secondary' - = button_to 'Usuń z grupy', story_group_student_path(@story_group, student), + = button_to t('.remove'), story_group_student_path(@story_group, student), method: :delete, class: 'gh-btn', form: { data: { turbo_frame: (@in_modal ? 'modal2' : nil) } } diff --git a/app/views/students/_form.html.haml b/app/views/students/_form.html.haml index 818de023..78df4f81 100644 --- a/app/views/students/_form.html.haml +++ b/app/views/students/_form.html.haml @@ -7,30 +7,28 @@ turbo_frame: (@in_modal ? 'modal' : nil), lives_stepper_start_value: lives } do |f| - %h2#gh-student-title.gh-h2 Edytuj studenta + %h2#gh-student-title.gh-h2= t('.title') %p = "#{student.display_name}." - Imię, nazwisko i e-mail pochodzą z konta studenta. + = t('.account_note') .gh-form-field - %p#gh-lives-label.gh-form-label Życia + %p#gh-lives-label.gh-form-label= t('.lives') .gh-number-stepper{ role: 'group', 'aria-labelledby': 'gh-lives-label' } - %button.gh-number-stepper-button.gh-octagon-clip{ type: 'button', 'aria-label': 'Odbierz życie', + %button.gh-number-stepper-button.gh-octagon-clip{ type: 'button', 'aria-label': t('.take_life'), disabled: lives.zero?, data: { lives_stepper_target: 'down', action: 'lives-stepper#down' } } %i.fa-solid.fa-minus{ 'aria-hidden': 'true' } %output{ 'aria-live': 'polite', data: { lives_stepper_target: 'output' } }= lives - %button.gh-number-stepper-button.gh-octagon-clip{ type: 'button', 'aria-label': 'Dodaj życie', + %button.gh-number-stepper-button.gh-octagon-clip{ type: 'button', 'aria-label': t('.add_life'), data: { lives_stepper_target: 'up', action: 'lives-stepper#up' } } %i.fa-solid.fa-plus{ 'aria-hidden': 'true' } = f.hidden_field :lives, value: lives, data: { lives_stepper_target: 'field' } - %span.gh-field-hint - Życie odbierasz za nieusprawiedliwioną nieobecność. Przy 0 życiach - student kupi tylko przedmioty dostępne przy 0 życiach. + %span.gh-field-hint= t('.lives_hint') - if error %p.gh-field-error{ role: 'alert' } @@ -38,19 +36,17 @@ %span= error .gh-form-section.gh-form-section--divider - %p.gh-form-label Usuń z grupy - %p.gh-small - Student straci dostęp do grupy, a jego odznaki, przedmioty i historia - waluty przepadną. Dołączy ponownie tylko z nowym kodem. + %p.gh-form-label= t('.remove_label') + %p.gh-small= t('.remove_hint') = link_to confirm_destroy_story_group_student_path(@story_group, student), class: 'gh-btn gh-btn--secondary gh-btn--small gh-remove-student-btn', data: { turbo_frame: 'modal2' } do %i.fa-solid.fa-trash{ 'aria-hidden': 'true' } - Usuń z grupy + = t('.remove') .gh-dialog-button-row - if @in_modal - %button.gh-btn.gh-btn--secondary{ type: 'button', data: { action: 'dialog#close' } } Anuluj + %button.gh-btn.gh-btn--secondary{ type: 'button', data: { action: 'dialog#close' } }= t('shared.actions.cancel') - else - = link_to 'Anuluj', story_group_student_path(@story_group, student), class: 'gh-btn gh-btn--secondary' - %button.gh-btn{ type: 'submit', data: { lives_stepper_target: 'submit' } } Zapisz + = link_to t('shared.actions.cancel'), story_group_student_path(@story_group, student), class: 'gh-btn gh-btn--secondary' + %button.gh-btn{ type: 'submit', data: { lives_stepper_target: 'submit' } }= t('shared.actions.save') diff --git a/app/views/students/_row.html.haml b/app/views/students/_row.html.haml index 0fef06f5..7c50a873 100644 --- a/app/views/students/_row.html.haml +++ b/app/views/students/_row.html.haml @@ -13,17 +13,17 @@ %small= student_list_subtitle(student) %span - %span.gh-mobile-field-label Ranga + %span.gh-mobile-field-label= t('.rank') %span.gh-student-rank-cell= row.rank&.name || '—' %span - %span.gh-mobile-field-label Życia + %span.gh-mobile-field-label= t('.lives') .gh-lives-control = button_to update_lives_story_group_student_path(@story_group, student, change: -1), method: :post, class: 'gh-number-stepper-button gh-number-stepper-button--sm gh-octagon-clip', disabled: lives.zero?, - form: { 'aria-label': "Odbierz życie: #{name}" }, - 'aria-label': "Odbierz życie: #{name}" do + form: { 'aria-label': t('.take_life', name: name) }, + 'aria-label': t('.take_life', name: name) do %i.fa-solid.fa-minus{ 'aria-hidden': 'true' } %span.gh-lives-badge{ class: ('gh-lives-badge--zero' if lives.zero?) } @@ -32,34 +32,34 @@ = button_to update_lives_story_group_student_path(@story_group, student, change: 1), method: :post, class: 'gh-number-stepper-button gh-number-stepper-button--sm gh-octagon-clip', - form: { 'aria-label': "Dodaj życie: #{name}" }, - 'aria-label': "Dodaj życie: #{name}" do + form: { 'aria-label': t('.add_life', name: name) }, + 'aria-label': t('.add_life', name: name) do %i.fa-solid.fa-plus{ 'aria-hidden': 'true' } %span - %span.gh-mobile-field-label Do wydania + %span.gh-mobile-field-label= t('.balance') %span.gh-numeric-cell= student.current_currency.to_i %span - %span.gh-mobile-field-label Zebrane + %span.gh-mobile-field-label= t('.total') %span.gh-numeric-cell= student.total_currency.to_i %span - %span.gh-mobile-field-label Odznaki + %span.gh-mobile-field-label= t('.badges') %span.gh-numeric-cell= row.badge_count .gh-student-actions-cell = link_to new_story_group_student_badge_path(@story_group, student), - class: 'gh-icon-button', title: 'Przyznaj odznakę', - 'aria-label': "Przyznaj odznakę: #{name}", + class: 'gh-icon-button', title: t('.award_badge'), + 'aria-label': t('.award_badge_for', name: name), data: { turbo_frame: 'modal' } do %i.fa-solid.fa-award{ 'aria-hidden': 'true' } = link_to new_story_group_student_currency_adjustment_path(@story_group, student), - class: 'gh-icon-button', title: 'Koryguj walutę', - 'aria-label': "Koryguj walutę: #{name}", + class: 'gh-icon-button', title: t('.adjust_currency'), + 'aria-label': t('.adjust_currency_for', name: name), data: { turbo_frame: 'modal' } do %i.fa-solid.fa-coins{ 'aria-hidden': 'true' } = link_to story_group_student_path(@story_group, student, tab: 'hist'), - class: 'gh-icon-button', title: 'Historia transakcji', - 'aria-label': "Historia: #{name}" do + class: 'gh-icon-button', title: t('.history'), + 'aria-label': t('.history_for', name: name) do %i.fa-solid.fa-clock-rotate-left{ 'aria-hidden': 'true' } diff --git a/app/views/students/_tab_badges.html.haml b/app/views/students/_tab_badges.html.haml index 35c32de8..0ac06b29 100644 --- a/app/views/students/_tab_badges.html.haml +++ b/app/views/students/_tab_badges.html.haml @@ -15,18 +15,18 @@ - if (discount = badge_discount_label(badge)) %span.gh-tag.gh-tag--discount= discount - if badge.deleted? - %span.gh-tag.gh-tag--removed Usunięta z listy + %span.gh-tag.gh-tag--removed= t('.removed') %p.gh-rule-text= badge.didactic_description - if badge.story_description.present? %p.gh-card-flavor= badge.story_description .gh-card-footer - %span.gh-card-meta= "Przyznana #{gh_when(students_badge.created_at)}" - = link_to 'Odbierz', + %span.gh-card-meta= t('.awarded', when: gh_when(students_badge.created_at)) + = link_to t('.revoke'), confirm_destroy_story_group_student_badge_path(@story_group, sheet.student, students_badge), class: 'gh-link-button', - 'aria-label': "Odbierz odznakę #{badge.name}", + 'aria-label': t('.revoke_label', name: badge.name), data: { turbo_frame: 'modal' } - else - %p.gh-explainer-note Student nie ma jeszcze żadnej odznaki. + %p.gh-explainer-note= t('.empty') diff --git a/app/views/students/_tab_history.html.haml b/app/views/students/_tab_history.html.haml index 891e8808..cab77949 100644 --- a/app/views/students/_tab_history.html.haml +++ b/app/views/students/_tab_history.html.haml @@ -3,4 +3,4 @@ = render 'currency_transactions/ledger', ledger: sheet.ledger, kind: kind, path_for: path_for, summary: ledger_summary_sentence(sheet.ledger), - empty_message: 'Brak wpisów tego typu.' + empty_message: t('.empty') diff --git a/app/views/students/_tab_items.html.haml b/app/views/students/_tab_items.html.haml index affd262c..e45e2150 100644 --- a/app/views/students/_tab_items.html.haml +++ b/app/views/students/_tab_items.html.haml @@ -1,6 +1,4 @@ -%p.gh-small.gh-tab-panel-intro - Przedmioty kupione w sklepie. Wykorzystanie przedmiotu (np. poprawę - wejściówki) rozliczasz na zajęciach. +%p.gh-small.gh-tab-panel-intro= t('.intro') - if sheet.purchases.any? .gh-item-list @@ -8,6 +6,4 @@ = render 'students_items/card', purchase: purchase, cost: true, foot: purchase_teacher_line(purchase) - else - %p.gh-explainer-note - Student nic jeszcze nie kupił. Przedmioty pojawią się tu razem z ceną, - jaką zapłacił. + %p.gh-explainer-note= t('.empty') diff --git a/app/views/students/confirm_destroy.html.haml b/app/views/students/confirm_destroy.html.haml index df74ff73..2413e968 100644 --- a/app/views/students/confirm_destroy.html.haml +++ b/app/views/students/confirm_destroy.html.haml @@ -1,4 +1,4 @@ -- content_for :title, "Usuń z grupy - #{@student.display_name}" +- content_for :title, t('.page_title', name: @student.display_name) - if @in_modal = turbo_frame_tag 'modal2' do diff --git a/app/views/students/edit.html.haml b/app/views/students/edit.html.haml index b1dac198..2d08d7e2 100644 --- a/app/views/students/edit.html.haml +++ b/app/views/students/edit.html.haml @@ -1,4 +1,4 @@ -- content_for :title, "Edytuj studenta - #{@student.display_name}" +- content_for :title, t('.page_title', name: @student.display_name) - if @in_modal = turbo_frame_tag 'modal' do diff --git a/app/views/students/index.html.haml b/app/views/students/index.html.haml index 6e10c9f7..3740ee37 100644 --- a/app/views/students/index.html.haml +++ b/app/views/students/index.html.haml @@ -1,60 +1,58 @@ -- content_for :title, 'Studenci' +- content_for :title, t('.title') %section.gh-panel.gh-page-header .gh-page-header-text - %h1.gh-h1 Studenci + %h1.gh-h1= t('.title') %p.gh-lead= student_list_lead(@list) .gh-button-row = link_to story_group_invites_path(@story_group), class: 'gh-btn gh-btn--secondary' do %i.fa-solid.fa-qrcode{ 'aria-hidden': 'true' } - Kody i zaproszenia + = t('.invites') = link_to quick_story_group_invites_path(@story_group), class: 'gh-btn', data: { turbo_frame: 'modal' } do %i.fa-solid.fa-eye{ 'aria-hidden': 'true' } - Pokaż kod dla studentów + = t('.show_code') - if @list.any? %div{data: { controller: 'list-search', list_search_total_value: @list.size, - list_search_of_value: 'z', }} + list_search_of_value: t('.of'), }} .gh-student-list-toolbar %label.gh-search-input %i.fa-solid.fa-magnifying-glass{ 'aria-hidden': 'true' } %input{type: 'search', - placeholder: 'Szukaj studenta', - 'aria-label': 'Szukaj studenta', + placeholder: t('.search'), + 'aria-label': t('.search'), data: { list_search_target: 'input', action: 'input->list-search#filter' },} %span.gh-card-meta{ data: { list_search_target: 'count' } } - = "#{@list.size} z #{@list.size}" + = t('.count', shown: @list.size, total: @list.size) %section.gh-panel.gh-student-list-panel %ul.gh-student-rows{ role: 'list' } %li.gh-student-row.gh-student-row--header{ 'aria-hidden': 'true' } - %span Student - %span Ranga - %span Życia - %span Do wydania - %span Zebrane - %span Odznaki - %span.gh-ledger-cell--right Akcje + %span= t('.columns.student') + %span= t('.columns.rank') + %span= t('.columns.lives') + %span= t('.columns.balance') + %span= t('.columns.total') + %span= t('.columns.badges') + %span.gh-ledger-cell--right= t('.columns.actions') = render partial: 'students/row', collection: @list.rows, as: :row %section.gh-panel.gh-empty-state{ hidden: true, data: { list_search_target: 'empty' } } %h2.gh-h2 - Brak studentów o nazwie + = t('.no_match') %span{ data: { list_search_target: 'emptyQuery' } } - %p.gh-lead Wyczyść wyszukiwanie albo sprawdź pisownię. + %p.gh-lead= t('.no_match_hint') - else %section.gh-panel.gh-empty-state - %h2.gh-h2 Nie ma jeszcze nikogo w grupie - %p.gh-lead - Wyślij studentom kod albo zaproszenie — kiedy dołączą, pojawią się tutaj - razem z rangą, walutą i życiami. + %h2.gh-h2= t('.empty_title') + %p.gh-lead= t('.empty_body') .gh-button-row - = link_to 'Kody i zaproszenia', story_group_invites_path(@story_group), class: 'gh-btn' + = link_to t('.invites'), story_group_invites_path(@story_group), class: 'gh-btn' = link_to quick_story_group_invites_path(@story_group), class: 'gh-btn gh-btn--secondary', data: { turbo_frame: 'modal' } do %i.fa-solid.fa-eye{ 'aria-hidden': 'true' } - Pokaż kod dla studentów + = t('.show_code') diff --git a/app/views/students/show.html.haml b/app/views/students/show.html.haml index 4da24aeb..3394ceea 100644 --- a/app/views/students/show.html.haml +++ b/app/views/students/show.html.haml @@ -6,7 +6,7 @@ %span.gh-avatar.gh-octagon-clip{ 'data-gh-h': gh_avatar_hue(name), 'aria-hidden': 'true' }= gh_initials(name) %div %p.gh-breadcrumbs - = link_to 'Studenci', story_group_students_path(@story_group) + = link_to t('.breadcrumb'), story_group_students_path(@story_group) %i.fa-solid.fa-chevron-right{ 'aria-hidden': 'true' } %span= name %h1.gh-h1= name @@ -16,19 +16,19 @@ = link_to new_story_group_student_badge_path(@story_group, @student), class: 'gh-btn', data: { turbo_frame: 'modal' } do %i.fa-solid.fa-award{ 'aria-hidden': 'true' } - Przyznaj odznakę + = t('.award_badge') = link_to new_story_group_student_currency_adjustment_path(@story_group, @student), class: 'gh-btn gh-btn--secondary', data: { turbo_frame: 'modal' } do %i.fa-solid.fa-coins{ 'aria-hidden': 'true' } - Koryguj walutę + = t('.adjust_currency') = link_to edit_story_group_student_path(@story_group, @student), class: 'gh-btn gh-btn--secondary', data: { turbo_frame: 'modal' } do %i.fa-solid.fa-pen{ 'aria-hidden': 'true' } - Edytuj + = t('shared.actions.edit') %dl.gh-student-stats .gh-student-stat - %dt Ranga + %dt= t('.rank') %dd = @sheet.rank&.name || '—' - if (line = student_rank_progress_line(@sheet)) @@ -36,27 +36,27 @@ - if (progress = @sheet.rank_progress) .gh-progress-bar.gh-octagon-clip{ role: 'progressbar', 'aria-valuemin': 0, 'aria-valuemax': 100, 'aria-valuenow': progress, - 'aria-label': "Postęp do rangi #{@sheet.next_rank.name}" } + 'aria-label': t('.rank_progress_label', rank: @sheet.next_rank.name) } %i.gh-octagon-clip{ style: "--gh-p: #{progress}%" } .gh-student-stat - %dt Do wydania + %dt= t('.balance') %dd= @sheet.balance .gh-student-stat - %dt Zebrane łącznie + %dt= t('.total') %dd= @sheet.total .gh-student-stat - %dt Życia + %dt= t('.lives') %dd %span.gh-lives-badge{ class: ('gh-lives-badge--zero' if @sheet.lives.zero?) } %i.fa-solid.fa-heart{ 'aria-hidden': 'true' } = @sheet.lives -%nav.gh-group-tabs{ 'aria-label': 'Sekcje studenta' } - - StudentSheet::TABS.each do |key, label| +%nav.gh-group-tabs{ 'aria-label': t('.sections') } + - StudentSheet::TABS.each do |key| = link_to story_group_student_path(@story_group, @student, tab: key), class: 'gh-group-tab', 'aria-current': (@tab == key).to_s do - = label + = t(".tabs.#{key}") %span.gh-count-label= @sheet.count_for(key) %section.gh-panel.gh-tab-panel diff --git a/app/views/students_activity_group_categories/_review_dialog.html.haml b/app/views/students_activity_group_categories/_review_dialog.html.haml index 1d6eed95..18e18729 100644 --- a/app/views/students_activity_group_categories/_review_dialog.html.haml +++ b/app/views/students_activity_group_categories/_review_dialog.html.haml @@ -3,14 +3,14 @@ action: 'close->grading#closeReview click->grading#closeOnBackdrop' } } .gh-dialog-body.gh-octagon-clip %h2#gh-review-title.gh-h2 - Przyznajesz + = t('.awarding') %span{ data: { grading_target: 'reviewSum' } } = @story_group.currency_name %span{ data: { grading_target: 'reviewStudents' } } %p.gh-warning-note{ role: 'alert' } %i.fa-solid.fa-lock{ 'aria-hidden': 'true' } - Po zatwierdzeniu nie da się tego cofnąć. Sprawdź listę, zanim przyznasz nagrody. + = t('.warning') %ul.gh-review-list - students.each do |student| @@ -34,8 +34,8 @@ .gh-dialog-button-row %button.gh-btn.gh-btn--secondary{ type: 'button', autofocus: true, data: { action: 'grading#closeReview' } } - Wróć do edycji + = t('shared.actions.back_to_editing') %button.gh-btn.gh-btn--gold{ type: 'submit', form: 'gh-grade-form' } - Przyznaj + = t('.confirm') %span{ data: { grading_target: 'reviewConfirmSum' } } = @story_group.currency_name diff --git a/app/views/students_activity_group_categories/edit.html.haml b/app/views/students_activity_group_categories/edit.html.haml index dc795a66..b897f0e8 100644 --- a/app/views/students_activity_group_categories/edit.html.haml +++ b/app/views/students_activity_group_categories/edit.html.haml @@ -3,46 +3,37 @@ - students = @sheet.students - template = @activity_group.activity_group_template -- grid_data = { controller: 'grading', - grading_cells_value: %w[pole pola pól].to_json, - grading_students_value: %w[studenta studentów studentów].to_json, - grading_recipients_value: %w[studentowi studentom studentom].to_json, - grading_states_value: %w[puste zaznaczone przyznane].to_json, - grading_currency_value: @story_group.currency_name.to_s, - grading_award_label_value: 'Przyznaj' } +- grid_data = { controller: 'grading', grading_i18n_value: t('.js').to_json } .gh-grading-screen{ data: grid_data } %section.gh-panel.gh-page-header.gh-grading-head .gh-page-header-text %p.gh-breadcrumbs - = link_to 'Arkusze ocen', story_group_activity_groups_path(@story_group) + = link_to t('layouts.navigation.sheets'), story_group_activity_groups_path(@story_group) %i.fa-solid.fa-chevron-right{ 'aria-hidden': 'true' } %span= template.base_name %h1.gh-h1= @activity_group.name - %p.gh-lead - = "#{categories.size} #{gh_plural(categories.size, 'kategoria', 'kategorie', 'kategorii')}," - = "#{students.size} #{gh_plural(students.size, 'student', 'studentów', 'studentów')}." - Zaznacz, kto zdobył nagrody, a przed przyznaniem przejrzysz wszystko jeszcze raz. + %p.gh-lead= t('.lead', categories: t('.categories', count: categories.size), count: students.size) .gh-grading-head-actions %label.gh-search-input %i.fa-solid.fa-magnifying-glass{ 'aria-hidden': 'true' } - = search_field_tag :student_search, nil, placeholder: 'Szukaj studenta', - 'aria-label': 'Szukaj studenta', + = search_field_tag :student_search, nil, placeholder: t('.search'), + 'aria-label': t('.search'), data: { grading_target: 'search', action: 'input->grading#filter' } - %ul.gh-cell-legend{ 'aria-label': 'Legenda' } + %ul.gh-cell-legend{ 'aria-label': t('.legend') } %li %span.gh-grading-cell.gh-grading-cell--sm{ 'aria-hidden': 'true' } - Puste + = t('.empty') %li %span.gh-grading-cell.gh-grading-cell--sm.gh-grading-cell--pending{ 'aria-hidden': 'true' } %i.fa-solid.fa-check - Zaznaczone teraz + = t('.marked_now') %li %span.gh-grading-cell.gh-grading-cell--sm.gh-grading-cell--awarded{ 'aria-hidden': 'true' } %i.fa-solid.fa-check - Przyznane, nie do cofnięcia + = t('.awarded_locked') - if @sheet.gradeable? = form_with url: story_group_activity_group_students_activity_group_categories_path(@story_group, @activity_group), method: :patch, id: 'gh-grade-form', class: 'gh-grading-form' do @@ -51,7 +42,7 @@ %table.gh-grading-table %thead %tr - %th.gh-grading-th-student{ scope: 'col' } Student + %th.gh-grading-th-student{ scope: 'col' }= t('.student') - categories.each do |category| %th{ scope: 'col' } .gh-grading-column-head @@ -60,12 +51,12 @@ %span.gh-price-badge %b= "+#{category.reward.to_i}" = render 'layouts/currency_token', story_group: @story_group, size: 16 - %button.gh-grading-column-select-all{ type: 'button', title: 'Zaznacz całą kolumnę', + %button.gh-grading-column-select-all{ type: 'button', title: t('.select_column'), data: { action: 'grading#toggleColumn', grading_column_param: category.id } } %i.fa-solid.fa-check-double{ 'aria-hidden': 'true' } - Wszyscy - %th.gh-grading-th-total{ scope: 'col' } Razem + = t('.everyone') + %th.gh-grading-th-total{ scope: 'col' }= t('.total') %tbody - students.each do |student| @@ -79,13 +70,14 @@ = name - categories.each do |category| + - label = "#{name}, #{category.didactic_description}" %td - if @sheet.awarded?(student, category) - fresh = @awarded_pairs.include?([category.id, student.id]) %span.gh-grading-cell.gh-grading-cell--awarded{ class: ('gh-grading-cell--stamp' if fresh), - title: 'Przyznane, nie można cofnąć', + title: t('.locked_title'), role: 'img', - 'aria-label': "#{name}, #{category.didactic_description}: przyznane", + 'aria-label': t('.cell_awarded', label: label), data: { action: 'click->grading#locked' } } %i.fa-solid.fa-check{ 'aria-hidden': 'true' } %i.fa-solid.fa-lock.gh-grading-cell-lock-icon{ 'aria-hidden': 'true' } @@ -95,9 +87,9 @@ grading_column: category.id } } = check_box_tag "completions[#{student.id}][#{category.id}]", '1', false, id: nil, - 'aria-label': "#{name}, #{category.didactic_description}: puste", + 'aria-label': t('.cell_empty', label: label), data: { action: 'grading#toggle', - gh_label: "#{name}, #{category.didactic_description}" } + gh_label: label } %i.fa-solid.fa-check{ 'aria-hidden': 'true' } %td.gh-grading-td-total @@ -106,8 +98,7 @@ %section.gh-panel.gh-action-bar{ 'aria-live': 'polite' } %p.gh-action-bar-text - %span{ data: { grading_target: 'barEmpty' } } - Nic nie jest zaznaczone. Kliknij pola studentów, którzy zdobyli nagrody. + %span{ data: { grading_target: 'barEmpty' } }= t('.nothing_marked') %span{ hidden: true, data: { grading_target: 'barActive' } } %b{ data: { grading_target: 'barCells' } } %span{ data: { grading_target: 'barStudents' } } @@ -118,39 +109,33 @@ .gh-action-bar-buttons %button.gh-btn.gh-btn--secondary{ type: 'button', hidden: true, data: { grading_target: 'clear', action: 'grading#clear' } } - Wyczyść zaznaczenia + = t('.clear') %button.gh-btn{ type: 'submit', data: { grading_target: 'submit', action: 'grading#review' } } - Przejrzyj i przyznaj + = t('.review') = render 'review_dialog', categories: categories, students: students - elsif categories.empty? %section.gh-panel.gh-empty-state - %h2.gh-h2 Ten arkusz nie ma żadnej kolumny - %p.gh-lead - Dodaj kolumny w ustawieniach arkusza, żeby było za co nagradzać. + %h2.gh-h2= t('.no_columns_title') + %p.gh-lead= t('.no_columns_body') .gh-button-row - = link_to 'Ustawienia arkusza', edit_story_group_activity_group_path(@story_group, @activity_group), + = link_to t('.sheet_settings'), edit_story_group_activity_group_path(@story_group, @activity_group), class: 'gh-btn' - else %section.gh-panel.gh-empty-state - %h2.gh-h2 W tej grupie nie ma jeszcze studentów - %p.gh-lead - Zaproś studentów do grupy — arkusz czeka gotowy i nic w nim nie przepada. + %h2.gh-h2= t('.no_students_title') + %p.gh-lead= t('.no_students_body') .gh-button-row - = link_to 'Zaproszenia', story_group_invites_path(@story_group), class: 'gh-btn' + = link_to t('.invites'), story_group_invites_path(@story_group), class: 'gh-btn' .gh-grading-mobile-fallback %section.gh-panel.gh-grading-mobile-panel .gh-grading-mobile-icon.gh-octagon-clip %i.fa-solid.fa-desktop{ 'aria-hidden': 'true' } %h1.gh-h2= @activity_group.name - %p.gh-lead - Ocenianie w tabeli działa na komputerze. Ta tabela ma - = "#{categories.size} #{gh_plural(categories.size, 'kategorię', 'kategorie', 'kategorii')}" - i - = "#{students.size} #{gh_plural(students.size, 'studenta', 'studentów', 'studentów')}," - więc na telefonie byłaby nieczytelna. - %p.gh-small Na telefonie sprawdzisz ostatnie zakupy i powiadomienia. - = link_to 'Wróć na start', root_path, class: 'gh-btn gh-btn--secondary' + %p.gh-lead= t('.mobile_body', categories: t('.mobile_categories', count: categories.size), + students: t('.mobile_students', count: students.size)) + %p.gh-small= t('.mobile_hint') + = link_to t('.back_home'), root_path, class: 'gh-btn gh-btn--secondary' diff --git a/app/views/students_badges/_confirm_destroy.html.haml b/app/views/students_badges/_confirm_destroy.html.haml index 01b73249..f3eaea22 100644 --- a/app/views/students_badges/_confirm_destroy.html.haml +++ b/app/views/students_badges/_confirm_destroy.html.haml @@ -1,16 +1,16 @@ - badge = students_badge.badge -%h2#gh-revoke-title.gh-h2= "Odebrać odznakę „#{badge.name}”?" +%h2#gh-revoke-title.gh-h2= t('.title', name: badge.name) %p= badge_revoke_sentence(@student, badge) .gh-dialog-button-row - if @in_modal %button.gh-btn.gh-btn--secondary{ type: 'button', data: { action: 'dialog#close' }, autofocus: true } - Anuluj + = t('shared.actions.cancel') - else - = link_to 'Anuluj', story_group_student_path(@story_group, @student), class: 'gh-btn gh-btn--secondary' + = link_to t('shared.actions.cancel'), story_group_student_path(@story_group, @student), class: 'gh-btn gh-btn--secondary' - = button_to 'Odbierz odznakę', + = button_to t('.revoke'), story_group_student_badge_path(@story_group, @student, students_badge), method: :delete, class: 'gh-btn', form: { data: { turbo_frame: (@in_modal ? 'modal' : nil) } } diff --git a/app/views/students_badges/_form.html.haml b/app/views/students_badges/_form.html.haml index 2066309d..961619ef 100644 --- a/app/views/students_badges/_form.html.haml +++ b/app/views/students_badges/_form.html.haml @@ -5,16 +5,16 @@ method: :post, scope: :students_badge, data: { controller: 'badge-picker', turbo_frame: (@in_modal ? 'modal' : nil), - badge_picker_pick_value: 'Przyznaj', - badge_picker_blank_value: 'Wybierz odznakę', } do |f| + badge_picker_pick_value: t('.pick'), + badge_picker_blank_value: t('.blank'), } do |f| - %h2#gh-assign-title.gh-h2 Przyznaj odznakę + %h2#gh-assign-title.gh-h2= t('.title') %p= badge_assign_lead(@student, held, @badges.size) - if @badges.any? %label.gh-search-input.gh-search--full-width %i.fa-solid.fa-magnifying-glass{ 'aria-hidden': 'true' } - %input{ type: 'search', placeholder: 'Szukaj odznaki', 'aria-label': 'Szukaj odznaki', + %input{ type: 'search', placeholder: t('.search'), 'aria-label': t('.search'), data: { badge_picker_target: 'input', action: 'input->badge-picker#filter' } } %ul.gh-badge-picker-list @@ -38,12 +38,9 @@ badge_picker_for: badge.id } } = badge_effect_sentence(badge) - %p.gh-explainer-note{ hidden: true, data: { badge_picker_target: 'empty' } } - Brak odznak o tej nazwie. + %p.gh-explainer-note{ hidden: true, data: { badge_picker_target: 'empty' } }= t('.no_match') - else - %p.gh-explainer-note - W tej grupie nie ma jeszcze odznak. Dodaj je w zakładce Odznaki, a potem - wróć tutaj. + %p.gh-explainer-note= t('.empty') - if error %p.gh-field-error{ role: 'alert' } @@ -52,9 +49,9 @@ .gh-dialog-button-row - if @in_modal - %button.gh-btn.gh-btn--secondary{ type: 'button', data: { action: 'dialog#close' } } Anuluj + %button.gh-btn.gh-btn--secondary{ type: 'button', data: { action: 'dialog#close' } }= t('shared.actions.cancel') - else - = link_to 'Anuluj', story_group_student_path(@story_group, @student), class: 'gh-btn gh-btn--secondary' + = link_to t('shared.actions.cancel'), story_group_student_path(@story_group, @student), class: 'gh-btn gh-btn--secondary' %button.gh-btn{ type: 'submit', disabled: @badges.empty?, data: { badge_picker_target: 'submit' } } - Wybierz odznakę + = t('.submit') diff --git a/app/views/students_badges/confirm_destroy.html.haml b/app/views/students_badges/confirm_destroy.html.haml index bf0792f9..ac2a0faa 100644 --- a/app/views/students_badges/confirm_destroy.html.haml +++ b/app/views/students_badges/confirm_destroy.html.haml @@ -1,4 +1,4 @@ -- content_for :title, "Odbierz odznakę #{@students_badge.name}" +- content_for :title, t('.page_title', name: @students_badge.name) - if @in_modal = turbo_frame_tag 'modal' do diff --git a/app/views/students_badges/new.html.haml b/app/views/students_badges/new.html.haml index ddc13389..6b6a19df 100644 --- a/app/views/students_badges/new.html.haml +++ b/app/views/students_badges/new.html.haml @@ -1,4 +1,4 @@ -- content_for :title, "Przyznaj odznakę - #{@student.display_name}" +- content_for :title, t('.page_title', name: @student.display_name) - if @in_modal = turbo_frame_tag 'modal' do diff --git a/app/views/students_items/_card.html.haml b/app/views/students_items/_card.html.haml index 7c67a249..fecc4f0a 100644 --- a/app/views/students_items/_card.html.haml +++ b/app/views/students_items/_card.html.haml @@ -16,7 +16,7 @@ - if item.deleted? .gh-card-tags - %span.gh-tag.gh-tag--removed Usunięty z oferty + %span.gh-tag.gh-tag--removed= t('.removed') %p.gh-rule-text= item.didactic_description - if item.story_description.present? diff --git a/app/views/students_items/index.html.haml b/app/views/students_items/index.html.haml index cc32d236..dd1bd048 100644 --- a/app/views/students_items/index.html.haml +++ b/app/views/students_items/index.html.haml @@ -1,4 +1,4 @@ -- title = @own_items ? 'Moje przedmioty' : "Przedmioty — #{@student.display_name}" +- title = @own_items ? t('.own_title') : t('.title', name: @student.display_name) - content_for :title, title - count = @purchases.size @@ -9,14 +9,13 @@ - if @own_items = inventory_lead(count) - else - = "#{count} #{gh_plural(count, 'przedmiot', 'przedmioty', 'przedmiotów')}." - Wykorzystanie przedmiotu student rozlicza z Tobą na zajęciach. + = t('.teacher_lead', count: count) - if @own_items .gh-button-row = link_to story_group_shop_index_path(@story_group), class: 'gh-btn gh-btn--secondary' do %i.fa-solid.fa-bag-shopping{ 'aria-hidden': 'true' } - Przejdź do sklepu + = t('.go_to_shop') - if @purchases.any? .gh-inventory-grid @@ -27,17 +26,14 @@ - if @shop .gh-panel.gh-empty-slot-panel %p= inventory_shop_line(@shop.affordable) - = link_to 'Zobacz sklep', story_group_shop_index_path(@story_group), class: 'gh-btn gh-btn--secondary' + = link_to t('.see_shop'), story_group_shop_index_path(@story_group), class: 'gh-btn gh-btn--secondary' - else %section.gh-panel.gh-empty-state - if @own_items - %h2.gh-h2 Nie masz jeszcze żadnego przedmiotu - %p.gh-lead - Zbieraj walutę na zajęciach i wydaj ją w sklepie — kupione przedmioty - zostaną tutaj razem z datą zakupu. + %h2.gh-h2= t('.own_empty_title') + %p.gh-lead= t('.own_empty_body') .gh-button-row - = link_to 'Przejdź do sklepu', story_group_shop_index_path(@story_group), class: 'gh-btn' + = link_to t('.go_to_shop'), story_group_shop_index_path(@story_group), class: 'gh-btn' - else - %h2.gh-h2 Student nic jeszcze nie kupił - %p.gh-lead - Przedmioty pojawią się tu razem z ceną, jaką zapłacił, i datą zakupu. + %h2.gh-h2= t('.empty_title') + %p.gh-lead= t('.empty_body') diff --git a/app/views/teachers/_confirm_destroy.html.haml b/app/views/teachers/_confirm_destroy.html.haml index 28240657..c0455b09 100644 --- a/app/views/teachers/_confirm_destroy.html.haml +++ b/app/views/teachers/_confirm_destroy.html.haml @@ -1,14 +1,12 @@ -- name = teacher.full_name - -%h2#gh-del-title.gh-h2= "Usunąć #{name} z grupy?" -%p Straci dostęp do grupy. Nagrody, które przyznał, zostają u studentów. +%h2#gh-del-title.gh-h2= t('.title', name: teacher.full_name) +%p= t('.body') .gh-dialog-button-row - if @in_modal - %button.gh-btn.gh-btn--secondary{ type: 'button', data: { action: 'dialog#close' }, autofocus: true } Anuluj + %button.gh-btn.gh-btn--secondary{ type: 'button', data: { action: 'dialog#close' }, autofocus: true }= t('shared.actions.cancel') - else - = link_to 'Anuluj', story_group_teachers_path(@story_group), class: 'gh-btn gh-btn--secondary' + = link_to t('shared.actions.cancel'), story_group_teachers_path(@story_group), class: 'gh-btn gh-btn--secondary' - = button_to 'Usuń z grupy', story_group_teacher_path(@story_group, teacher), + = button_to t('.remove'), story_group_teacher_path(@story_group, teacher), method: :delete, class: 'gh-btn', form: { data: { turbo_frame: (@in_modal ? 'modal' : nil) } } diff --git a/app/views/teachers/_form.html.haml b/app/views/teachers/_form.html.haml index 38303d3b..423ac565 100644 --- a/app/views/teachers/_form.html.haml +++ b/app/views/teachers/_form.html.haml @@ -5,20 +5,17 @@ turbo_frame: (@in_modal ? 'modal' : nil), teacher_picker_min_value: 2, teacher_picker_max_value: 8, - teacher_picker_one_value: 'wynik', - teacher_picker_few_value: 'wyniki', - teacher_picker_many_value: 'wyników', - teacher_picker_overflow_value: ', pokazujemy 8 pierwszych', } do + teacher_picker_i18n_value: t('.js').to_json, } do - %h2#gh-add-teacher-title.gh-h2 Dodaj nauczyciela - %p Nauczyciel dostanie dostęp do grupy od razu po dodaniu. + %h2#gh-add-teacher-title.gh-h2= t('.title') + %p= t('.hint') .gh-teacher-picker - if pool.addable? %label.gh-search-input.gh-search--full-width %i.fa-solid.fa-magnifying-glass{ 'aria-hidden': 'true' } - %input{ type: 'search', placeholder: 'Imię, nazwisko albo e-mail', - 'aria-label': 'Szukaj nauczyciela', autocomplete: 'off', autofocus: true, + %input{ type: 'search', placeholder: t('.placeholder'), + 'aria-label': t('.search'), autocomplete: 'off', autofocus: true, data: { teacher_picker_target: 'input', action: 'input->teacher-picker#filter keydown.enter->teacher-picker#noSubmit', } } @@ -39,26 +36,23 @@ %small= teacher_candidate_subtitle(candidate, cross_university: pool.cross_university?) - if candidate.in_group? - %span.gh-badge-picker-status Już w grupie + %span.gh-badge-picker-status= t('.in_group') - else %button.gh-btn.gh-btn--secondary.gh-btn--small{ type: 'submit', name: 'story_group_teacher[user_id]', value: candidate.person.id, - 'aria-label': "Dodaj #{candidate.name}" } + 'aria-label': t('.add_label', name: candidate.name) } %i.fa-solid.fa-plus{ 'aria-hidden': 'true' } - Dodaj + = t('shared.actions.add') - %p.gh-small{ hidden: true, data: { teacher_picker_target: 'overflow' } } - Nie widzisz tej osoby? Wpisz więcej liter albo e-mail. + %p.gh-small{ hidden: true, data: { teacher_picker_target: 'overflow' } }= t('.overflow_hint') %p.gh-explainer-note{ hidden: true, data: { teacher_picker_target: 'empty' } } - Brak wyników dla + = t('.no_results') %span{ data: { teacher_picker_target: 'emptyQuery' } } - Sprawdź pisownię albo wpisz e-mail. + = t('.no_results_hint') - else - %p.gh-explainer-note - Nie ma kogo dodać: w Twojej uczelni nie ma jeszcze innych kont - nauczycieli. + %p.gh-explainer-note= t('.nobody') - if error %p.gh-field-error{ role: 'alert' } @@ -67,6 +61,6 @@ .gh-dialog-button-row - if @in_modal - %button.gh-btn.gh-btn--secondary{ type: 'button', data: { action: 'dialog#close' } } Zamknij + %button.gh-btn.gh-btn--secondary{ type: 'button', data: { action: 'dialog#close' } }= t('shared.actions.close') - else - = link_to 'Zamknij', story_group_teachers_path(@story_group), class: 'gh-btn gh-btn--secondary' + = link_to t('shared.actions.close'), story_group_teachers_path(@story_group), class: 'gh-btn gh-btn--secondary' diff --git a/app/views/teachers/_row.html.haml b/app/views/teachers/_row.html.haml index f2685b60..c2126b64 100644 --- a/app/views/teachers/_row.html.haml +++ b/app/views/teachers/_row.html.haml @@ -11,20 +11,20 @@ %span.gh-teacher-role - if row.owner? - Właściciel - %small Utworzył grupę + = t('.owner') + %small= t('.created_group') - else - Nauczyciel wspomagający + = t('.assistant') %span - %span.gh-mobile-field-label Dodano + %span.gh-mobile-field-label= t('.added') %span.gh-small= teacher_added_on(row.added_at) %span.gh-teacher-row-actions - if manage && row.removable? = link_to confirm_destroy_story_group_teacher_path(@story_group, row.membership), class: 'gh-btn gh-btn--secondary gh-btn--small', - 'aria-label': "Usuń z grupy: #{name}", + 'aria-label': t('.remove_label', name: name), data: { turbo_frame: 'modal' } do %i.fa-solid.fa-trash{ 'aria-hidden': 'true' } - Usuń + = t('shared.actions.remove') diff --git a/app/views/teachers/confirm_destroy.html.haml b/app/views/teachers/confirm_destroy.html.haml index 3d275f04..45db430c 100644 --- a/app/views/teachers/confirm_destroy.html.haml +++ b/app/views/teachers/confirm_destroy.html.haml @@ -1,4 +1,4 @@ -- content_for :title, "Usunąć #{@teacher.full_name} z grupy?" +- content_for :title, t('.page_title', name: @teacher.full_name) - if @in_modal = turbo_frame_tag 'modal' do diff --git a/app/views/teachers/index.html.haml b/app/views/teachers/index.html.haml index 67ed014e..49b90966 100644 --- a/app/views/teachers/index.html.haml +++ b/app/views/teachers/index.html.haml @@ -1,23 +1,23 @@ -- content_for :title, 'Nauczyciele' +- content_for :title, t('.title') - manage = policy(@story_group).manage_teachers? %section.gh-panel.gh-page-header .gh-page-header-text - %h1.gh-h1 Nauczyciele + %h1.gh-h1= t('.title') %p.gh-lead= teacher_list_lead(manage: manage) - if manage = link_to new_story_group_teacher_path(@story_group), class: 'gh-btn', data: { turbo_frame: 'modal' } do %i.fa-solid.fa-plus{ 'aria-hidden': 'true' } - Dodaj nauczyciela + = t('.add') %section.gh-panel.gh-teachers-panel .gh-teacher-row.gh-teacher-row--header{ 'aria-hidden': 'true' } - %span Nauczyciel - %span Rola - %span Dodano + %span= t('.teacher') + %span= t('.role') + %span= t('.added') %span = render partial: 'row', collection: @list.rows, as: :row, locals: { manage: manage } diff --git a/app/views/teachers/new.html.haml b/app/views/teachers/new.html.haml index 705e1d18..89f43c23 100644 --- a/app/views/teachers/new.html.haml +++ b/app/views/teachers/new.html.haml @@ -1,4 +1,4 @@ -- content_for :title, 'Dodaj nauczyciela' +- content_for :title, t('.page_title') - if @in_modal = turbo_frame_tag 'modal' do diff --git a/config/application.rb b/config/application.rb index 37779237..82c53ca5 100644 --- a/config/application.rb +++ b/config/application.rb @@ -30,5 +30,9 @@ class Application < Rails::Application config.generators.system_tests = nil config.action_controller.include_all_helpers = false + + config.i18n.available_locales = %i[pl en] + config.i18n.default_locale = :pl + config.i18n.load_path += Dir[Rails.root.join('config/locales/**/*.yml')] end end diff --git a/config/environments/development.rb b/config/environments/development.rb index ed7024c8..d0e02491 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -71,7 +71,7 @@ config.assets.quiet = true # Raises error for missing translations. - # config.i18n.raise_on_missing_translations = true + config.i18n.raise_on_missing_translations = true # Annotate rendered view with file names. config.action_view.annotate_rendered_view_with_filenames = true diff --git a/config/environments/test.rb b/config/environments/test.rb index 37fc1f7b..7effe50a 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -45,7 +45,7 @@ config.active_support.deprecation = :stderr # Raises error for missing translations. - # config.i18n.raise_on_missing_translations = true + config.i18n.raise_on_missing_translations = true # Annotate rendered view with file names. # config.action_view.annotate_rendered_view_with_filenames = true diff --git a/config/locales/views/activity_group_templates.en.yml b/config/locales/views/activity_group_templates.en.yml new file mode 100644 index 00000000..cf0aa194 --- /dev/null +++ b/config/locales/views/activity_group_templates.en.yml @@ -0,0 +1,40 @@ +en: + activity_group_templates: + flash: + created: "Created the template “%{name}”." + updated: "Saved the template. New sheets will get these categories." + destroyed: "Deleted the template “%{name}”. Sheets created from it stay." + # The first category a new template starts with. + default_category: "Attendance" + + new: + page_title: "New sheet template" + + edit: + page_title: "Edit template %{name}" + + confirm_destroy: + page_title: "Delete template %{name}" + title: "Delete the template “%{name}”?" + body: "The template will disappear from the list, and sheets created from it stay." + sheets: + one: "%{count} sheet stays unchanged: %{names}" + other: "%{count} sheets stay unchanged: %{names}" + remove: "Delete template" + + activerecord: + errors: + models: + activity_group_template: + attributes: + base_name: + blank: "Enter the template name." + base: + no_category: "Add at least one visible category." + # RewardCategory — a category of a template. + activity_group_template_category: + attributes: + didactic_description: + blank: "Say what the reward is for." + reward: + too_low: "The reward must be at least 1." diff --git a/config/locales/views/activity_group_templates.pl.yml b/config/locales/views/activity_group_templates.pl.yml new file mode 100644 index 00000000..3c40ea66 --- /dev/null +++ b/config/locales/views/activity_group_templates.pl.yml @@ -0,0 +1,42 @@ +pl: + activity_group_templates: + flash: + created: "Utworzono szablon „%{name}”." + updated: "Zapisano szablon. Nowe arkusze dostaną te kategorie." + destroyed: "Usunięto szablon „%{name}”. Utworzone z niego arkusze zostają." + # The first category a new template starts with. + default_category: "Obecność" + + new: + page_title: "Nowy szablon arkusza" + + edit: + page_title: "Edytuj szablon %{name}" + + confirm_destroy: + page_title: "Usuń szablon %{name}" + title: "Usunąć szablon „%{name}”?" + body: "Szablon zniknie z listy, a utworzone z niego arkusze zostają." + sheets: + one: "%{count} arkusz zostaje bez zmian: %{names}" + few: "%{count} arkusze zostają bez zmian: %{names}" + many: "%{count} arkuszy zostaje bez zmian: %{names}" + other: "%{count} arkusza zostaje bez zmian: %{names}" + remove: "Usuń szablon" + + activerecord: + errors: + models: + activity_group_template: + attributes: + base_name: + blank: "Podaj nazwę szablonu." + base: + no_category: "Dodaj co najmniej jedną widoczną kategorię." + # RewardCategory — a category of a template. + activity_group_template_category: + attributes: + didactic_description: + blank: "Podaj, za co jest nagroda." + reward: + too_low: "Nagroda musi wynosić co najmniej 1." diff --git a/config/locales/views/activity_groups.en.yml b/config/locales/views/activity_groups.en.yml new file mode 100644 index 00000000..26302086 --- /dev/null +++ b/config/locales/views/activity_groups.en.yml @@ -0,0 +1,84 @@ +en: + activity_groups: + flash: + created_one: "Created: %{name}." + created_many: "Created: %{first} – %{last}." + updated: "Saved the settings of the sheet %{name}." + destroyed: "Deleted the sheet %{name}. Awarded rewards stay in the students' history." + + index: + title: "Grade sheets" + lead: "Sheets grouped by template. The newest are at the top." + new_template: "New template" + empty_title: "You don't have any templates yet" + empty_body: "A template is a set of categories you reward students for. From one template you then create sheets — one per class." + + template_panel: + meta: + one: "Template: %{count} category, up to %{max} %{currency} per student per sheet." + other: "Template: %{count} categories, up to %{max} %{currency} per student per sheet." + edit: "Edit template" + create_sheet: "Create a sheet" + no_sheets: "No sheets have been created from this template yet." + + sheet_row: + modified: "Columns changed" + awarded: + one: "Awarded %{count} reward" + other: "Awarded %{count} rewards" + nothing: "Nothing awarded yet" + grade: "Grade" + settings: "Sheet settings" + settings_label: "Settings: %{name}" + + new: + page_title: "New sheet from the template %{name}" + + new_form: + title: "New sheet from the template %{name}" + columns: + one: "Each sheet gets %{count} category from the template." + other: "Each sheet gets %{count} categories from the template." + columns_note: "You'll change the columns later in the sheet settings." + how_many: "How many sheets" + one: "One sheet" + many: "Several at once" + name_hint: "You can add a date, e.g. %{example} (14.10)." + count: "How many sheets to create" + names: "Names" + # sheet_create_controller.ts + js: + create_one: "Create sheet" + create_many: + one: "Create %{count} sheet" + other: "Create %{count} sheets" + + edit: + page_title: "Sheet settings %{name}" + + confirm_destroy: + page_title: "Delete sheet %{name}" + title: "Delete the sheet “%{name}”?" + body: "The sheet will disappear from the list, and awarded rewards stay with the students and in their history." + awarded: + one: "%{count} reward was awarded from it." + other: "%{count} rewards were awarded from it." + nothing_undone: "Deleting the sheet doesn't undo anything." + remove: "Delete sheet" + + activerecord: + errors: + models: + activity_group: + attributes: + name: + blank: "Enter the sheet name." + base: + no_visible_category: "Add at least one visible category." + # RewardCategory — a column of a sheet. + activity_group_category: + attributes: + didactic_description: + blank: "Say what the reward is for." + reward: + too_low: "The reward must be at least 1." diff --git a/config/locales/views/activity_groups.pl.yml b/config/locales/views/activity_groups.pl.yml new file mode 100644 index 00000000..ea529902 --- /dev/null +++ b/config/locales/views/activity_groups.pl.yml @@ -0,0 +1,94 @@ +pl: + activity_groups: + flash: + created_one: "Utworzono: %{name}." + created_many: "Utworzono: %{first} – %{last}." + updated: "Zapisano ustawienia arkusza %{name}." + destroyed: "Usunięto arkusz %{name}. Przyznane nagrody zostają w historii studentów." + + index: + title: "Arkusze ocen" + lead: "Arkusze pogrupowane według szablonów. Najnowsze są na górze." + new_template: "Nowy szablon" + empty_title: "Nie masz jeszcze żadnego szablonu" + empty_body: "Szablon to zestaw kategorii, za które nagradzasz studentów. Z jednego szablonu tworzysz potem kolejne arkusze — po jednym na zajęcia." + + template_panel: + meta: + one: "Szablon: %{count} kategoria, do %{max} %{currency} na studenta za arkusz." + few: "Szablon: %{count} kategorie, do %{max} %{currency} na studenta za arkusz." + many: "Szablon: %{count} kategorii, do %{max} %{currency} na studenta za arkusz." + other: "Szablon: %{count} kategorii, do %{max} %{currency} na studenta za arkusz." + edit: "Edytuj szablon" + create_sheet: "Utwórz arkusz" + no_sheets: "Z tego szablonu nie utworzono jeszcze arkuszy." + + sheet_row: + modified: "Zmienione kolumny" + awarded: + one: "Przyznano %{count} nagrodę" + few: "Przyznano %{count} nagrody" + many: "Przyznano %{count} nagród" + other: "Przyznano %{count} nagrody" + nothing: "Jeszcze nic nie przyznano" + grade: "Oceń" + settings: "Ustawienia arkusza" + settings_label: "Ustawienia: %{name}" + + new: + page_title: "Nowy arkusz z szablonu %{name}" + + new_form: + title: "Nowy arkusz z szablonu %{name}" + columns: + one: "Każdy arkusz dostanie %{count} kategorię z szablonu." + few: "Każdy arkusz dostanie %{count} kategorie z szablonu." + many: "Każdy arkusz dostanie %{count} kategorii z szablonu." + other: "Każdy arkusz dostanie %{count} kategorii z szablonu." + columns_note: "Kolumny zmienisz później w ustawieniach arkusza." + how_many: "Ile arkuszy" + one: "Jeden arkusz" + many: "Kilka naraz" + name_hint: "Możesz dopisać datę, np. %{example} (14.10)." + count: "Ile arkuszy utworzyć" + names: "Nazwy" + # sheet_create_controller.ts + js: + create_one: "Utwórz arkusz" + create_many: + one: "Utwórz %{count} arkusz" + few: "Utwórz %{count} arkusze" + many: "Utwórz %{count} arkuszy" + other: "Utwórz %{count} arkusza" + + edit: + page_title: "Ustawienia arkusza %{name}" + + confirm_destroy: + page_title: "Usuń arkusz %{name}" + title: "Usunąć arkusz „%{name}”?" + body: "Arkusz zniknie z listy, a przyznane nagrody zostają u studentów i w ich historii." + awarded: + one: "Przyznano z niego %{count} nagrodę." + few: "Przyznano z niego %{count} nagrody." + many: "Przyznano z niego %{count} nagród." + other: "Przyznano z niego %{count} nagrody." + nothing_undone: "Usunięcie arkusza niczego nie cofa." + remove: "Usuń arkusz" + + activerecord: + errors: + models: + activity_group: + attributes: + name: + blank: "Podaj nazwę arkusza." + base: + no_visible_category: "Dodaj co najmniej jedną widoczną kategorię." + # RewardCategory — a column of a sheet. + activity_group_category: + attributes: + didactic_description: + blank: "Podaj, za co jest nagroda." + reward: + too_low: "Nagroda musi wynosić co najmniej 1." diff --git a/config/locales/views/application.en.yml b/config/locales/views/application.en.yml new file mode 100644 index 00000000..7ba07fc0 --- /dev/null +++ b/config/locales/views/application.en.yml @@ -0,0 +1,79 @@ +en: + # ApplicationHelper and ApplicationController concerns — wording shared by many screens. + application: + flash: + log_in_first: "Log in to continue." + already_signed_in: "You're already logged in." + not_found: "Not found." + helpers: + today: "today %{time}" + yesterday: "yesterday %{time}" + minutes: + one: "%{count} minute" + other: "%{count} minutes" + feed: + purchase: "Purchase: %{name}" + purchase_fallback: "Purchase" + reward_fallback: "Activity reward" + adjustment: "Currency adjustment" + transaction: + purchase: "%{student} bought %{item} for %{amount} %{currency}" + item_fallback: "an item" + reward: "%{student} earned a reward for: %{category}" + category_fallback: "activity" + adjustment: "%{adjuster} adjusted %{student}'s currency (%{amount})" + adjuster_fallback: "the teacher" + # PresetSet.label_for — names of the ready-made images, read out by screen readers. + presets: + glyphs: + bolt: "Lightning bolt" + carrot: "Carrot" + carrotStar: "Carrot with a star" + chat: "Conversation" + chev1: "Chevron" + chev2: "Double chevron" + clock: "Clock" + compass: "Compass" + crew: "Crew" + crown: "Crown" + flask: "Flask" + heartPlus: "Heart with a plus" + hourglass: "Hourglass" + note: "Envelope" + paperCheck: "Sheet with a tick" + papers3: "Three sheets" + papers3shield: "Three sheets with a shield" + percent: "Percent" + rabbit: "Rabbit" + retake: "Sheet with an arrow" + rocket: "Rocket" + shield: "Shield" + starPlus: "Star with a sparkle" + starTrail: "Shooting star" + wrench: "Wrench" + art: + brackets: "Code" + castle: "Castle" + flow: "Algorithm" + tables: "Tables" + waves: "Sea" + currency: + bit: "Bit" + carrot: "Carrot" + coin: "Coin" + gem: "Gem" + pearl: "Pearl" + + # Iconable — shared by every model with a picture. + activerecord: + errors: + messages: + unknown_art: "Unknown image." + unknown_icon: "Unknown icon." + art_type: "The image must be a GIF, JPG or PNG file." + art_missing: "Choose a ready-made image or upload your own." + + time: + formats: + gh_time: "%H:%M" + gh_day_time: "%b %-d, %H:%M" diff --git a/config/locales/views/application.pl.yml b/config/locales/views/application.pl.yml new file mode 100644 index 00000000..52763027 --- /dev/null +++ b/config/locales/views/application.pl.yml @@ -0,0 +1,81 @@ +pl: + # ApplicationHelper and ApplicationController concerns — wording shared by many screens. + application: + flash: + log_in_first: "Zaloguj się, aby kontynuować." + already_signed_in: "Jesteś już zalogowany." + not_found: "Nie znaleziono." + helpers: + today: "dziś %{time}" + yesterday: "wczoraj %{time}" + minutes: + one: "%{count} minuta" + few: "%{count} minuty" + many: "%{count} minut" + other: "%{count} minuty" + feed: + purchase: "Zakup: %{name}" + purchase_fallback: "Zakup" + reward_fallback: "Nagroda za aktywność" + adjustment: "Korekta waluty" + transaction: + purchase: "%{student} zakupił %{item} za %{amount} %{currency}" + item_fallback: "przedmiot" + reward: "%{student} zdobył nagrodę za: %{category}" + category_fallback: "aktywność" + adjustment: "%{adjuster} skorygował walutę %{student} (%{amount})" + adjuster_fallback: "nauczyciel" + # PresetSet.label_for — names of the ready-made images, read out by screen readers. + presets: + glyphs: + bolt: "Błyskawica" + carrot: "Marchewka" + carrotStar: "Marchewka z gwiazdą" + chat: "Rozmowa" + chev1: "Belka" + chev2: "Podwójna belka" + clock: "Zegar" + compass: "Kompas" + crew: "Załoga" + crown: "Korona" + flask: "Kolba" + heartPlus: "Serce z plusem" + hourglass: "Klepsydra" + note: "Koperta" + paperCheck: "Kartka z ptaszkiem" + papers3: "Trzy kartki" + papers3shield: "Trzy kartki z tarczą" + percent: "Procent" + rabbit: "Królik" + retake: "Kartka ze strzałką" + rocket: "Rakieta" + shield: "Tarcza" + starPlus: "Gwiazda z gwiazdką" + starTrail: "Gwiazda ze smugą" + wrench: "Klucz" + art: + brackets: "Kod" + castle: "Zamek" + flow: "Algorytm" + tables: "Tabele" + waves: "Morze" + currency: + bit: "Bit" + carrot: "Marchewka" + coin: "Moneta" + gem: "Klejnot" + pearl: "Perła" + + # Iconable — shared by every model with a picture. + activerecord: + errors: + messages: + unknown_art: "Nieznana grafika." + unknown_icon: "Nieznana ikona." + art_type: "Grafika musi być plikiem GIF, JPG lub PNG." + art_missing: "Wybierz gotową grafikę albo wgraj własną." + + time: + formats: + gh_time: "%H:%M" + gh_day_time: "%d.%m, %H:%M" diff --git a/config/locales/views/auth.en.yml b/config/locales/views/auth.en.yml new file mode 100644 index 00000000..24b201e0 --- /dev/null +++ b/config/locales/views/auth.en.yml @@ -0,0 +1,27 @@ +en: + # Auth::PasswordlessController, Auth::UsosController and Auth::SignIn. + auth: + flash: + signed_in: "Logged in successfully." + invalid_token: "Invalid token" + resent: "We've sent a new link. The previous one no longer works." + invalid_provider: "Invalid login provider." + invalid_auth: "Invalid authorization attempt." + + passwordless: + new: + title: "Log in with e-mail" + back: "Other ways to log in" + lead: "We'll send you a login link. No password needed." + email: "E-mail address" + placeholder: "john.smith@example.com" + send: "Send link" + inbox: + title: "Check your inbox" + sent_to: "We've sent a link to" + valid_for: "The link works for %{minutes}." + spam: "If you can't see it, check your Spam folder." + # %{time} is filled in by countdown_controller.ts. + waiting: "Resend in %{time}" + resend: "Resend the link" + change_email: "Change e-mail address" diff --git a/config/locales/views/auth.pl.yml b/config/locales/views/auth.pl.yml new file mode 100644 index 00000000..9bba17d8 --- /dev/null +++ b/config/locales/views/auth.pl.yml @@ -0,0 +1,27 @@ +pl: + # Auth::PasswordlessController, Auth::UsosController and Auth::SignIn. + auth: + flash: + signed_in: "Zalogowano pomyślnie." + invalid_token: "Nieprawidłowy token" + resent: "Wysłaliśmy nowy link. Poprzedni już nie działa." + invalid_provider: "Nieprawidłowy dostawca logowania." + invalid_auth: "Nieprawidłowa próba autoryzacji." + + passwordless: + new: + title: "Logowanie e-mailem" + back: "Inne sposoby logowania" + lead: "Wyślemy Ci link do logowania. Hasło nie jest potrzebne." + email: "Adres e-mail" + placeholder: "jan.nowak@example.com" + send: "Wyślij link" + inbox: + title: "Sprawdź skrzynkę" + sent_to: "Wysłaliśmy link na adres" + valid_for: "Link działa przez %{minutes}." + spam: "Jeśli go nie widzisz, zajrzyj do folderu Spam." + # %{time} is filled in by countdown_controller.ts. + waiting: "Wyślij ponownie za %{time}" + resend: "Wyślij link ponownie" + change_email: "Zmień adres e-mail" diff --git a/config/locales/views/badges.en.yml b/config/locales/views/badges.en.yml new file mode 100644 index 00000000..31ebca9b --- /dev/null +++ b/config/locales/views/badges.en.yml @@ -0,0 +1,110 @@ +en: + badges: + flash: + created: "Added the “%{name}” badge." + updated: "Saved the “%{name}” badge." + destroyed: "Deleted the “%{name}” badge. Students who have it keep it in their history." + + helpers: + nobody_holds: "Nobody has it yet" + holders: + one: "%{count} student has it" + other: "%{count} students have it" + discount: "−%{discount}% in the shop" + rule_placeholder: "How to earn it…" + name_placeholder: "Badge name" + + index: + title: "Badges" + + new: + page_title: "New badge" + + edit: + page_title: "Edit badge - %{name}" + + teacher_grid: + title: "Badges" + lead: + one: "%{count} badge. You award them by hand, from this list or from the student list." + other: "%{count} badges. You award them by hand, from this list or from the student list." + new: "New badge" + award: "Award" + edit_label: "Edit badge %{name}" + empty_title: "There are no badges yet" + empty_body: "Add badges to reward what points don't show. You award them by hand, and each can give a discount in the shop." + + student_deck: + title: "Badges" + lead: "You have %{earned} of %{total}. Badges are awarded by the teacher. The back of each one you don't have yet tells you how to earn it." + empty_title: "This group has no badges yet" + empty_body: "The teacher hasn't added any. Once they do, you'll see here what it takes to earn them." + + flip_card: + label: "Badge %{name}" + label_unearned: "Badge %{name}, not earned yet" + + front: + removed: "Removed from the list" + flip_label: "Show how to earn the %{name} badge" + flip: "How to earn it" + + back: + how: "How to earn it" + flip_label: "Show the %{name} badge" + flip: "Show badge" + + preview: + summary: "Card preview" + students_view: "This is what students will see" + side: "Card side" + earned: "Earned" + unearned: "Not earned yet" + picker: "In the award dialog" + + form: + breadcrumb: "Badges" + new_title: "New badge" + edit_title: "Edit badge" + lead: "The preview shows both sides of the card: earned and not earned yet." + holders: + one: "%{count} student has this badge." + other: "%{count} students have this badge." + holders_note: "A discount change applies to their next purchases right away." + card: "Badge card" + card_hint: "This is how the badge looks in the student's deck and in the award dialog." + name_placeholder: "e.g. Flawless Flight" + rule: "How to earn it" + rule_placeholder: "e.g. At least 3 quizzes in a row without mistakes" + rule_hint: "Students see this on the back of a badge they don't have yet. Be specific." + story: "Story description" + story_placeholder: "e.g. A flight without a single scratch" + discount_section: "Shop discount" + discount_hint: "Only applies to items that count it. You set that in the item form." + discount: "Shop discount" + used_by: "Counted by" + unused: "No item counts it yet." + removal_hint: "The badge will disappear from the list and from the award dialog. Students who have it keep it in their history." + remove: "Delete badge" + create: "Add badge" + + confirm_destroy: + page_title: "Delete badge %{name}" + title: "Delete the “%{name}” badge?" + body: "Students who have this badge keep it in their history, but nobody new will get it." + unlocking: + one: "The item %{items} is unlocked by this badge, and after deleting it only those who already have the badge can buy it." + other: "The items %{items} are unlocked by this badge, and after deleting it only those who already have the badge can buy them." + remove: "Delete badge" + + activerecord: + errors: + models: + badge: + attributes: + name: + blank: "Enter the badge name." + didactic_description: + blank: "Describe how to earn this badge." + discount: + out_of_range: "The discount must be between 0 and 100%." diff --git a/config/locales/views/badges.pl.yml b/config/locales/views/badges.pl.yml new file mode 100644 index 00000000..5c56cae9 --- /dev/null +++ b/config/locales/views/badges.pl.yml @@ -0,0 +1,116 @@ +pl: + badges: + flash: + created: "Dodano odznakę „%{name}”." + updated: "Zapisano odznakę „%{name}”." + destroyed: "Usunięto odznakę „%{name}”. Studenci, którzy ją mają, zachowują ją w historii." + + helpers: + nobody_holds: "Nikt jej jeszcze nie ma" + holders: + one: "Ma ją %{count} student" + few: "Ma ją %{count} studentów" + many: "Ma ją %{count} studentów" + other: "Ma ją %{count} studenta" + discount: "−%{discount}% w sklepie" + rule_placeholder: "Jak zdobyć…" + name_placeholder: "Nazwa odznaki" + + index: + title: "Odznaki" + + new: + page_title: "Nowa odznaka" + + edit: + page_title: "Edytuj odznakę - %{name}" + + teacher_grid: + title: "Odznaki" + lead: + one: "%{count} odznaka. Przyznajesz je ręcznie, z tej listy albo z listy studentów." + few: "%{count} odznaki. Przyznajesz je ręcznie, z tej listy albo z listy studentów." + many: "%{count} odznak. Przyznajesz je ręcznie, z tej listy albo z listy studentów." + other: "%{count} odznaki. Przyznajesz je ręcznie, z tej listy albo z listy studentów." + new: "Nowa odznaka" + award: "Przyznaj" + edit_label: "Edytuj odznakę %{name}" + empty_title: "Nie ma jeszcze żadnych odznak" + empty_body: "Dodaj odznaki, żeby mieć czym nagradzać to, czego nie widać w punktach. Przyznajesz je ręcznie, a każda może dawać zniżkę w sklepie." + + student_deck: + title: "Odznaki" + lead: "Masz %{earned} z %{total}. Odznaki przyznaje prowadzący. Na odwrocie każdej niezdobytej znajdziesz, jak ją zdobyć." + empty_title: "W tej grupie nie ma jeszcze odznak" + empty_body: "Prowadzący ich nie dodał. Kiedy się pojawią, zobaczysz tu, co trzeba zrobić, żeby je zdobyć." + + flip_card: + label: "Odznaka %{name}" + label_unearned: "Odznaka %{name}, jeszcze niezdobyta" + + front: + removed: "Usunięta z listy" + flip_label: "Pokaż, jak zdobyć odznakę %{name}" + flip: "Jak zdobyć" + + back: + how: "Jak zdobyć" + flip_label: "Pokaż odznakę %{name}" + flip: "Pokaż odznakę" + + preview: + summary: "Podgląd karty" + students_view: "Tak zobaczą to studenci" + side: "Strona karty" + earned: "Zdobyta" + unearned: "Jeszcze niezdobyta" + picker: "W oknie przyznawania" + + form: + breadcrumb: "Odznaki" + new_title: "Nowa odznaka" + edit_title: "Edytuj odznakę" + lead: "Podgląd obok pokazuje obie strony karty: zdobytą i jeszcze niezdobytą." + holders: + one: "%{count} student ma tę odznakę." + few: "%{count} studentów ma tę odznakę." + many: "%{count} studentów ma tę odznakę." + other: "%{count} studenta ma tę odznakę." + holders_note: "Zmiana zniżki od razu obejmie ich kolejne zakupy." + card: "Karta odznaki" + card_hint: "Tak odznaka wygląda w talii studenta i w oknie przyznawania." + name_placeholder: "np. Perfekcyjny Lot" + rule: "Jak zdobyć" + rule_placeholder: "np. Co najmniej 3 wejściówki z rzędu bez błędów" + rule_hint: "Studenci widzą to na odwrocie odznaki, której jeszcze nie mają. Pisz konkretnie." + story: "Opis fabularny" + story_placeholder: "np. Lot bez najmniejszej rysy" + discount_section: "Zniżka w sklepie" + discount_hint: "Działa tylko na przedmioty, które ją uwzględniają. Ustawiasz to w formularzu przedmiotu." + discount: "Zniżka w sklepie" + used_by: "Uwzględniają ją" + unused: "Na razie żaden przedmiot jej nie uwzględnia." + removal_hint: "Odznaka zniknie z listy i z okna przyznawania. Studenci, którzy ją mają, zachowają ją w historii." + remove: "Usuń odznakę" + create: "Dodaj odznakę" + + confirm_destroy: + page_title: "Usuń odznakę %{name}" + title: "Usunąć odznakę „%{name}”?" + body: "Studenci, którzy mają tę odznakę, zachowają ją w historii, ale nikt nowy jej nie dostanie." + unlocking: + one: "Przedmiot %{items} odblokowuje się tą odznaką i po usunięciu kupi go tylko ci, którzy już ją mają." + other: "Przedmioty %{items} odblokowują się tą odznaką i po usunięciu kupią je tylko ci, którzy już ją mają." + remove: "Usuń odznakę" + + activerecord: + errors: + models: + badge: + attributes: + name: + blank: "Podaj nazwę odznaki." + didactic_description: + blank: "Napisz, jak zdobyć tę odznakę." + discount: + out_of_range: "Zniżka musi mieścić się między 0 a 100%." diff --git a/config/locales/views/currency_adjustments.en.yml b/config/locales/views/currency_adjustments.en.yml new file mode 100644 index 00000000..c14e9d1e --- /dev/null +++ b/config/locales/views/currency_adjustments.en.yml @@ -0,0 +1,37 @@ +en: + currency_adjustments: + flash: + added: "Added %{value}. Balance: %{balance}." + taken: "Subtracted %{value}. Balance: %{balance}." + + new: + page_title: "Adjust currency - %{name}" + + form: + title: "Adjust currency" + lead: "%{name} now has %{balance} to spend." + kind_label: "Adjustment type" + take: "Subtract" + amount: "Amount" + balance: "To spend" + total: "Collected in total" + rank: "Rank" + unchanged: "unchanged" + total_unchanged: "%{total} (unchanged)" + blank: "Enter an amount" + over: "The student only has %{balance} to spend." + add_note: "Adding also raises the collected total, so it may raise the rank." + take_note: "Subtracting only lowers what the student has to spend. The collected total and the rank stay the same." + submit_add: "Add %{value}" + submit_take: "Subtract %{value}" + + # CurrencyAdjustmentForm#validate (errors.add(:amount, :key)). + activemodel: + errors: + models: + currency_adjustment_form: + attributes: + amount: + missing: "Enter an amount." + not_positive: "Enter an amount greater than zero." + overdrawn: "The student only has %{balance} to spend." diff --git a/config/locales/views/currency_adjustments.pl.yml b/config/locales/views/currency_adjustments.pl.yml new file mode 100644 index 00000000..fe283b64 --- /dev/null +++ b/config/locales/views/currency_adjustments.pl.yml @@ -0,0 +1,37 @@ +pl: + currency_adjustments: + flash: + added: "Dodano %{value}. Saldo: %{balance}." + taken: "Odjęto %{value}. Saldo: %{balance}." + + new: + page_title: "Koryguj walutę - %{name}" + + form: + title: "Koryguj walutę" + lead: "%{name} ma teraz %{balance} do wydania." + kind_label: "Rodzaj korekty" + take: "Odejmij" + amount: "Ile" + balance: "Do wydania" + total: "Zebrane łącznie" + rank: "Ranga" + unchanged: "bez zmian" + total_unchanged: "%{total} (bez zmian)" + blank: "Podaj kwotę" + over: "Student ma tylko %{balance} do wydania." + add_note: "Dodanie podnosi też sumę zebranych, więc może podnieść rangę." + take_note: "Odjęcie zmniejsza tylko to, co student ma do wydania. Suma zebranych i ranga zostają bez zmian." + submit_add: "Dodaj %{value}" + submit_take: "Odejmij %{value}" + + # CurrencyAdjustmentForm#validate (errors.add(:amount, :key)). + activemodel: + errors: + models: + currency_adjustment_form: + attributes: + amount: + missing: "Podaj kwotę." + not_positive: "Podaj kwotę większą od zera." + overdrawn: "Student ma tylko %{balance} do wydania." diff --git a/config/locales/views/currency_transactions.en.yml b/config/locales/views/currency_transactions.en.yml new file mode 100644 index 00000000..2d83c754 --- /dev/null +++ b/config/locales/views/currency_transactions.en.yml @@ -0,0 +1,38 @@ +en: + currency_transactions: + # CurrencyLedger::KINDS — the filter chips name a set of rows. + filters: + all: "All" + reward: "Rewards" + purchase: "Purchases" + adjustment: "Adjustments" + # LedgerEntry#label — the pill on a single row. + kinds: + reward: "Reward" + purchase: "Purchase" + adjustment: "Adjustment" + # CurrencyLedger#title_for, when the row has no name of its own. + entry_titles: + reward: "Reward" + adjustment: "Currency adjustment" + + helpers: + balance_sentence_html: "You have %{balance} to spend and %{total} collected in total." + summary_sentence_html: "Balance: %{balance}, collected in total: %{total}." + + ledger: + table_label: "Currency history" + columns: + amount: "Amount" + kind: "Type" + source: "For" + when: "When" + balance_after: "Balance after" + withdrawn: "Removed from the shop" + + index: + title: "Currency history" + go_to_shop: "Go to the shop" + empty_kind: "You have no entries of this type yet." + empty_title: "Your first entries will show up after class" + empty_body: "The teacher awards currency for attendance, punctuality and activity. Everything you earn and spend will be recorded here." diff --git a/config/locales/views/currency_transactions.pl.yml b/config/locales/views/currency_transactions.pl.yml new file mode 100644 index 00000000..30c1a192 --- /dev/null +++ b/config/locales/views/currency_transactions.pl.yml @@ -0,0 +1,38 @@ +pl: + currency_transactions: + # CurrencyLedger::KINDS — the filter chips name a set of rows. + filters: + all: "Wszystkie" + reward: "Nagrody" + purchase: "Zakupy" + adjustment: "Korekty" + # LedgerEntry#label — the pill on a single row. + kinds: + reward: "Nagroda" + purchase: "Zakup" + adjustment: "Korekta" + # CurrencyLedger#title_for, when the row has no name of its own. + entry_titles: + reward: "Nagroda" + adjustment: "Korekta waluty" + + helpers: + balance_sentence_html: "Masz %{balance} do wydania i %{total} zebrane łącznie." + summary_sentence_html: "Saldo: %{balance}, zebrane łącznie: %{total}." + + ledger: + table_label: "Historia waluty" + columns: + amount: "Kwota" + kind: "Typ" + source: "Za co" + when: "Kiedy" + balance_after: "Saldo po" + withdrawn: "Usunięty z oferty" + + index: + title: "Historia waluty" + go_to_shop: "Przejdź do sklepu" + empty_kind: "Nie masz jeszcze wpisów tego typu." + empty_title: "Pierwsze wpisy pojawią się po zajęciach" + empty_body: "Prowadzący przyznaje walutę za obecność, punktualność i aktywność. Wszystko, co zdobędziesz i wydasz, zapisze się tutaj." diff --git a/config/locales/views/home.en.yml b/config/locales/views/home.en.yml new file mode 100644 index 00000000..5de1c44b --- /dev/null +++ b/config/locales/views/home.en.yml @@ -0,0 +1,159 @@ +en: + home: + index: + title: "Home" + + # HomeHelper#home_greeting — the admin dashboards. + admin_greeting: "Good morning, %{name}" + admin_greeting_anonymous: "Good morning" + + capacity_bar: + unlimited: + one: "%{count} account · no limit" + other: "%{count} accounts · no limit" + label: "Seats taken" + + # Dashboard::GlobalAdmin and home/_global_admin_start. + global_admin_start: + tiles: + organizations: "Organizations" + accounts: "Accounts in organizations" + free: "Free seats" + pending: "Invitations not accepted" + stats_label: "Organizations summary" + no_organizations_lead: "There are no organizations yet. Create the first one so new accounts have somewhere to go." + attention_lead: + one: "%{count} organization — some of them need your attention." + other: "%{count} organizations — some of them need your attention." + fine_lead: + one: "%{count} organization. All of them have an administrator and free seats." + other: "%{count} organizations. All of them have an administrator and free seats." + create: "Create an organization" + organizations: "Organizations" + admins: + one: "%{count} administrator" + other: "%{count} administrators" + pending_admins: + one: "%{count} invitation pending" + other: "%{count} invitations pending" + all_organizations: "All organizations" + attention: "Needs attention" + no_admin: "No administrator" + add: "Add" + no_free_seats: "No free seats" + free_seats: + one: "%{count} seat left" + other: "%{count} seats left" + limit: "Limit" + fine_title: "Everything is fine" + fine_body: "Every organization has an administrator and free seats." + how_title: "How it works" + how_body: "Everyone who signs in through USOS lands in the organization %{usos}. It has no seat limit and can't be deleted. Accounts in the other organizations are added by their administrators." + empty_title: "Start with the first organization" + empty_lead: "Set an account limit and add an administrator — they'll invite everyone else." + + # Dashboard::OrganizationAdmin and home/_organization_admin_start. + organization_admin_start: + tiles: + students: "Students" + teachers: "Teachers" + free: "Free seats" + no_limit: "No limit" + of_max: "of %{max}" + pending: "Pending invitations" + stats_label: "Organization summary" + no_organization: "Your administrator account isn't assigned to any organization. Contact the system administrator." + full_lead: "The organization has no free seats — new people won't be able to join until someone is removed or the limit is raised." + pending_lead: + one: "%{count} person hasn't accepted the invitation yet." + other: "%{count} people haven't accepted the invitation yet." + users: "Users" + add_user: "Add a user" + recent: "Recently added" + invited: "Invitation sent" + added: "Added %{when}" + all_users: "All users" + nobody: "Nobody is here yet. Add students and teachers or wait until they sign in through the university." + seats: "Seats" + unlimited_note: "This organization has no seat limit. Everyone who signs in through USOS lands here automatically." + limited_note: "The limit is set by the system administrator. Every account, the administrator's included, takes one seat." + pending_title: "Waiting for the invitation to be accepted" + + # TeacherStartDashboard::DAYS + days: + today: "Today" + yesterday: "Yesterday" + earlier: "Earlier" + + teacher_start: + greeting: "Good morning, %{name}" + recent_lead: "Since yesterday students have bought %{items} %{groups}." + recent_items: + one: "%{count} item" + other: "%{count} items" + recent_groups: + one: "in %{count} group" + other: "in %{count} groups" + quiet_lead: "Nobody has bought anything since yesterday. Look into your groups below." + no_groups_lead: "You don't lead any groups yet. Create your first one to get started." + purchases: "Recent purchases" + filter: "Show purchases from group" + all_groups: "All groups" + removed_item: "Item removed from the shop" + no_group_purchases: "There are no purchases in this group yet." + no_purchases: "Nobody has bought anything yet. Purchases will show up here on their own." + pending: "Waiting to be graded" + pending_left: "%{categories} left for %{students}." + pending_categories: + one: "%{count} category" + other: "%{count} categories" + pending_students: + one: "%{count} student" + other: "%{count} students" + grade: "Grade" + your_groups: "Your groups" + group_meta: "%{role}, %{students}" + students: + one: "%{count} student" + other: "%{count} students" + new_purchases: + one: "%{count} new purchase" + other: "%{count} new purchases" + no_groups: "You don't lead any groups yet." + create_group: "Create a group" + + student_start: + greeting: "Hi, %{name}" + groups_lead: + one: "You're in %{count} group. Currency, ranks and badges are separate in each one." + other: "You're in %{count} groups. Currency, ranks and badges are separate in each one." + no_groups_lead: "You're not in any group yet. Ask your teacher for a code and join below." + fresh: "You're just starting out. You'll earn your first %{currency} in class." + to_spend: "To spend" + lives: "Lives" + rank: "Rank:" + no_rank: "none" + to_next: "%{total} of %{needed} to the %{rank} rank" + top: "%{total} collected. This is the highest rank." + progress_label: "Progress to the next rank in the group %{name}" + badges: "Badges" + badges_of: "of %{total}" + open_group: "Open the group" + shop: "Shop" + have_code: "Got a code from your teacher?" + code_hint: "The code has 6 characters. You can also scan the QR code with your phone's camera." + join: "Join a group" + feed: "Recently across all groups" + feed_meta: "%{group}, %{when}" + + notifications: + panel: + title: "Notifications" + mark_all: "Mark all as read" + new: "New" + earlier: "Earlier" + empty: "No notifications." + notification: + purchase: "Purchase: %{name}" + meta: "%{group}, %{when}" + unread: "new" diff --git a/config/locales/views/home.pl.yml b/config/locales/views/home.pl.yml new file mode 100644 index 00000000..926c7a98 --- /dev/null +++ b/config/locales/views/home.pl.yml @@ -0,0 +1,187 @@ +pl: + home: + index: + title: "Start" + + # HomeHelper#home_greeting — the admin dashboards. + admin_greeting: "Dzień dobry, %{name}" + admin_greeting_anonymous: "Dzień dobry" + + capacity_bar: + unlimited: + one: "%{count} konto · bez limitu" + few: "%{count} konta · bez limitu" + many: "%{count} kont · bez limitu" + other: "%{count} konta · bez limitu" + label: "Zajęte miejsca" + + # Dashboard::GlobalAdmin and home/_global_admin_start. + global_admin_start: + tiles: + organizations: "Organizacje" + accounts: "Konta w organizacjach" + free: "Wolne miejsca" + pending: "Niezaakceptowane zaproszenia" + stats_label: "Podsumowanie organizacji" + no_organizations_lead: "Nie ma jeszcze żadnej organizacji. Utwórz pierwszą, żeby nowe konta miały gdzie trafić." + attention_lead: + one: "%{count} organizacja — część z nich potrzebuje Twojej uwagi." + few: "%{count} organizacje — część z nich potrzebuje Twojej uwagi." + many: "%{count} organizacji — część z nich potrzebuje Twojej uwagi." + other: "%{count} organizacji — część z nich potrzebuje Twojej uwagi." + fine_lead: + one: "%{count} organizacja. Wszystkie mają administratora i wolne miejsca." + few: "%{count} organizacje. Wszystkie mają administratora i wolne miejsca." + many: "%{count} organizacji. Wszystkie mają administratora i wolne miejsca." + other: "%{count} organizacji. Wszystkie mają administratora i wolne miejsca." + create: "Utwórz organizację" + organizations: "Organizacje" + admins: + one: "%{count} administrator" + few: "%{count} administratorów" + many: "%{count} administratorów" + other: "%{count} administratorów" + pending_admins: + one: "%{count} zaproszenie czeka" + few: "%{count} zaproszenia czekają" + many: "%{count} zaproszeń czeka" + other: "%{count} zaproszeń czeka" + all_organizations: "Wszystkie organizacje" + attention: "Wymaga uwagi" + no_admin: "Brak administratora" + add: "Dodaj" + no_free_seats: "Brak wolnych miejsc" + free_seats: + one: "Zostało %{count} miejsce" + few: "Zostały %{count} miejsca" + many: "Zostało %{count} miejsc" + other: "Zostało %{count} miejsca" + limit: "Limit" + fine_title: "Wszystko w porządku" + fine_body: "Każda organizacja ma administratora i wolne miejsca." + how_title: "Jak to działa" + how_body: "Każdy, kto zaloguje się przez USOS, trafia do organizacji %{usos}. Nie ma ona limitu miejsc i nie można jej usunąć. Do pozostałych organizacji konta dodają ich administratorzy." + empty_title: "Zacznij od pierwszej organizacji" + empty_lead: "Ustaw limit kont i dodaj administratora — on zaprosi resztę osób." + + # Dashboard::OrganizationAdmin and home/_organization_admin_start. + organization_admin_start: + tiles: + students: "Studenci" + teachers: "Nauczyciele" + free: "Wolne miejsca" + no_limit: "Bez limitu" + of_max: "z %{max}" + pending: "Oczekujące zaproszenia" + stats_label: "Podsumowanie organizacji" + no_organization: "Twoje konto administratora nie jest przypisane do żadnej organizacji. Skontaktuj się z administratorem systemu." + full_lead: "Organizacja nie ma wolnych miejsc — nowe osoby nie dołączą, dopóki ktoś nie zostanie usunięty albo limit nie wzrośnie." + pending_lead: + one: "%{count} osoba jeszcze nie przyjęła zaproszenia." + few: "%{count} osoby jeszcze nie przyjęły zaproszenia." + many: "%{count} osób jeszcze nie przyjęło zaproszenia." + other: "%{count} osoby jeszcze nie przyjęły zaproszenia." + users: "Użytkownicy" + add_user: "Dodaj użytkownika" + recent: "Ostatnio dodani" + invited: "Zaproszenie wysłane" + added: "Dodano %{when}" + all_users: "Wszyscy użytkownicy" + nobody: "Nikogo tu jeszcze nie ma. Dodaj studentów i nauczycieli albo poczekaj, aż zalogują się przez uczelnię." + seats: "Miejsca" + unlimited_note: "Ta organizacja nie ma limitu miejsc. Każdy, kto zaloguje się przez USOS, trafia tutaj automatycznie." + limited_note: "Limit ustala administrator systemu. Każde konto, także administratora, zajmuje jedno miejsce." + pending_title: "Czeka na przyjęcie zaproszenia" + + # TeacherStartDashboard::DAYS + days: + today: "Dziś" + yesterday: "Wczoraj" + earlier: "Wcześniej" + + teacher_start: + greeting: "Dzień dobry, %{name}" + recent_lead: "Od wczoraj studenci kupili %{items} %{groups}." + recent_items: + one: "%{count} przedmiot" + few: "%{count} przedmioty" + many: "%{count} przedmiotów" + other: "%{count} przedmiotu" + recent_groups: + one: "w %{count} grupie" + few: "w %{count} grupach" + many: "w %{count} grupach" + other: "w %{count} grupy" + quiet_lead: "Od wczoraj nikt nic nie kupił. Zajrzyj do swoich grup poniżej." + no_groups_lead: "Nie prowadzisz jeszcze żadnej grupy. Utwórz pierwszą, żeby zacząć." + purchases: "Ostatnie zakupy" + filter: "Pokaż zakupy z grupy" + all_groups: "Wszystkie grupy" + removed_item: "Przedmiot usunięty z oferty" + no_group_purchases: "W tej grupie nie ma jeszcze żadnych zakupów." + no_purchases: "Nikt jeszcze niczego nie kupił. Zakupy pojawią się tutaj same." + pending: "Czeka na ocenę" + pending_left: "Zostało %{categories} dla %{students}." + pending_categories: + one: "%{count} kategoria" + few: "%{count} kategorie" + many: "%{count} kategorii" + other: "%{count} kategorii" + pending_students: + one: "%{count} studenta" + few: "%{count} studentów" + many: "%{count} studentów" + other: "%{count} studenta" + grade: "Oceń" + your_groups: "Twoje grupy" + group_meta: "%{role}, %{students}" + students: + one: "%{count} student" + few: "%{count} studentów" + many: "%{count} studentów" + other: "%{count} studenta" + new_purchases: + one: "%{count} nowy zakup" + few: "%{count} nowe zakupy" + many: "%{count} nowych zakupów" + other: "%{count} nowego zakupu" + no_groups: "Nie prowadzisz jeszcze żadnej grupy." + create_group: "Utwórz grupę" + + student_start: + greeting: "Cześć, %{name}" + groups_lead: + one: "Należysz do %{count} grupy. Waluta, rangi i odznaki są osobne w każdej z nich." + few: "Należysz do %{count} grup. Waluta, rangi i odznaki są osobne w każdej z nich." + many: "Należysz do %{count} grup. Waluta, rangi i odznaki są osobne w każdej z nich." + other: "Należysz do %{count} grupy. Waluta, rangi i odznaki są osobne w każdej z nich." + no_groups_lead: "Nie należysz jeszcze do żadnej grupy. Poproś prowadzącego o kod i dołącz poniżej." + fresh: "Dopiero zaczynasz. Pierwsze %{currency} zdobędziesz na zajęciach." + to_spend: "Do wydania" + lives: "Życia" + rank: "Ranga:" + no_rank: "brak" + to_next: "%{total} z %{needed} do rangi %{rank}" + top: "%{total} zebranych. To najwyższa ranga." + progress_label: "Postęp do następnej rangi w grupie %{name}" + badges: "Odznaki" + badges_of: "z %{total}" + open_group: "Otwórz grupę" + shop: "Sklep" + have_code: "Masz kod od prowadzącego?" + code_hint: "Kod ma 6 znaków. Możesz też zeskanować kod QR aparatem telefonu." + join: "Dołącz do grupy" + feed: "Ostatnio we wszystkich grupach" + feed_meta: "%{group}, %{when}" + + notifications: + panel: + title: "Powiadomienia" + mark_all: "Oznacz wszystkie" + new: "Nowe" + earlier: "Wcześniej" + empty: "Brak powiadomień." + notification: + purchase: "Zakup: %{name}" + meta: "%{group}, %{when}" + unread: "nowe" diff --git a/config/locales/views/invitation_mailer.en.yml b/config/locales/views/invitation_mailer.en.yml new file mode 100644 index 00000000..2a56315c --- /dev/null +++ b/config/locales/views/invitation_mailer.en.yml @@ -0,0 +1,19 @@ +en: + invitation_mailer: + roles: + admin: "an administrator" + member: "a member" + invitation_email: + subject: "Invitation to Gamification Hub" + lead: "You've been added as %{role} of the organization %{organization} in Gamification Hub." + + # shared/_invitation_email.{html,text} + shared: + invitation_email: + title: "Invitation" + text: "Click the button below to finish setting up your account and fill in your details." + text_plain: "To finish setting up your account and fill in your details, open the link below." + button: "Finish setting up your account" + note: "The link is valid for 7 days. If the button doesn't work, copy this address into your browser:" + valid_for: "The link is valid for 7 days." + footer: "If you weren't expecting this invitation, you can ignore this message." diff --git a/config/locales/views/invitation_mailer.pl.yml b/config/locales/views/invitation_mailer.pl.yml new file mode 100644 index 00000000..aa52f12d --- /dev/null +++ b/config/locales/views/invitation_mailer.pl.yml @@ -0,0 +1,19 @@ +pl: + invitation_mailer: + roles: + admin: "administrator" + member: "członek" + invitation_email: + subject: "Zaproszenie do systemu GamificationHub" + lead: "Zostałeś dodany jako %{role} organizacji %{organization} w Gamification Hub." + + # shared/_invitation_email.{html,text} + shared: + invitation_email: + title: "Zaproszenie" + text: "Kliknij przycisk poniżej, aby dokończyć zakładanie konta i uzupełnić swoje dane." + text_plain: "Aby dokończyć zakładanie konta i uzupełnić swoje dane, otwórz poniższy link." + button: "Dokończ zakładanie konta" + note: "Link jest ważny przez 7 dni. Jeśli przycisk nie działa, skopiuj ten adres do przeglądarki:" + valid_for: "Link jest ważny przez 7 dni." + footer: "Jeśli nie spodziewałeś się tego zaproszenia, możesz zignorować tę wiadomość." diff --git a/config/locales/views/items.en.yml b/config/locales/views/items.en.yml new file mode 100644 index 00000000..6855a55e --- /dev/null +++ b/config/locales/views/items.en.yml @@ -0,0 +1,156 @@ +en: + items: + flash: + created: "Added “%{name}” to the shop." + updated: "Saved “%{name}”. Changes apply to new purchases." + destroyed: "Removed “%{name}” from the shop. Copies already bought stay with the students." + + helpers: + nobody_bought: "Nobody has bought it yet" + bought: + one: "Bought %{count} time" + other: "Bought %{count} times" + name_placeholder: "Item name" + rules_placeholder: "What it gives the student…" + unlock_option_rank: "from the %{name} rank" + unlock_option_everyone: "any student" + discount_option_rank: "from %{name} (−%{discount}% and more)" + discount_option_none: "none" + + # Sentences built by ItemsHelper and, live, by item_form_controller.ts. + # %{...} slots in bold are names and numbers; the rest are fragments below. + sentences: + unlock_everyone: "Any student can buy this item%{lives}" + unlock_restricted: "Only students%{rank}%{badges} can buy it%{lives}" + unlock_rank: " with the %{rank} rank or higher" + unlock_badges: + one: " who have the %{badges} badge" + other: " who have all the badges %{badges}" + lives_yes: ", even at 0 lives." + lives_no: " if they have at least 1 life." + no_discount: "No discounts. Every buyer pays %{price}." + example: "For example: a student%{holder} saves %{percent} and pays %{paid} instead of %{price}." + max: "At most: a student%{holder} gets a %{percent} discount." + holder_rank: " with the %{rank} rank" + holder_badges: + one: "the %{badges} badge" + other: "the badges %{badges}" + holder_all_badges: "all the badges" + holder_join_and: " and %{phrase}" + holder_join_with: " with %{phrase}" + warn_cap: "The discounts add up to %{raw}%, but the shop takes off at most %{cap}%. The excess is lost — consider lower discounts on ranks and badges." + warn_rank_everyone: "The rank discount will apply to every buyer." + warn_rank_gate: "Only students from the %{rank} rank can buy it, so the rank discount will apply to every buyer." + warn_badge_required: "The %{name} badge is required to buy it, so its discount will apply to every buyer." + + # ItemCard, ItemOffer and RequirementPhrasing (shared with the shop). + labels: + discount_up_to: "Discounts up to −%{discount}%" + discount: "−%{discount}% %{source}" + source_rank_and_badges: "for rank and badges" + source_rank: "for rank" + source_badges: "for badges" + lock_rank: "From the %{name} rank" + seal_rank: "From the %{name} rank" + seal_badge: "For the %{name} badge" + seal_lives: "Unavailable at 0 lives" + requires_rank: "Requires the rank" + requires_badge: "Requires the badge" + requires_life: "You have 0 lives. Get a life back first." + + index: + title: "Items" + lead: + one: "%{count} item in the shop, cheapest first." + other: "%{count} items in the shop, cheapest first." + new: "New item" + empty_title: "The shop is still empty" + empty_body: "Add items so the collected currency has something to go to. Each can have requirements and discounts, so the shop opens up gradually as students progress." + + new: + page_title: "New item" + + edit: + page_title: "Edit item - %{name}" + + grid_card: + discount: "Discount" + zero_lives: "At 0 lives" + edit_label: "Edit item %{name}" + + card: + zero_lives: "Works at 0 lives" + buy: "Buy for %{price}" + collect_more: "Collect" + progress_label: "Progress to the %{rank} rank" + + preview: + summary: "Card preview" + students_view: "This is what students will see" + state: "Card state" + afford: "Available" + save: "Saving up" + sealed: "Sealed" + seal_hint: "Without requirements an item is never sealed." + thumb: "Thumbnail in notifications" + thumb_sample: "Student purchase, today 10:21" + + form: + breadcrumb: "Items" + new_title: "New item" + edit_title: "Edit item" + new_lead: "Fill in the card, and the preview will show how students will see it." + edit_lead: "The preview shows the card after your changes." + owners: + one: "%{count} student has this item." + other: "%{count} students have this item." + owners_note: "Changes to the price, requirements and discounts only apply to new purchases." + card: "Item card" + card_hint: "This is how the item looks in the shop and in the student's inventory." + name_placeholder: "e.g. Quiz retake" + rules: "What it gives the student" + rules_placeholder: "e.g. A chance to retake a quiz" + rules_hint: "Didactic description: a concrete benefit, no story." + story: "Story description" + story_placeholder: "e.g. The ship's emergency return to base" + price_section: "Price" + price: "Price" + zero_lives: "Can be bought at 0 lives" + zero_lives_hint: "Useful for items that restore a life." + who_section: "Who can buy it" + who_hint: "Without requirements the item is available to everyone." + who: "Can be bought by" + who_label: "Who can buy it" + required_badges: "Required badges" + add_badge: "Add a badge…" + no_badges: "This group has no badges yet." + discounts_section: "Discounts" + discounts_hint: "A rank discount is the percentage assigned to the student's rank. Badge discounts add up. Meeting any one condition is enough for the discount to apply — with no conditions every buyer gets it. The amount is always worked out from the student's rank and all their badges, including those not chosen here." + rank_discount: "Rank discount" + badge_discount: "Badge discount" + discount_badges: "Badges with a discount" + add_discount_badge: "Add a badge with a discount…" + removal_hint: "The item will disappear from the shop and from lists. Students who bought it still have it, and it stays in their history as “Removed from the shop”." + remove: "Delete item" + create: "Add item" + + confirm_destroy: + page_title: "Delete item %{name}" + title: "Remove “%{name}” from the shop?" + body: "The item will disappear from the shop and from lists. Students who bought it keep it, and it will be marked “Removed from the shop” in their history." + owners: + one: "%{count} student has this item and will keep it after it's removed." + other: "%{count} students have this item and will keep it after it's removed." + remove: "Remove from the shop" + + activerecord: + errors: + models: + item: + attributes: + name: + blank: "Enter the item name." + didactic_description: + blank: "Describe what the item gives the student." + price: + too_low: "The price must be at least 1." diff --git a/config/locales/views/items.pl.yml b/config/locales/views/items.pl.yml new file mode 100644 index 00000000..f3a129b8 --- /dev/null +++ b/config/locales/views/items.pl.yml @@ -0,0 +1,164 @@ +pl: + items: + flash: + created: "Dodano przedmiot „%{name}” do sklepu." + updated: "Zapisano „%{name}”. Zmiany dotyczą nowych zakupów." + destroyed: "Usunięto „%{name}” z oferty. Kupione egzemplarze zostają u studentów." + + helpers: + nobody_bought: "Jeszcze nikt nie kupił" + bought: + one: "Kupiono %{count} raz" + few: "Kupiono %{count} razy" + many: "Kupiono %{count} razy" + other: "Kupiono %{count} razy" + name_placeholder: "Nazwa przedmiotu" + rules_placeholder: "Co daje studentowi…" + unlock_option_rank: "od rangi %{name}" + unlock_option_everyone: "każdy student" + discount_option_rank: "od %{name} (−%{discount}% i więcej)" + discount_option_none: "brak" + + # Sentences built by ItemsHelper and, live, by item_form_controller.ts. + # %{...} slots in bold are names and numbers; the rest are fragments below. + sentences: + unlock_everyone: "Każdy student może kupić ten przedmiot%{lives}" + unlock_restricted: "Kupią tylko studenci%{rank}%{badges}%{lives}" + unlock_rank: " z rangą %{rank} lub wyższą" + unlock_badges: + one: ", którzy mają odznakę %{badges}" + other: ", którzy mają wszystkie odznaki %{badges}" + lives_yes: ". Także przy 0 życiach." + lives_no: ", jeśli mają co najmniej 1 życie." + no_discount: "Bez zniżek. Każdy kupujący zapłaci %{price}." + example: "Przykładowo: student%{holder} zaoszczędzi %{percent} i zapłaci %{paid} zamiast %{price}." + max: "Maksymalnie: student%{holder} uzyska zniżkę %{percent}." + holder_rank: " z rangą %{rank}" + holder_badges: + one: "odznaką %{badges}" + other: "odznakami %{badges}" + holder_all_badges: "wszystkimi odznakami" + holder_join_and: " oraz %{phrase}" + holder_join_with: " z %{phrase}" + warn_cap: "Zniżki sumują się do %{raw}%, a sklep odejmie najwyżej %{cap}%. Nadwyżka przepada — rozważ niższe zniżki przy rangach i odznakach." + warn_rank_everyone: "Zniżka dla rang obejmie każdego kupującego." + warn_rank_gate: "Kupić mogą tylko studenci od rangi %{rank}, więc zniżka dla rang obejmie każdego kupującego." + warn_badge_required: "Odznaka %{name} jest wymagana do zakupu, więc jej zniżka obejmie każdego kupującego." + + # ItemCard, ItemOffer and RequirementPhrasing (shared with the shop). + labels: + discount_up_to: "Zniżki do −%{discount}%" + discount: "−%{discount}% %{source}" + source_rank_and_badges: "za rangę i odznaki" + source_rank: "za rangę" + source_badges: "za odznaki" + lock_rank: "Od rangi %{name}" + seal_rank: "Od rangi %{name}" + seal_badge: "Za odznakę %{name}" + seal_lives: "Niedostępne przy 0 życiach" + requires_rank: "Wymaga rangi" + requires_badge: "Wymaga odznaki" + requires_life: "Masz 0 żyć. Najpierw odzyskaj życie." + + index: + title: "Przedmioty" + lead: + one: "%{count} przedmiot w sklepie, od najtańszego." + few: "%{count} przedmioty w sklepie, od najtańszego." + many: "%{count} przedmiotów w sklepie, od najtańszego." + other: "%{count} przedmiotu w sklepie, od najtańszego." + new: "Nowy przedmiot" + empty_title: "Sklep jest jeszcze pusty" + empty_body: "Dodaj przedmioty, żeby zebrana waluta miała na co iść. Każdy może mieć wymagania i zniżki, więc sklep otwiera się stopniowo wraz z postępami." + + new: + page_title: "Nowy przedmiot" + + edit: + page_title: "Edytuj przedmiot - %{name}" + + grid_card: + discount: "Zniżka" + zero_lives: "Przy 0 życiach" + edit_label: "Edytuj przedmiot %{name}" + + card: + zero_lives: "Działa przy 0 życiach" + buy: "Kup za %{price}" + collect_more: "Zbierz jeszcze" + progress_label: "Postęp do rangi %{rank}" + + preview: + summary: "Podgląd karty" + students_view: "Tak zobaczą to studenci" + state: "Stan karty" + afford: "Dostępny" + save: "Zbierasz" + sealed: "Zapieczętowany" + seal_hint: "Bez wymagań przedmiot nigdy nie będzie zapieczętowany." + thumb: "Miniatura w powiadomieniach" + thumb_sample: "Zakup studenta, dziś 10:21" + + form: + breadcrumb: "Przedmioty" + new_title: "Nowy przedmiot" + edit_title: "Edytuj przedmiot" + new_lead: "Wypełnij kartę, a podgląd obok pokaże, jak zobaczą ją studenci." + edit_lead: "Podgląd obok pokazuje kartę po zmianach." + owners: + one: "%{count} student ma ten przedmiot." + few: "%{count} studentów ma ten przedmiot." + many: "%{count} studentów ma ten przedmiot." + other: "%{count} studenta ma ten przedmiot." + owners_note: "Zmiany ceny, wymagań i zniżek dotyczą tylko nowych zakupów." + card: "Karta przedmiotu" + card_hint: "Tak przedmiot wygląda w sklepie i w ekwipunku studenta." + name_placeholder: "np. Poprawa wejściówki" + rules: "Co daje studentowi" + rules_placeholder: "np. Możliwość ponownego napisania wejściówki" + rules_hint: "Opis dydaktyczny: konkretna korzyść, bez fabuły." + story: "Opis fabularny" + story_placeholder: "np. Awaryjny powrót statku do bazy" + price_section: "Cena" + price: "Cena" + zero_lives: "Można kupić przy 0 życiach" + zero_lives_hint: "Przydaje się dla przedmiotów, które przywracają życie." + who_section: "Kto może kupić" + who_hint: "Bez wymagań przedmiot jest dostępny dla wszystkich." + who: "Kupić może" + who_label: "Kto może kupić" + required_badges: "Wymagane odznaki" + add_badge: "Dodaj odznakę…" + no_badges: "W tej grupie nie ma jeszcze odznak." + discounts_section: "Zniżki" + discounts_hint: "Zniżka z rangi to procent przypisany do rangi studenta. Zniżki z odznak się sumują. Wystarczy spełnić jeden z warunków, żeby zniżka zadziałała — bez warunków dostaje ją każdy kupujący. Wysokość liczy się zawsze z rangi i wszystkich odznak studenta, także tych niewybranych tutaj." + rank_discount: "Zniżka dla rang" + badge_discount: "Zniżka za odznaki" + discount_badges: "Odznaki ze zniżką" + add_discount_badge: "Dodaj odznakę ze zniżką…" + removal_hint: "Przedmiot zniknie ze sklepu i z list. Studenci, którzy go kupili, nadal go mają, a w ich historii zostanie jako „Usunięty z oferty”." + remove: "Usuń przedmiot" + create: "Dodaj przedmiot" + + confirm_destroy: + page_title: "Usuń przedmiot %{name}" + title: "Usunąć „%{name}” ze sklepu?" + body: "Przedmiot zniknie ze sklepu i z list. Studenci, którzy go kupili, zachowają go, a w historii zostanie oznaczony jako „Usunięty z oferty”." + owners: + one: "%{count} student ma ten przedmiot i zachowa go po usunięciu." + few: "%{count} studentów ma ten przedmiot i zachowa go po usunięciu." + many: "%{count} studentów ma ten przedmiot i zachowa go po usunięciu." + other: "%{count} studenta ma ten przedmiot i zachowa go po usunięciu." + remove: "Usuń z oferty" + + activerecord: + errors: + models: + item: + attributes: + name: + blank: "Podaj nazwę przedmiotu." + didactic_description: + blank: "Napisz, co przedmiot daje studentowi." + price: + too_low: "Cena musi wynosić co najmniej 1." diff --git a/config/locales/views/join.en.yml b/config/locales/views/join.en.yml new file mode 100644 index 00000000..f75eb19f --- /dev/null +++ b/config/locales/views/join.en.yml @@ -0,0 +1,43 @@ +en: + join: + # InviteLookup::Result#error + errors: + missing: "Enter the invite code." + not_found: "We couldn't find that code. Check it with your teacher." + expired: "This code has expired. Ask your teacher for a new one." + exhausted: "The maximum number of people have already used this code. Ask your teacher for a new one." + + new: + page_title: "Join a group" + + show: + page_title: "Join a group" + + create: + page_title: "Joined the group" + + step_code: + step: "Step 1 of 2" + title: "Join a group" + hint: "Enter the 6-character code your teacher gave you." + code_label: "Invite code" + slot_label: "Character %{index} of %{total}" + member: "You're already in this group." + open_group: "Open the group" + qr_hint: "Got a QR code? Scan it with your phone's camera and the code fills in by itself." + + step_nickname: + step: "Step 2 of 2" + title: "You're joining the group %{name}" + hint: "In this group others will see you under a nickname — in the ranking too. You can change it later in the group settings." + nickname: "Nickname in this group" + nickname_hint: "Leave it empty to use your full name. You can set a nickname later." + join: "Join the group" + + step_done: + title: "Joined the group %{name}" + led_by: "Led by %{name}." + you_are_html: "You're there as %{name}." + currency: "The currency in this group is %{currency}." + done: "Done" + diff --git a/config/locales/views/join.pl.yml b/config/locales/views/join.pl.yml new file mode 100644 index 00000000..8b272d20 --- /dev/null +++ b/config/locales/views/join.pl.yml @@ -0,0 +1,43 @@ +pl: + join: + # InviteLookup::Result#error + errors: + missing: "Podaj kod zaproszenia." + not_found: "Nie znaleźliśmy takiego kodu. Sprawdź go z prowadzącym." + expired: "Ten kod wygasł. Poproś prowadzącego o nowy." + exhausted: "Z tego kodu skorzystała już maksymalna liczba osób. Poproś prowadzącego o nowy." + + new: + page_title: "Dołącz do grupy" + + show: + page_title: "Dołącz do grupy" + + create: + page_title: "Dołączono do grupy" + + step_code: + step: "Krok 1 z 2" + title: "Dołącz do grupy" + hint: "Wpisz 6-znakowy kod, który podał prowadzący." + code_label: "Kod zaproszenia" + slot_label: "Znak %{index} z %{total}" + member: "Już należysz do tej grupy." + open_group: "Otwórz grupę" + qr_hint: "Masz kod QR? Zeskanuj go aparatem telefonu, a kod wpisze się sam." + + step_nickname: + step: "Krok 2 z 2" + title: "Dołączasz do grupy %{name}" + hint: "W tej grupie inni zobaczą Cię pod pseudonimem — także w rankingu. Możesz go później zmienić w ustawieniach w grupie." + nickname: "Pseudonim w tej grupie" + nickname_hint: "Zostaw puste, jeśli chcesz używać imienia i nazwiska. Pseudonim możesz ustawić później." + join: "Dołącz do grupy" + + step_done: + title: "Dołączono do grupy %{name}" + led_by: "Prowadzi %{name}." + you_are_html: "Jesteś tam jako %{name}." + currency: "Walutą w tej grupie są %{currency}." + done: "Gotowe" + diff --git a/config/locales/views/layouts.en.yml b/config/locales/views/layouts.en.yml new file mode 100644 index 00000000..f8b72563 --- /dev/null +++ b/config/locales/views/layouts.en.yml @@ -0,0 +1,89 @@ +en: + layouts: + # Navigation (app/values/navigation.rb) — sidebar, deck, tab bar. + navigation: + start: "Home" + groups: "Groups" + notifications: "Notifications" + overview: "Overview" + students: "Students" + sheets: "Grade sheets" + items: "Items" + ranks: "Ranks" + badges: "Badges" + ranking: "Ranking" + invites: "Invites" + teachers: "Teachers" + settings: "Group settings" + shop: "Shop" + my_items: "My items" + history: "Currency history" + membership: "Group settings" + organizations: "Organizations" + short: + organization: "Organization" + sheets: "Sheets" + my_items: "Items" + + # GroupChrome#role_label + chrome_roles: + own: "You lead this group" + sup: "You assist this group" + lrn: "You're a participant" + adm: "You oversee this group" + + theme: + light: "Light theme" + dark: "Dark theme" + toggle: "Toggle theme" + + header: + home: "Gamification Hub, home" + switch_group: "Group %{name}, switch group" + balance_title: "%{balance} to spend, %{total} collected in total" + balance_label: "You have %{balance} to spend, show currency history" + to_spend: "to spend" + join: "Join a group" + notifications: "Notifications" + notifications_unread: + one: "Notifications, %{count} new" + other: "Notifications, %{count} new" + account: "Account: %{name}" + + sidebar: + main: "Main" + your_groups: "Your groups" + expand: "Expand" + collapse: "Collapse panel" + + tab_bar: + label: "Navigation" + + deck: + group: "Group %{name}" + switch: "Switch group" + sections: "Group sections" + + group_switcher: + title: "Switch group" + here: "You're here" + join: "Join a group" + + more_sheet: + create_group: "Create a group" + account: "Account" + account_settings: "Account settings" + logout: "Log out" + + account_menu: + label: "Account" + account_settings: "Account settings" + logout: "Log out" + + theme_toggle: + help: "Help:" + contact: "contact" + + locale_switcher: + switch_to: "Polski" + label: "Switch language to Polish" diff --git a/config/locales/views/layouts.pl.yml b/config/locales/views/layouts.pl.yml new file mode 100644 index 00000000..19d3eecf --- /dev/null +++ b/config/locales/views/layouts.pl.yml @@ -0,0 +1,92 @@ +pl: + layouts: + # Navigation (app/values/navigation.rb) — sidebar, deck, tab bar. + navigation: + start: "Start" + groups: "Grupy" + notifications: "Powiadomienia" + overview: "Przegląd" + students: "Studenci" + sheets: "Arkusze ocen" + items: "Przedmioty" + ranks: "Rangi" + badges: "Odznaki" + ranking: "Ranking" + invites: "Zaproszenia" + teachers: "Nauczyciele" + settings: "Ustawienia grupy" + shop: "Sklep" + my_items: "Moje przedmioty" + history: "Historia waluty" + membership: "Ustawienia w grupie" + organizations: "Organizacje" + short: + organization: "Organizacja" + sheets: "Arkusze" + my_items: "Przedmioty" + + # GroupChrome#role_label + chrome_roles: + own: "Prowadzisz tę grupę" + sup: "Wspierasz tę grupę" + lrn: "Jesteś uczestnikiem" + adm: "Nadzorujesz tę grupę" + + theme: + light: "Jasny motyw" + dark: "Ciemny motyw" + toggle: "Przełącz motyw" + + header: + home: "Gamification Hub, start" + switch_group: "Grupa %{name}, zmień grupę" + balance_title: "%{balance} do wydania, %{total} zebrane łącznie" + balance_label: "Masz %{balance} do wydania, pokaż historię waluty" + to_spend: "do wydania" + join: "Dołącz do grupy" + notifications: "Powiadomienia" + notifications_unread: + one: "Powiadomienia, %{count} nowe" + few: "Powiadomienia, %{count} nowe" + many: "Powiadomienia, %{count} nowych" + other: "Powiadomienia, %{count} nowego" + account: "Konto: %{name}" + + sidebar: + main: "Główna" + your_groups: "Twoje grupy" + expand: "Rozwiń" + collapse: "Zwiń panel" + + tab_bar: + label: "Nawigacja" + + deck: + group: "Grupa %{name}" + switch: "Zmień grupę" + sections: "Sekcje grupy" + + group_switcher: + title: "Zmień grupę" + here: "Tu jesteś" + join: "Dołącz do grupy" + + more_sheet: + create_group: "Utwórz grupę" + account: "Konto" + account_settings: "Ustawienia konta" + logout: "Wyloguj się" + + account_menu: + label: "Konto" + account_settings: "Ustawienia konta" + logout: "Wyloguj się" + + + theme_toggle: + help: "Pomoc:" + contact: "kontakt" + + locale_switcher: + switch_to: "English" + label: "Zmień język na angielski" diff --git a/config/locales/views/organizations.en.yml b/config/locales/views/organizations.en.yml new file mode 100644 index 00000000..f896cf1c --- /dev/null +++ b/config/locales/views/organizations.en.yml @@ -0,0 +1,185 @@ +en: + organizations: + flash: + created: "Organization created." + updated: "Organization updated." + destroyed: "Deleted organization %{name}." + admin_added: "Organization administrator added. An invitation has been sent by e-mail." + user_added: "User added to the organization. An invitation has been sent by e-mail." + admin_removed: "Deleted administrator: %{name}." + user_updated: "Changes saved: %{name}." + user_removed: "Deleted user: %{name}." + invitation_resent: "Invitation sent again: %{email}." + too_many_resends: "The invitation has already been sent several times. Try again in a few minutes." + + # OrganizationsHelper#user_role_label — a role not listed here is humanized. + roles: + student: "Student" + teacher: "Teacher" + + helpers: + capacity_accounts: + one: "%{count} account" + other: "%{count} accounts" + capacity_free: + one: "%{count} free" + other: "%{count} free" + capacity_unlimited: "%{accounts} · no seat limit" + capacity_limited: "%{members} of %{max} seats taken · %{free}" + + index: + title: "Organizations" + lead: "Universities using the app. Accounts that sign in through USOS go to the organization %{usos}." + create: "Create organization" + organization: "Organization" + members: "Members" + admins: "Administrators" + empty_title: "There are no organizations yet" + empty_lead: "Create an organization, then add its administrator — they'll invite everyone else." + + show: + breadcrumb: "Organizations" + edit: "Edit" + add_admin: "Add an administrator" + add_user: "Add a user" + danger_title: "Delete the organization" + danger_body: "All of its accounts will disappear along with the organization. This can't be undone." + danger_button: "Delete the organization" + + admins: + title: "Administrators" + role: "Administrator" + invited: "Invitation sent" + none: "The organization has no administrator yet." + remove: "Delete" + remove_label: "Delete: %{name}" + confirm_destroy: + page_title: "Delete the administrator?" + title: "Delete the administrator %{name}?" + body: "The account will be deleted permanently and one seat in the organization will be freed." + pending: "The invitation that was sent will stop working." + remove: "Delete the account" + + members: + search_placeholder: "Search by name or e-mail" + search_label: "Search for a user" + all_roles: "All roles" + filter_label: "Filter by role" + of: "of" + user: "User" + role: "Role" + index_number: "Index number" + actions: "Actions" + no_match: "No users match" + no_match_hint: "Clear the search, change the role or check the spelling." + empty_title: "There are no users yet" + empty_lead: "Add students and teachers — they'll get an invitation by e-mail. Accounts created by signing in through the university show up here on their own." + + member_row: + invited: "Invitation sent" + index_number: "Index number" + role: "Role" + edit: "Edit" + edit_label: "Edit: %{name}" + remove: "Delete" + remove_label: "Delete: %{name}" + + resend_invitation_button: + title: "Resend the invitation" + label: "Resend the invitation: %{name}" + + owned_groups_notice: + lead: + one: "The group led by this account will be deleted too:" + other: "The groups led by this account will be deleted too:" + rest: "— along with all students, currency, badges and grade sheets." + + form: + new_title: "New organization" + edit_title: "Edit the organization" + usos_note: "This organization is tied to USOS: everyone who signs in through USOS lands here. It has no member limit and can't be deleted." + name: "Name" + max_members: "Member limit" + create: "Create organization" + save: "Save" + + new: + page_title: "New organization" + + edit: + page_title: "Edit the organization — %{name}" + + new_admin: + page_title: "Add an organization administrator" + + admin_form: + title: "Add an administrator" + hint: "We'll send an invitation to the address you enter. After the first sign-in the administrator fills in their details." + email: "E-mail address" + email_placeholder: "john.smith@example.com" + submit: "Send the invitation" + + new_user: + page_title: "Add an organization user" + + user_form: + new_title: "Add a user" + edit_title: "Edit the user" + hint: "We'll send an invitation to the address you enter. The account takes up one seat in the organization." + email: "E-mail address" + email_placeholder: "john.smith@example.com" + full_name: "Full name" + role: "Role" + university_number: "Index number (optional)" + usos_id: "USOS ID (optional)" + submit_new: "Send the invitation" + submit_edit: "Save" + + users: + edit: + page_title: "Edit the user — %{name}" + confirm_destroy: + page_title: "Delete the account of %{name}?" + title: "Delete the account of %{name}?" + body: "The account will be deleted permanently, together with its group memberships, currency, badges and items. One seat in the organization will be freed." + remove: "Delete the account" + + confirm_destroy: + page_title: "Delete the organization %{name}" + title: "Delete the organization for good?" + accounts: + one: "%{count} account will be deleted" + other: "%{count} accounts will be deleted" + accounts_tail: "along with the organization, including the administrators' accounts." + confirm_label: "Type the organization's name to confirm:" + mismatch: "The name you typed doesn't match the organization's name. The organization was not deleted." + remove: "Delete the organization" + + dialog_actions: + cancel: "Cancel" + + account_setups: + flash: + done: "Account set up." + edit: + page_title: "Account setup" + title: "Fill in your details" + lead: "This is a one-time step. The teachers in your groups will see your full name." + email: "E-mail address" + full_name: "Full name" + full_name_placeholder: "John Smith" + submit: "Save and continue" + logout: "Log out" + + activerecord: + errors: + models: + organization: + attributes: + max_members: + below_members: "The limit can't be lower than the current number of members (%{count})." + usos_undeletable: "The organization tied to USOS can't be deleted." + user: + attributes: + organization: + member_limit: "The organization has reached its member limit." diff --git a/config/locales/views/organizations.pl.yml b/config/locales/views/organizations.pl.yml new file mode 100644 index 00000000..9d85ca52 --- /dev/null +++ b/config/locales/views/organizations.pl.yml @@ -0,0 +1,193 @@ +pl: + organizations: + flash: + created: "Pomyślnie utworzono organizację." + updated: "Pomyślnie zaktualizowano organizację." + destroyed: "Usunięto organizację %{name}." + admin_added: "Pomyślnie dodano administratora organizacji. Wysłano zaproszenie na email." + user_added: "Pomyślnie dodano użytkownika do organizacji. Wysłano zaproszenie na email." + admin_removed: "Usunięto administratora: %{name}." + user_updated: "Zapisano zmiany: %{name}." + user_removed: "Usunięto użytkownika: %{name}." + invitation_resent: "Wysłano ponownie zaproszenie: %{email}." + too_many_resends: "Zaproszenie zostało już wysłane kilka razy. Spróbuj ponownie za kilka minut." + + # OrganizationsHelper#user_role_label — a role not listed here is humanized. + roles: + student: "Student" + teacher: "Nauczyciel" + + helpers: + capacity_accounts: + one: "%{count} konto" + few: "%{count} konta" + many: "%{count} kont" + other: "%{count} konta" + capacity_free: + one: "%{count} wolne" + few: "%{count} wolne" + many: "%{count} wolnych" + other: "%{count} wolnego" + capacity_unlimited: "%{accounts} · bez limitu miejsc" + capacity_limited: "%{members} z %{max} miejsc zajętych · %{free}" + + index: + title: "Organizacje" + lead: "Uczelnie korzystające z aplikacji. Konta logujące się przez USOS trafiają do organizacji %{usos}." + create: "Utwórz organizację" + organization: "Organizacja" + members: "Członkowie" + admins: "Administratorzy" + empty_title: "Nie ma jeszcze żadnej organizacji" + empty_lead: "Utwórz organizację, a potem dodaj jej administratora — on zaprosi resztę osób." + + show: + breadcrumb: "Organizacje" + edit: "Edytuj" + add_admin: "Dodaj administratora" + add_user: "Dodaj użytkownika" + danger_title: "Usuń organizację" + danger_body: "Razem z organizacją znikną wszystkie jej konta. Tej operacji nie można cofnąć." + danger_button: "Usuń organizację" + + admins: + title: "Administratorzy" + role: "Administrator" + invited: "Zaproszenie wysłane" + none: "Organizacja nie ma jeszcze administratora." + remove: "Usuń" + remove_label: "Usuń: %{name}" + confirm_destroy: + page_title: "Usunąć administratora?" + title: "Usunąć administratora %{name}?" + body: "Konto zostanie usunięte na stałe i zwolni się jedno miejsce w organizacji." + pending: "Wysłane zaproszenie przestanie działać." + remove: "Usuń konto" + + members: + search_placeholder: "Szukaj po nazwisku lub e-mailu" + search_label: "Szukaj użytkownika" + all_roles: "Wszystkie role" + filter_label: "Filtruj po roli" + of: "z" + user: "Użytkownik" + role: "Rola" + index_number: "Numer indeksu" + actions: "Akcje" + no_match: "Brak użytkowników pasujących do" + no_match_hint: "Wyczyść wyszukiwanie, zmień rolę albo sprawdź pisownię." + empty_title: "Nie ma jeszcze użytkowników" + empty_lead: "Dodaj studentów i nauczycieli — dostaną zaproszenie e-mailem. Konta zakładane przez logowanie z uczelni pojawią się tu same." + + member_row: + invited: "Zaproszenie wysłane" + index_number: "Numer indeksu" + role: "Rola" + edit: "Edytuj" + edit_label: "Edytuj: %{name}" + remove: "Usuń" + remove_label: "Usuń: %{name}" + + resend_invitation_button: + title: "Wyślij zaproszenie ponownie" + label: "Wyślij zaproszenie ponownie: %{name}" + + owned_groups_notice: + lead: + one: "Usunięta zostanie też grupa prowadzona przez to konto:" + few: "Usunięte zostaną też grupy prowadzone przez to konto:" + many: "Usunięte zostaną też grupy prowadzone przez to konto:" + other: "Usunięte zostaną też grupy prowadzone przez to konto:" + rest: "— ze wszystkimi studentami, walutą, odznakami i arkuszami ocen." + + form: + new_title: "Nowa organizacja" + edit_title: "Edytuj organizację" + usos_note: "To organizacja powiązana z USOS: trafia do niej każdy, kto zaloguje się przez USOS. Nie ma limitu członków i nie można jej usunąć." + name: "Nazwa" + max_members: "Limit członków" + create: "Utwórz organizację" + save: "Zapisz" + + new: + page_title: "Nowa organizacja" + + edit: + page_title: "Edytuj organizację — %{name}" + + new_admin: + page_title: "Dodaj administratora organizacji" + + admin_form: + title: "Dodaj administratora" + hint: "Wyślemy zaproszenie na podany adres. Po pierwszym logowaniu administrator uzupełni swoje dane." + email: "Adres e-mail" + email_placeholder: "jan.nowak@example.com" + submit: "Wyślij zaproszenie" + + new_user: + page_title: "Dodaj użytkownika organizacji" + + user_form: + new_title: "Dodaj użytkownika" + edit_title: "Edytuj użytkownika" + hint: "Wyślemy zaproszenie na podany adres. Konto zajmie jedno miejsce w organizacji." + email: "Adres e-mail" + email_placeholder: "jan.nowak@example.com" + full_name: "Imię i nazwisko" + role: "Rola" + university_number: "Numer indeksu (opcjonalnie)" + usos_id: "ID USOS (opcjonalnie)" + submit_new: "Wyślij zaproszenie" + submit_edit: "Zapisz" + + users: + edit: + page_title: "Edytuj użytkownika — %{name}" + confirm_destroy: + page_title: "Usunąć konto %{name}?" + title: "Usunąć konto %{name}?" + body: "Konto zostanie usunięte na stałe, razem z członkostwem w grupach, walutą, odznakami i przedmiotami. Zwolni się jedno miejsce w organizacji." + remove: "Usuń konto" + + confirm_destroy: + page_title: "Usuń organizację %{name}" + title: "Usunąć organizację na zawsze?" + accounts: + one: "%{count} konto zostanie usunięte" + few: "%{count} konta zostaną usunięte" + many: "%{count} kont zostanie usuniętych" + other: "%{count} konta zostanie usuniętych" + accounts_tail: "razem z organizacją, w tym konta administratorów." + confirm_label: "Wpisz nazwę organizacji, żeby potwierdzić:" + mismatch: "Wpisana nazwa nie zgadza się z nazwą organizacji. Organizacja nie została usunięta." + remove: "Usuń organizację" + + dialog_actions: + cancel: "Anuluj" + + account_setups: + flash: + done: "Pomyślnie skonfigurowano konto." + edit: + page_title: "Konfiguracja konta" + title: "Uzupełnij swoje dane" + lead: "To jednorazowy krok. Imię i nazwisko zobaczą prowadzący w Twoich grupach." + email: "Adres e-mail" + full_name: "Imię i nazwisko" + full_name_placeholder: "Jan Nowak" + submit: "Zapisz i przejdź dalej" + logout: "Wyloguj" + + activerecord: + errors: + models: + organization: + attributes: + max_members: + below_members: "Limit nie może być mniejszy niż obecna liczba członków (%{count})." + usos_undeletable: "Organizacji powiązanej z USOS nie można usunąć." + user: + attributes: + organization: + member_limit: "Organizacja osiągnęła limit członków." diff --git a/config/locales/views/passwordless_mailer.en.yml b/config/locales/views/passwordless_mailer.en.yml new file mode 100644 index 00000000..065c952d --- /dev/null +++ b/config/locales/views/passwordless_mailer.en.yml @@ -0,0 +1,12 @@ +en: + passwordless_mailer: + token_email: + subject: "Logging in to Gamification Hub" + title: "Log in" + greeting: "Hi, %{name}!" + body: "Click the button below to log in. No password needed." + text_body: "To log in, open the link below. No password needed." + button: "Log in" + valid_for: "The link is valid for %{minutes}." + fallback: "The link is valid for %{minutes}. If the button doesn't work, copy this address into your browser:" + ignore: "If it wasn't you trying to log in, you can ignore this message." diff --git a/config/locales/views/passwordless_mailer.pl.yml b/config/locales/views/passwordless_mailer.pl.yml new file mode 100644 index 00000000..05f04bd1 --- /dev/null +++ b/config/locales/views/passwordless_mailer.pl.yml @@ -0,0 +1,12 @@ +pl: + passwordless_mailer: + token_email: + subject: "Logowanie do systemu Gamification Hub" + title: "Zaloguj się" + greeting: "Cześć, %{name}!" + body: "Kliknij przycisk poniżej, aby zalogować się. Hasło nie jest potrzebne." + text_body: "Aby zalogować się, otwórz poniższy link. Hasło nie jest potrzebne." + button: "Zaloguj się" + valid_for: "Link jest ważny przez %{minutes}." + fallback: "Link jest ważny przez %{minutes}. Jeśli przycisk nie działa, skopiuj ten adres do przeglądarki:" + ignore: "Jeśli to nie Ty próbowałeś się zalogować, możesz zignorować tę wiadomość." diff --git a/config/locales/views/ranking.en.yml b/config/locales/views/ranking.en.yml new file mode 100644 index 00000000..458d706b --- /dev/null +++ b/config/locales/views/ranking.en.yml @@ -0,0 +1,73 @@ +en: + ranking: + flash: + hidden: "The ranking is hidden from students." + full: "The full ranking is visible." + podium: "The ranking is visible: podium and own place." + + show: + page_title: "Ranking - %{group}" + + confirm_mode: + page_title: "Ranking - %{mode}" + full_title: "Show the full ranking?" + full_body: "Every student will see everyone's places and nicknames, including those at the bottom of the list." + podium_title: "Limit the ranking to the podium?" + podium_body: "Students will keep the top 3 places and their own. Nobody will see who is at the bottom of the list." + keep_podium: "Keep the podium" + show_full: "Show the full ranking" + show_podium: "Show only the podium" + + confirm_visibility: + show_page_title: "Show ranking" + hide_page_title: "Hide ranking" + show_title: "Show the ranking to students?" + show_body: "Students will see the ranking right away. Choose how much they see." + modes_label: "What students see" + podium_body: "Everyone sees the top 3 places. Each student also sees their own place, but not anyone else's." + full_body: "Everyone sees everyone's places and nicknames, including at the bottom of the list." + show: "Show ranking" + hide_title: "Hide the ranking from students?" + hide_body: "Students will stop seeing places and nicknames. You still see the whole ranking, and collected currency and ranks don't change." + keep_visible: "Keep it visible" + hide: "Hide ranking" + + board: + table_label: "Ranking" + place: "Place" + nickname_and_student: "Nickname and student" + nickname: "Nickname" + rank: "Rank" + collected: "Collected" + hidden_rows: "The other places are hidden" + place_label: "Place %{place}" + you: "You" + no_nickname: "No nickname" + + teacher_board: + title: "Ranking" + lead: "Ordered by total currency collected (%{currency}). Students only see nicknames; you also see full names." + visible: "Visible to students" + modes_label: "What students see" + hidden_note: "The ranking is hidden from students." + hidden_note_tail: "You can still see it." + podium_note: "Students only see the podium and their own place." + podium_note_tail: "Nobody will see who is at the bottom of the list. You see everyone." + footnote: "A tie gives the same place. Students without a nickname are shown by their full name." + empty_title: "Nobody has joined the group yet" + empty_body: "The ranking will build itself once students join and start collecting currency." + invites: "Invites" + + student_board: + title: "Ranking" + hidden_lead: "The teacher has hidden the ranking for now." + podium_lead: "The teacher only shows the podium. Only you can see your own place." + full_lead: "Ordered by total currency collected (%{currency})." + own_place: "#%{place}" + own_place_label: "Your place out of %{count}" + collected: "Collected in total" + nickname: "Your nickname in this group:" + no_nickname: "none" + change: "Change" + hidden_title: "The ranking is hidden right now" + hidden_body: "Keep collecting currency. Once the teacher turns the ranking on, you'll see your place here." diff --git a/config/locales/views/ranking.pl.yml b/config/locales/views/ranking.pl.yml new file mode 100644 index 00000000..026efa95 --- /dev/null +++ b/config/locales/views/ranking.pl.yml @@ -0,0 +1,73 @@ +pl: + ranking: + flash: + hidden: "Ranking ukryty przed studentami." + full: "Ranking widoczny w całości." + podium: "Ranking widoczny: podium i własne miejsce." + + show: + page_title: "Ranking - %{group}" + + confirm_mode: + page_title: "Ranking - %{mode}" + full_title: "Pokazać pełny ranking?" + full_body: "Każdy student zobaczy miejsca i pseudonimy wszystkich, także osób na końcu listy." + podium_title: "Ograniczyć ranking do podium?" + podium_body: "Studentom zostaną pierwsze 3 miejsca i ich własne. Nikt nie zobaczy, kto jest na końcu listy." + keep_podium: "Zostaw podium" + show_full: "Pokaż pełny ranking" + show_podium: "Pokaż tylko podium" + + confirm_visibility: + show_page_title: "Pokaż ranking" + hide_page_title: "Ukryj ranking" + show_title: "Pokazać ranking studentom?" + show_body: "Studenci zobaczą ranking od razu. Wybierz, ile mają widzieć." + modes_label: "Co widzą studenci" + podium_body: "Wszyscy widzą pierwsze 3 miejsca. Każdy widzi też swoje miejsce, ale nie cudze." + full_body: "Wszyscy widzą miejsca i pseudonimy wszystkich, także na końcu listy." + show: "Pokaż ranking" + hide_title: "Ukryć ranking przed studentami?" + hide_body: "Studenci przestaną widzieć miejsca i pseudonimy. Ty nadal widzisz cały ranking, a zebrana waluta i rangi nie zmieniają się." + keep_visible: "Zostaw widoczny" + hide: "Ukryj ranking" + + board: + table_label: "Ranking" + place: "Miejsce" + nickname_and_student: "Pseudonim i student" + nickname: "Pseudonim" + rank: "Ranga" + collected: "Zebrane" + hidden_rows: "Pozostałe miejsca są ukryte" + place_label: "Miejsce %{place}" + you: "Ty" + no_nickname: "Bez pseudonimu" + + teacher_board: + title: "Ranking" + lead: "Kolejność według łącznie zebranej waluty (%{currency}). Studenci widzą tylko pseudonimy, Ty widzisz też imiona i nazwiska." + visible: "Widoczny dla studentów" + modes_label: "Co widzą studenci" + hidden_note: "Ranking jest ukryty przed studentami." + hidden_note_tail: "Ty nadal go widzisz." + podium_note: "Studenci widzą tylko podium i swoje miejsce." + podium_note_tail: "Nikt nie zobaczy, kto jest na końcu listy. Ty widzisz wszystkich." + footnote: "Remis daje to samo miejsce. Studenci bez pseudonimu są widoczni pod imieniem i nazwiskiem." + empty_title: "Nikt jeszcze nie dołączył do grupy" + empty_body: "Ranking ułoży się sam, gdy studenci dołączą i zaczną zbierać walutę." + invites: "Zaproszenia" + + student_board: + title: "Ranking" + hidden_lead: "Prowadzący ukrył ranking na razie." + podium_lead: "Prowadzący pokazuje tylko podium. Swoje miejsce widzisz tylko Ty." + full_lead: "Kolejność według łącznie zebranej waluty (%{currency})." + own_place: "%{place}." + own_place_label: "Twoje miejsce z %{count}" + collected: "Zebrane łącznie" + nickname: "Twój pseudonim w tej grupie:" + no_nickname: "brak" + change: "Zmień" + hidden_title: "Ranking jest teraz ukryty" + hidden_body: "Zbieraj dalej walutę. Gdy prowadzący włączy ranking, zobaczysz tu swoje miejsce." diff --git a/config/locales/views/ranks.en.yml b/config/locales/views/ranks.en.yml new file mode 100644 index 00000000..33c28c8c --- /dev/null +++ b/config/locales/views/ranks.en.yml @@ -0,0 +1,125 @@ +en: + ranks: + flash: + created: "Added the “%{name}” rank." + updated: "Saved the “%{name}” rank." + destroyed: "Deleted the “%{name}” rank." + blocked: "Can't delete the “%{name}” rank: the items %{items} require it." + + # RanksHelper; rank_form_controller.ts gets the same keys through the form. + helpers: + starting: "Starting rank" + starting_sentence: "Starting rank." + from: "From %{min} collected" + from_sentence: "From %{min} collected." + summary_no_discount: "%{opening}, no discount" + summary_discount: "%{opening}, −%{discount}% in the shop" + gives_discount: "Gives −%{discount}% in the shop." + discount: "−%{discount}% in the shop" + no_discount: "No discount" + required: "Required: %{min} collected." + name_placeholder: "Rank name" + new_rank: "New rank" + from_short: "from %{min}" + + index: + title: "Ranks" + + new: + page_title: "New rank" + + edit: + page_title: "Edit rank - %{name}" + + teacher_ladder: + title: "Ranks" + lead: + one: "%{count} rank. A rank depends on the total currency collected, so spending it doesn't lower the rank." + other: "%{count} ranks. A rank depends on the total currency collected, so spending it doesn't lower the rank." + new: "New rank" + gap: "↑ %{count} collected to promote" + required_by: "Required to buy: %{items}" + holders: + one: "student" + other: "students" + progress_label: "Students with the %{name} rank" + empty_title: "There are no ranks yet" + empty_body: "Add ranks so students can see what they are working towards. A rank depends on the total currency collected, and each can give a discount in the shop." + + student_ladder: + title: "Ranks" + lead: "Your rank grows with the total currency you collect. Spending it doesn't lower your rank." + yours: "Your rank" + achieved: "Earned" + to_first: "%{collected} of %{needed} to the first rank" + to_next: "%{collected} of %{needed} to the next one" + progress_label: "Progress to the rank above %{name}" + top: "Highest rank" + missing: "%{count} to go" + empty_title: "This group has no ranks yet" + empty_body: "The teacher hasn't added any. Keep collecting currency — once ranks appear, everything you've collected so far will count." + + preview: + summary: "Rank preview" + card: "Rank card" + ladder: "Rank ladder" + this_rank: "(this rank)" + + form: + breadcrumb: "Ranks" + new_title: "New rank" + edit_title: "Edit rank" + lead: "The preview shows where the rank lands on the ladder and how many students it covers." + holders: + one: "%{count} student has this rank." + other: "%{count} students have this rank." + holders_note: "Changing the threshold may move students up or down. The preview shows the effect." + card: "Rank card" + card_hint: "This is how the rank looks on the group page and in the ranking." + name_placeholder: "e.g. Fleet Admiral" + threshold_section: "Threshold and reward" + threshold_hint: "A rank depends on the total currency collected, so spending it doesn't lower the rank." + threshold: "Collected required" + discount: "Shop discount" + removal_hint: "Students with this rank will drop to the rank below. Items that require it must first be assigned to another rank." + remove: "Delete rank" + create: "Add rank" + # rank_form_controller.ts + js: + impact_got_lead: "Right away it goes to " + impact_got: + one: "%{count} student" + other: "%{count} students" + impact_got_tail: ". Everyone else will see it as their next goal." + impact_nobody: "Nobody has collected that much yet. Students will see it as their next goal." + impact_unchanged: "No student will change rank." + impact_changed_lead: "After saving, " + impact_changed: + one: "%{count} student will change rank" + other: "%{count} students will change rank" + impact_up: "%{count} promoted" + impact_down: "%{count} demoted" + warn_below: "The rank below (%{name}) gives −%{other}%, but this one only −%{discount}%. A promotion would lower the discount." + warn_above: "The rank above (%{name}) only gives −%{other}%. A promotion to it would lower the discount." + + confirm_destroy: + page_title: "Delete rank %{name}" + title: "Delete the “%{name}” rank?" + body: "Students with this rank will drop to the rank below." + blocked: + one: "The item %{items} requires this rank. Before deleting it, assign that item another rank." + other: "The items %{items} require this rank. Before deleting it, assign those items another rank." + remove: "Delete rank" + + activerecord: + errors: + models: + rank: + attributes: + name: + blank: "Enter the rank name." + discount: + out_of_range: "The discount must be between 0 and 100%." + required_currency_value: + negative: "The threshold can't be negative." + threshold_taken: "The %{rank} rank already has the threshold %{threshold}. Choose another one." diff --git a/config/locales/views/ranks.pl.yml b/config/locales/views/ranks.pl.yml new file mode 100644 index 00000000..bd561b7d --- /dev/null +++ b/config/locales/views/ranks.pl.yml @@ -0,0 +1,135 @@ +pl: + ranks: + flash: + created: "Dodano rangę „%{name}”." + updated: "Zapisano rangę „%{name}”." + destroyed: "Usunięto rangę „%{name}”." + blocked: "Nie można usunąć rangi „%{name}”: wymagają jej przedmioty %{items}." + + # RanksHelper; rank_form_controller.ts gets the same keys through the form. + helpers: + starting: "Ranga startowa" + starting_sentence: "Ranga startowa." + from: "Od %{min} zebranych" + from_sentence: "Od %{min} zebranych." + summary_no_discount: "%{opening}, bez zniżki" + summary_discount: "%{opening}, −%{discount}% w sklepie" + gives_discount: "Daje −%{discount}% w sklepie." + discount: "−%{discount}% w sklepie" + no_discount: "Bez zniżki" + required: "Wymagane: %{min} zebranych." + name_placeholder: "Nazwa rangi" + new_rank: "Nowa ranga" + from_short: "od %{min}" + + index: + title: "Rangi" + + new: + page_title: "Nowa ranga" + + edit: + page_title: "Edytuj rangę - %{name}" + + teacher_ladder: + title: "Rangi" + lead: + one: "%{count} ranga. Ranga zależy od łącznie zebranej waluty, więc wydawanie jej nie obniża rangi." + few: "%{count} rangi. Ranga zależy od łącznie zebranej waluty, więc wydawanie jej nie obniża rangi." + many: "%{count} rang. Ranga zależy od łącznie zebranej waluty, więc wydawanie jej nie obniża rangi." + other: "%{count} rangi. Ranga zależy od łącznie zebranej waluty, więc wydawanie jej nie obniża rangi." + new: "Nowa ranga" + gap: "↑ %{count} zebranych do awansu" + required_by: "Wymagana do zakupu: %{items}" + holders: + one: "student" + few: "studentów" + many: "studentów" + other: "studenta" + progress_label: "Studenci z rangą %{name}" + empty_title: "Nie ma jeszcze żadnych rang" + empty_body: "Dodaj rangi, żeby studenci widzieli, do czego dążą. Ranga zależy od łącznie zebranej waluty, a każda może dawać zniżkę w sklepie." + + student_ladder: + title: "Rangi" + lead: "Ranga rośnie razem z łącznie zebraną walutą. Wydawanie jej nie obniża rangi." + yours: "Twoja ranga" + achieved: "Zdobyta" + to_first: "%{collected} z %{needed} do pierwszej rangi" + to_next: "%{collected} z %{needed} do następnej" + progress_label: "Postęp do rangi wyżej niż %{name}" + top: "Najwyższa ranga" + missing: "Brakuje %{count}" + empty_title: "W tej grupie nie ma jeszcze rang" + empty_body: "Prowadzący ich nie dodał. Zbieraj walutę — kiedy rangi się pojawią, zaliczy się wszystko, co zebrałeś do tej pory." + + preview: + summary: "Podgląd rangi" + card: "Karta rangi" + ladder: "Drabinka rang" + this_rank: "(ta ranga)" + + form: + breadcrumb: "Rangi" + new_title: "Nowa ranga" + edit_title: "Edytuj rangę" + lead: "Podgląd obok pokazuje, gdzie ranga trafi na drabince i ilu studentów obejmie." + holders: + one: "%{count} student ma tę rangę." + few: "%{count} studentów ma tę rangę." + many: "%{count} studentów ma tę rangę." + other: "%{count} studenta ma tę rangę." + holders_note: "Zmiana progu może przesunąć studentów w górę lub w dół. Podgląd obok pokazuje skutki." + card: "Karta rangi" + card_hint: "Tak ranga wygląda na stronie grupy i w rankingu." + name_placeholder: "np. Admirał Floty" + threshold_section: "Próg i nagroda" + threshold_hint: "Ranga zależy od łącznie zebranej waluty, więc wydawanie jej nie obniża rangi." + threshold: "Wymagane zebrane" + discount: "Zniżka w sklepie" + removal_hint: "Studenci z tą rangą spadną do rangi niżej. Przedmioty, które jej wymagają, trzeba będzie najpierw przypisać do innej rangi." + remove: "Usuń rangę" + create: "Dodaj rangę" + # rank_form_controller.ts + js: + impact_got_lead: "Od razu dostanie ją " + impact_got: + one: "%{count} student" + few: "%{count} studentów" + many: "%{count} studentów" + other: "%{count} studenta" + impact_got_tail: ". Pozostali zobaczą ją jako kolejny cel." + impact_nobody: "Na razie nikt nie ma tylu zebranych. Studenci zobaczą ją jako kolejny cel." + impact_unchanged: "Żaden student nie zmieni rangi." + impact_changed_lead: "Po zapisaniu " + impact_changed: + one: "%{count} student zmieni rangę" + few: "%{count} studentów zmieni rangę" + many: "%{count} studentów zmieni rangę" + other: "%{count} studenta zmieni rangę" + impact_up: "%{count} awansuje" + impact_down: "%{count} spadnie" + warn_below: "Ranga niżej (%{name}) daje −%{other}%, a ta tylko −%{discount}%. Awans obniżyłby zniżkę." + warn_above: "Ranga wyżej (%{name}) daje tylko −%{other}%. Awans na nią obniżyłby zniżkę." + + confirm_destroy: + page_title: "Usuń rangę %{name}" + title: "Usunąć rangę „%{name}”?" + body: "Studenci z tą rangą spadną do rangi niżej." + blocked: + one: "Przedmiot %{items} wymaga tej rangi. Zanim ją usuniesz, przypisz mu inną rangę." + other: "Przedmioty %{items} wymagają tej rangi. Zanim ją usuniesz, przypisz im inną rangę." + remove: "Usuń rangę" + + activerecord: + errors: + models: + rank: + attributes: + name: + blank: "Podaj nazwę rangi." + discount: + out_of_range: "Zniżka musi mieścić się między 0 a 100%." + required_currency_value: + negative: "Próg nie może być ujemny." + threshold_taken: "Ranga %{rank} ma już próg %{threshold}. Wybierz inny." diff --git a/config/locales/views/sessions.en.yml b/config/locales/views/sessions.en.yml new file mode 100644 index 00000000..7c5793c1 --- /dev/null +++ b/config/locales/views/sessions.en.yml @@ -0,0 +1,10 @@ +en: + sessions: + new: + title: "Log in" + lead: "Choose the way you use at your university or school." + usos: "Log in with USOS" + usos_hint: "University students and teachers" + or: "or" + email: "Log in with e-mail" + email_hint: "School pupils and people added by an administrator" diff --git a/config/locales/views/sessions.pl.yml b/config/locales/views/sessions.pl.yml new file mode 100644 index 00000000..a87dce14 --- /dev/null +++ b/config/locales/views/sessions.pl.yml @@ -0,0 +1,10 @@ +pl: + sessions: + new: + title: "Zaloguj się" + lead: "Wybierz sposób, z którego korzystasz na swojej uczelni lub w szkole." + usos: "Zaloguj się przez USOS" + usos_hint: "Studenci i nauczyciele uczelni" + or: "albo" + email: "Zaloguj się e-mailem" + email_hint: "Uczniowie szkół i osoby dodane przez administratora" diff --git a/config/locales/views/shared.en.yml b/config/locales/views/shared.en.yml new file mode 100644 index 00000000..1325562b --- /dev/null +++ b/config/locales/views/shared.en.yml @@ -0,0 +1,108 @@ +en: + shared: + # Generic buttons and labels used across screens. + actions: + add: "Add" + back_to_editing: "Back to editing" + cancel: "Cancel" + close: "Close" + edit: "Edit" + fewer: "Fewer" + more: "More" + name: "Name" + next: "Next" + optional: "(optional)" + preview: "Preview" + removal: "Deleting" + remove: "Remove" + save: "Save" + save_changes: "Save changes" + # shared/_sheet_form — template (new, edit) and sheet settings. + sheet_form: + template_new_title: "New sheet template" + template_edit_title: "Edit sheet template" + sheet_title: "Sheet settings" + breadcrumb: "Grade sheets" + new_template: "New template" + sheet_lead: "Changes only apply to this sheet." + template_lead: "The preview shows the grade sheet header." + template_changes: "Changes only apply to sheets created from now on." + sheets_range_unchanged: "%{first} – %{last} stay unchanged." + sheet_unchanged: "%{name} stays unchanged." + awarded: + one: "%{count} reward has already been awarded in this sheet." + other: "%{count} rewards have already been awarded in this sheet." + awarded_note: "Column changes only apply to future rewards. The template %{name} doesn't change." + sheet_name: "Sheet name" + template_name: "Template name" + sheet_name_placeholder: "e.g. Lab 5 (14.10)" + template_name_placeholder: "e.g. Lab" + name_example: "Lab" + template_name_hint: "New sheets get the template name with the next number, e.g. %{example} 6." + sheet_columns: "Grade sheet columns" + template_categories: "Reward categories" + columns_hint: "Each category is a column in the grade sheet. The order here is the column order." + add_column: "Add column" + add_category: "Add category" + summary: "%{count} in the grade sheet. A student can earn up to %{max} %{currency} per sheet." + sheet_removal_hint: "The sheet will disappear from the list, and awarded rewards stay with the students and in their history." + template_removal_hint: "The template will disappear from the list, and sheets created from it stay." + remove_sheet: "Delete sheet" + remove_template: "Delete template" + no_changes: "No changes" + create: "Create template" + discard_title: "Discard changes?" + discard_body: "Unsaved changes will be lost." + discard: "Discard changes" + sample_names: ["Alex Johnson", "Barbara Miller"] + # sheet_form_controller.ts + js: + columns: + one: "%{count} column" + other: "%{count} columns" + dirty: "Unsaved changes" + clean: "No changes" + name_label: "Category %{number}: for what" + reward_label: "Reward for category %{number}" + story_label: "Story description of category %{number}" + destroy_column: "Remove column %{number}" + destroy_category: "Remove category %{number}" + + category_row: + drag: "Drag to reorder" + name_placeholder: "For what, e.g. Helping others" + add_story: "Add a story description" + story_placeholder: "Story description, e.g. Towing a damaged ship" + sheet_only: "Only in this sheet" + hidden_column: "Hidden in the grade sheet" + awarded: + one: "%{count} reward awarded." + other: "%{count} rewards awarded." + locked_note: "The column can't be removed, but it can be hidden." + reward_note: "The new value only applies to future rewards." + move_up: "Move up" + move_down: "Move down" + show: "Show in the grade sheet" + hide: "Hide in the grade sheet" + + grid_preview: + summary: "Grade sheet preview" + header: "Grade sheet header" + student: "Student" + hint: "Hover over a header in the real sheet to see the story description." + + # shared/_image_field — the defaults, overridden per form. + image_field: + label: "Image" + presets: "Ready-made images" + own: "Your image" + upload: "Or upload your own image" + hint: "JPG or PNG, ideally at least 400 px." + crop: "Crop the image" + cropped: "Move and resize the selection — the preview updates as you go. We save a card-shaped crop, so it fills the card completely. In lists you'll see it as a square thumbnail, also in full." + preset_label: "%{label}: %{name}" + choose_file: "Choose file" + + register_prompt: + no_account: "Don't have an account?" + register: "Sign up" diff --git a/config/locales/views/shared.pl.yml b/config/locales/views/shared.pl.yml new file mode 100644 index 00000000..f7668f8d --- /dev/null +++ b/config/locales/views/shared.pl.yml @@ -0,0 +1,114 @@ +pl: + shared: + # Generic buttons and labels used across screens. + actions: + add: "Dodaj" + back_to_editing: "Wróć do edycji" + cancel: "Anuluj" + close: "Zamknij" + edit: "Edytuj" + fewer: "Mniej" + more: "Więcej" + name: "Nazwa" + next: "Dalej" + optional: "(opcjonalnie)" + preview: "Podgląd" + removal: "Usuwanie" + remove: "Usuń" + save: "Zapisz" + save_changes: "Zapisz zmiany" + # shared/_sheet_form — template (new, edit) and sheet settings. + sheet_form: + template_new_title: "Nowy szablon arkusza" + template_edit_title: "Edytuj szablon arkusza" + sheet_title: "Ustawienia arkusza" + breadcrumb: "Arkusze ocen" + new_template: "Nowy szablon" + sheet_lead: "Zmiany dotyczą tylko tego arkusza." + template_lead: "Podgląd obok pokazuje nagłówek tabeli ocen." + template_changes: "Zmiany dotyczą tylko arkuszy utworzonych od teraz." + sheets_range_unchanged: "%{first} – %{last} zostają bez zmian." + sheet_unchanged: "%{name} zostaje bez zmian." + awarded: + one: "W tym arkuszu przyznano już %{count} nagrodę." + few: "W tym arkuszu przyznano już %{count} nagrody." + many: "W tym arkuszu przyznano już %{count} nagród." + other: "W tym arkuszu przyznano już %{count} nagrody." + awarded_note: "Zmiany kolumn dotyczą tylko przyszłych nagród. Szablon %{name} się nie zmienia." + sheet_name: "Nazwa arkusza" + template_name: "Nazwa szablonu" + sheet_name_placeholder: "np. Laboratoria 5 (14.10)" + template_name_placeholder: "np. Laboratoria" + name_example: "Laboratoria" + template_name_hint: "Nowe arkusze dostaną nazwę szablonu z kolejnym numerem, np. %{example} 6." + sheet_columns: "Kolumny w tabeli ocen" + template_categories: "Kategorie nagród" + columns_hint: "Każda kategoria to kolumna w tabeli ocen. Kolejność tutaj to kolejność kolumn." + add_column: "Dodaj kolumnę" + add_category: "Dodaj kategorię" + summary: "%{count} w tabeli ocen. Student może zdobyć do %{max} %{currency} za arkusz." + sheet_removal_hint: "Arkusz zniknie z listy, a przyznane nagrody zostają u studentów i w ich historii." + template_removal_hint: "Szablon zniknie z listy, a utworzone z niego arkusze zostają." + remove_sheet: "Usuń arkusz" + remove_template: "Usuń szablon" + no_changes: "Brak zmian" + create: "Utwórz szablon" + discard_title: "Odrzucić zmiany?" + discard_body: "Niezapisane zmiany zostaną utracone." + discard: "Odrzuć zmiany" + sample_names: ["Adam Pawłowski", "Barbara Kowalewska"] + # sheet_form_controller.ts + js: + columns: + one: "%{count} kolumna" + few: "%{count} kolumny" + many: "%{count} kolumn" + other: "%{count} kolumny" + dirty: "Niezapisane zmiany" + clean: "Brak zmian" + name_label: "Kategoria %{number}: za co" + reward_label: "Nagroda za kategorię %{number}" + story_label: "Opis fabularny kategorii %{number}" + destroy_column: "Usuń kolumnę %{number}" + destroy_category: "Usuń kategorię %{number}" + + category_row: + drag: "Przeciągnij, aby zmienić kolejność" + name_placeholder: "Za co, np. Pomoc innym" + add_story: "Dodaj opis fabularny" + story_placeholder: "Opis fabularny, np. Holowanie uszkodzonego statku" + sheet_only: "Tylko w tym arkuszu" + hidden_column: "Ukryta w tabeli ocen" + awarded: + one: "Przyznano %{count} nagrodę." + few: "Przyznano %{count} nagrody." + many: "Przyznano %{count} nagród." + other: "Przyznano %{count} nagrody." + locked_note: "Kolumny nie można usunąć, można ją ukryć." + reward_note: "Nowa wartość obejmie tylko przyszłe nagrody." + move_up: "Przesuń wyżej" + move_down: "Przesuń niżej" + show: "Pokaż w tabeli ocen" + hide: "Ukryj w tabeli ocen" + + grid_preview: + summary: "Podgląd tabeli ocen" + header: "Nagłówek tabeli ocen" + student: "Student" + hint: "Najedź na nagłówek w prawdziwej tabeli, żeby zobaczyć opis fabularny." + + # shared/_image_field — the defaults, overridden per form. + image_field: + label: "Grafika" + presets: "Gotowe grafiki" + own: "Twoja grafika" + upload: "Albo wgraj własną grafikę" + hint: "JPG lub PNG, najlepiej co najmniej 400 px." + crop: "Przytnij grafikę" + cropped: "Przesuń i przeskaluj zaznaczenie — podgląd obok zmienia się na bieżąco. Zapisujemy kadr w kształcie karty, więc wypełni ją w całości. Na listach zobaczysz go w kwadratowej miniaturze, też w całości." + preset_label: "%{label}: %{name}" + choose_file: "Wybierz plik" + + register_prompt: + no_account: "Nie masz konta?" + register: "Zarejestruj się" diff --git a/config/locales/views/shop.en.yml b/config/locales/views/shop.en.yml new file mode 100644 index 00000000..3a0d2ce6 --- /dev/null +++ b/config/locales/views/shop.en.yml @@ -0,0 +1,45 @@ +en: + shop: + flash: + unavailable: "You can't buy “%{name}” right now." + bought: "Bought: “%{name}”. You have %{balance} left." + + # Shop::ZONES + zones: + afford: "You can afford now" + save: "Saving up for" + sealed: "Sealed" + + # PurchaseEligibilityService and ItemPurchaseService. + errors: + no_lives: "You need at least one life." + rank_required: "Required rank: %{rank}." + badges_missing: "Missing badges: %{badges}." + not_enough: "You don't have enough currency to buy this item." + failed: "Something went wrong with the purchase: %{message}" + + helpers: + balance_html: "You have %{balance} to spend." + discount_badges_html: + one: "Your %{badges} badge lowers the price of some items." + other: "Your badges %{badges} lower the price of some items." + + index: + title: "Shop" + to_spend: "to spend" + collected: "collected in total" + empty_title: "The shop is still empty" + empty_body: "The teacher hasn't added any items yet. Keep collecting currency — it will come in handy once the shop opens." + + buy_button: + label: "Buy %{name} for %{price}" + buy: "Buy for %{price}" + + confirm_buy: + page_title: "Buy %{name}" + title: "Buy “%{name}”?" + price: "Price" + left: "You'll have left" + discount: "Discount %{label}." + notice: "The teacher will be notified about the purchase." + buy: "Buy for %{price}" diff --git a/config/locales/views/shop.pl.yml b/config/locales/views/shop.pl.yml new file mode 100644 index 00000000..96b3933d --- /dev/null +++ b/config/locales/views/shop.pl.yml @@ -0,0 +1,47 @@ +pl: + shop: + flash: + unavailable: "Nie możesz teraz kupić „%{name}”." + bought: "Kupione: „%{name}”. Zostało Ci %{balance}." + + # Shop::ZONES + zones: + afford: "Stać cię teraz" + save: "Zbierasz na to" + sealed: "Zapieczętowane" + + # PurchaseEligibilityService and ItemPurchaseService. + errors: + no_lives: "Wymagane jest posiadanie przynajmniej jednego życia." + rank_required: "Wymagana ranga: %{rank}." + badges_missing: "Brakujące odznaki: %{badges}." + not_enough: "Masz za mało waluty, aby kupić ten przedmiot." + failed: "Wystąpił błąd podczas zakupu: %{message}" + + helpers: + balance_html: "Masz %{balance} do wydania." + discount_badges_html: + one: "Twoja odznaka %{badges} obniża ceny niektórych przedmiotów." + few: "Twoje odznaki %{badges} obniżają ceny niektórych przedmiotów." + many: "Twoje odznaki %{badges} obniżają ceny niektórych przedmiotów." + other: "Twoje odznaki %{badges} obniżają ceny niektórych przedmiotów." + + index: + title: "Sklep" + to_spend: "do wydania" + collected: "zebrane łącznie" + empty_title: "Sklep jest jeszcze pusty" + empty_body: "Prowadzący nie dodał jeszcze przedmiotów. Zbieraj walutę — przyda się, kiedy sklep się otworzy." + + buy_button: + label: "Kup %{name} za %{price}" + buy: "Kup za %{price}" + + confirm_buy: + page_title: "Kup %{name}" + title: "Kupić „%{name}”?" + price: "Cena" + left: "Zostanie Ci" + discount: "Zniżka %{label}." + notice: "Prowadzący dostanie powiadomienie o zakupie." + buy: "Kup za %{price}" diff --git a/config/locales/views/story_group_invites.en.yml b/config/locales/views/story_group_invites.en.yml new file mode 100644 index 00000000..fab933b0 --- /dev/null +++ b/config/locales/views/story_group_invites.en.yml @@ -0,0 +1,126 @@ +en: + story_group_invites: + flash: + created: "Created the code %{code}. Click “Show” to display it to students." + updated: "Saved the invite %{code}." + destroyed: "Deleted the invite %{code}." + + # StoryGroupInvite#status_label + statuses: + active: "Active" + expired: "Expired" + exhausted: "Used up" + + helpers: + uses_unlimited: "%{uses}, no limit" + uses_limited: "%{uses} of %{max}" + no_expiry: "No expiry date" + expires: "Expires %{date}" + expired: "Expired %{date}" + stat_sentence: "Uses: %{uses}. %{expiry}." + code_label: "Code: %{code}" + option: "%{code} – %{uses}, %{validity}" + option_unlimited: "no limit" + option_no_expiry: "no expiry" + option_valid_until: "valid until %{date}" + + # InviteForm and invite_form_controller.ts. + summary: + sentence: "The code will work %{limit} %{expiry}." + limit: + one: "for %{count} person" + other: "for %{count} people" + no_limit: "with no limit of people" + expiry: "until %{date}" + no_expiry: "and with no expiry date" + unit: + one: "person" + other: "people" + # %{day} %{month} two digits, %{dayNumber} without padding, %{monthName} short. + stamp: "%{monthName} %{dayNumber}, %{time}" + + index: + title: "Invites" + lead: "The code has 6 characters with no confusable letters (no O, 0, I, 1 or L). Students type it into “Join a group” or scan the QR code." + new: "New invite" + code: "Code" + status: "Status" + uses: "Uses" + validity: "Validity" + empty: "No active invites. Create a new one so students can join." + inactive: "Inactive (%{count}): expired and used up" + + invite_row: + uses: "Uses: %{uses}" + usage_label: "Usage of code %{code}" + show: "Show" + edit_label: "Edit invite %{code}" + remove: "Delete" + remove_label: "Delete invite %{code}" + + copy_button: + title: "Copy code %{code}" + done: "Copied" + message: "Copied the code %{code}." + label: "Copy code" + + + code: + title: "Join the group %{name}" + hint: "Type the code into the app (“Join a group”) or scan the QR code." + + quick: + page_title: "Code for students" + title: "Join the group %{name}" + hint: "Type the code into the app (“Join a group”) or scan the QR code." + picker: "Invite (active: %{count}, newest first)" + hidden_note: "Expired and used-up invites are hidden. You'll find all of them in the Invites tab." + empty: "No active invites. Create a new one so students can join." + new: "New invite" + manage: "Manage invites" + + show: + page_title: "Invite %{code}" + + new: + page_title: "New invite" + + edit: + page_title: "Edit invite %{code}" + + form: + edit_title: "Edit invite %{code}" + new_title: "New invite" + edit_hint: "The code stays the same. You only change the limits." + new_hint: "Both limits are optional." + today: "For today's class" + no_limits: "No limits" + limit: "Use limit" + limit_hint: "The code stops working once this many people have joined." + expiry: "Expiry date" + expiry_hint: "After this moment the code stops working." + date: "Date" + time: "Time" + create: "Create invite" + + confirm_destroy: + page_title: "Delete invite %{code}?" + title: "Delete invite %{code}?" + body: "The code will stop working right away." + joined: + one: "%{count} person who joined with this code stays in the group." + other: "%{count} people who joined with this code stay in the group." + remove: "Delete invite" + + activerecord: + errors: + models: + story_group_invite: + attributes: + max_uses: + too_low: "The limit must be at least 1." + below_uses: + one: "%{count} person has already used this code. The limit can't be lower." + other: "%{count} people have already used this code. The limit can't be lower." + expires_at: + past: "That date has already passed. Choose a later one." diff --git a/config/locales/views/story_group_invites.pl.yml b/config/locales/views/story_group_invites.pl.yml new file mode 100644 index 00000000..c1dc3918 --- /dev/null +++ b/config/locales/views/story_group_invites.pl.yml @@ -0,0 +1,134 @@ +pl: + story_group_invites: + flash: + created: "Utworzono kod %{code}. Kliknij „Pokaż”, żeby wyświetlić go studentom." + updated: "Zapisano zaproszenie %{code}." + destroyed: "Usunięto zaproszenie %{code}." + + # StoryGroupInvite#status_label + statuses: + active: "Aktywne" + expired: "Wygasło" + exhausted: "Wyczerpane" + + helpers: + uses_unlimited: "%{uses}, bez limitu" + uses_limited: "%{uses} z %{max}" + no_expiry: "Bez daty ważności" + expires: "Wygasa %{date}" + expired: "Wygasło %{date}" + stat_sentence: "Użycia: %{uses}. %{expiry}." + code_label: "Kod: %{code}" + option: "%{code} – %{uses}, %{validity}" + option_unlimited: "bez limitu" + option_no_expiry: "bezterminowo" + option_valid_until: "ważne do %{date}" + + # InviteForm and invite_form_controller.ts. + summary: + sentence: "Kod będzie działał %{limit} %{expiry}." + limit: + one: "dla %{count} osoby" + few: "dla %{count} osób" + many: "dla %{count} osób" + other: "dla %{count} osoby" + no_limit: "bez limitu osób" + expiry: "do %{date}" + no_expiry: "i bez daty ważności" + unit: + one: "osoba" + few: "osoby" + many: "osób" + other: "osoby" + # %{day} %{month} two digits, %{dayNumber} without padding, %{monthName} short. + stamp: "%{day}.%{month}, %{time}" + + index: + title: "Zaproszenia" + lead: "Kod ma 6 znaków bez mylących liter (bez O, 0, I, 1 i L). Studenci wpisują go w „Dołącz do grupy” albo skanują kod QR." + new: "Nowe zaproszenie" + code: "Kod" + status: "Status" + uses: "Użycia" + validity: "Ważność" + empty: "Brak aktywnych zaproszeń. Utwórz nowe, żeby studenci mogli dołączyć." + inactive: "Nieaktywne (%{count}): wygasłe i wyczerpane" + + invite_row: + uses: "Użycia: %{uses}" + usage_label: "Wykorzystanie kodu %{code}" + show: "Pokaż" + edit_label: "Edytuj zaproszenie %{code}" + remove: "Usuń" + remove_label: "Usuń zaproszenie %{code}" + + copy_button: + title: "Kopiuj kod %{code}" + done: "Skopiowano" + message: "Skopiowano kod %{code}." + label: "Kopiuj kod" + + + code: + title: "Dołącz do grupy %{name}" + hint: "Wpisz kod w aplikacji („Dołącz do grupy”) albo zeskanuj kod QR." + + quick: + page_title: "Kod dla studentów" + title: "Dołącz do grupy %{name}" + hint: "Wpisz kod w aplikacji („Dołącz do grupy”) albo zeskanuj kod QR." + picker: "Zaproszenie (aktywne: %{count}, najnowsze na górze)" + hidden_note: "Wygasłe i wyczerpane zaproszenia są ukryte. Wszystkie znajdziesz w zakładce Zaproszenia." + empty: "Brak aktywnych zaproszeń. Utwórz nowe, żeby studenci mogli dołączyć." + new: "Nowe zaproszenie" + manage: "Zarządzaj zaproszeniami" + + show: + page_title: "Zaproszenie %{code}" + + new: + page_title: "Nowe zaproszenie" + + edit: + page_title: "Edytuj zaproszenie %{code}" + + form: + edit_title: "Edytuj zaproszenie %{code}" + new_title: "Nowe zaproszenie" + edit_hint: "Kod zostaje ten sam. Zmieniasz tylko ograniczenia." + new_hint: "Oba ograniczenia są opcjonalne." + today: "Na dzisiejsze zajęcia" + no_limits: "Bez ograniczeń" + limit: "Limit użyć" + limit_hint: "Kod przestanie działać, gdy dołączy tyle osób." + expiry: "Data ważności" + expiry_hint: "Po tej chwili kod przestanie działać." + date: "Data" + time: "Godzina" + create: "Utwórz zaproszenie" + + confirm_destroy: + page_title: "Usunąć zaproszenie %{code}?" + title: "Usunąć zaproszenie %{code}?" + body: "Kod przestanie działać od razu." + joined: + one: "%{count} osoba, która dołączyła z tym kodem, zostaje w grupie." + few: "%{count} osoby, które dołączyły z tym kodem, zostają w grupie." + many: "%{count} osób, które dołączyły z tym kodem, zostaje w grupie." + other: "%{count} osoby, które dołączyły z tym kodem, zostają w grupie." + remove: "Usuń zaproszenie" + + activerecord: + errors: + models: + story_group_invite: + attributes: + max_uses: + too_low: "Limit musi wynosić co najmniej 1." + below_uses: + one: "Z tego kodu skorzystała już %{count} osoba. Limit nie może być mniejszy." + few: "Z tego kodu skorzystały już %{count} osoby. Limit nie może być mniejszy." + many: "Z tego kodu skorzystało już %{count} osób. Limit nie może być mniejszy." + other: "Z tego kodu skorzystało już %{count} osoby. Limit nie może być mniejszy." + expires_at: + past: "Ta data już minęła. Wybierz późniejszą." diff --git a/config/locales/views/story_group_memberships.en.yml b/config/locales/views/story_group_memberships.en.yml new file mode 100644 index 00000000..816f297c --- /dev/null +++ b/config/locales/views/story_group_memberships.en.yml @@ -0,0 +1,56 @@ +en: + story_group_memberships: + flash: + nickname: "Your nickname in this group: %{name}." + left: "You left the group %{name}." + + edit: + page_title: "Group settings - %{name}" + title: "Group settings" + lead: "These settings only apply to the group %{name}." + nickname: "Nickname" + nickname_hint: "Visible in the ranking only in this group. You can have a different one in other groups." + teacher_sees: "What the teacher sees" + teacher_sees_hint: "Other students only see your nickname, and only in the ranking." + full_name: "Full name" + email: "E-mail" + university_number: "Student number" + none: "None" + leave_title: "Leave the group" + leave_body: "You'll lose access to the group, and your currency, badges and items in it will be gone. To come back, you'll need a new code from your teacher." + leave: "Leave the group" + + nickname: + page_title: "Nickname in the group %{name}" + title: "Nickname in the group %{name}" + hint: "Visible in the ranking only in this group. You can have a different one in other groups. The teacher also sees your full name." + save: "Save nickname" + + nickname_field: + label: "Your nickname" + hint: "Up to 24 characters. Leave it empty to show your full name." + + confirm_leave: + page_title: "Leave the group %{name}" + title: "Leave the group %{name}?" + lose: "You'll lose access to the shop, the ranking and your history in this group: %{balance} %{currency}, %{badges} and %{items}." + badges: + one: "%{count} badge" + other: "%{count} badges" + items: + one: "%{count} item" + other: "%{count} items" + warning: "This can't be undone. If you come back to this group with a new code from your teacher, you'll start from scratch." + stay: "Stay in the group" + leave: "Leave the group" + + # StoryGroupStudent — the nickname is written both when joining and in the + # group settings. + activerecord: + errors: + models: + story_group_student: + attributes: + nickname: + length: "The nickname must be 2 to 24 characters long." + taken: "This nickname is already taken in this group." diff --git a/config/locales/views/story_group_memberships.pl.yml b/config/locales/views/story_group_memberships.pl.yml new file mode 100644 index 00000000..f3776c80 --- /dev/null +++ b/config/locales/views/story_group_memberships.pl.yml @@ -0,0 +1,60 @@ +pl: + story_group_memberships: + flash: + nickname: "Twój pseudonim w tej grupie: %{name}." + left: "Opuszczono grupę %{name}." + + edit: + page_title: "Ustawienia w grupie - %{name}" + title: "Ustawienia w grupie" + lead: "Te ustawienia dotyczą tylko grupy %{name}." + nickname: "Pseudonim" + nickname_hint: "Widoczny w rankingu tylko w tej grupie. W innych grupach możesz mieć inny." + teacher_sees: "Co widzi prowadzący" + teacher_sees_hint: "Pozostali studenci widzą tylko Twój pseudonim, i to tylko w rankingu." + full_name: "Imię i nazwisko" + email: "E-mail" + university_number: "Numer indeksu" + none: "Brak" + leave_title: "Opuść grupę" + leave_body: "Stracisz dostęp do grupy, a Twoja waluta, odznaki i przedmioty w niej przepadną. Żeby wrócić, potrzebujesz nowego kodu od prowadzącego." + leave: "Opuść grupę" + + nickname: + page_title: "Pseudonim w grupie %{name}" + title: "Pseudonim w grupie %{name}" + hint: "Widoczny w rankingu tylko w tej grupie. W innych grupach możesz mieć inny. Prowadzący widzi też Twoje imię i nazwisko." + save: "Zapisz pseudonim" + + nickname_field: + label: "Twój pseudonim" + hint: "Do 24 znaków. Zostaw puste, żeby pokazywać imię i nazwisko." + + confirm_leave: + page_title: "Opuść grupę %{name}" + title: "Opuścić grupę %{name}?" + lose: "Stracisz dostęp do sklepu, rankingu i swojej historii w tej grupie: %{balance} %{currency}, %{badges} i %{items}." + badges: + one: "%{count} odznaka" + few: "%{count} odznaki" + many: "%{count} odznak" + other: "%{count} odznaki" + items: + one: "%{count} przedmiot" + few: "%{count} przedmioty" + many: "%{count} przedmiotów" + other: "%{count} przedmiotu" + warning: "Tego nie da się cofnąć. Jeśli wrócisz do tej grupy z nowym kodem od prowadzącego, zaczniesz od zera." + stay: "Zostań w grupie" + leave: "Opuść grupę" + + # StoryGroupStudent — the nickname is written both when joining and in the + # group settings. + activerecord: + errors: + models: + story_group_student: + attributes: + nickname: + length: "Pseudonim musi mieć od 2 do 24 znaków." + taken: "Ten pseudonim jest już zajęty w tej grupie." diff --git a/config/locales/views/story_groups.en.yml b/config/locales/views/story_groups.en.yml new file mode 100644 index 00000000..20a83191 --- /dev/null +++ b/config/locales/views/story_groups.en.yml @@ -0,0 +1,425 @@ +en: + story_groups: + flash: + updated: "Saved the group settings." + destroyed: "Deleted the group %{name}." + duplicate_threshold: "Two ranks can't share a threshold: %{first} and %{second} both have %{value}." + + # StoryGroup.ranking_mode_label / #ranking_summary + ranking_modes: + podium_and_own: "Podium and own place" + full: "Full ranking" + ranking_off: "Off" + + # StoryGroupsListing + listing: + roles: + own: "You lead" + sup: "You assist" + lrn: "You learn" + tabs: + all: "All" + mine: "Mine" + teacher: "Teaching" + student: "Learning" + owner: "Owner: %{name}" + no_purchases: "No new purchases" + purchases: + one: "%{count} new purchase" + other: "%{count} new purchases" + balance: "You have %{balance} %{currency}" + + # TeacherOverview + overview: + stats: + students: "Students" + purchases: "Purchases this week" + purchases_note: "for %{amount}" + rewards: "Rewards this week" + ranking: "Ranking" + zero_lives_title: "%{name} has 0 lives" + zero_lives_detail: "Can only buy items available at 0 lives." + open: "Open" + sheet_in_progress: "%{name} in progress" + sheet_awarded: + one: "Awarded %{count} reward" + other: "Awarded %{count} rewards" + sheet_pending: + one: "%{count} column not graded." + other: "%{count} columns not graded." + grade: "Grade" + + helpers: + teacher_lead: + one: "%{count} student in this group. Add a description in the settings to welcome newcomers." + other: "%{count} students in this group. Add a description in the settings to welcome newcomers." + next_rank: "%{missing} more to the %{rank} rank." + next_rank_discount: "That rank gives −%{discount}% in the shop." + ranking_off: "The ranking is off — students don't see this podium." + ranking_full: "Students see the full ranking, under nicknames." + ranking_podium: "Students see the podium and their own place, under nicknames." + place: "#%{place} in the group ranking" + hand: + one: "Grab something in the shop. You can afford %{count} item right now." + other: "Grab something in the shop. You can afford %{count} items right now." + + index: + title: "Groups" + student_lead: "Groups you learn in." + teacher_lead: "Groups you lead, assist or learn in." + join: "Join a group" + create: "Create a group" + filter: "Group filter" + search: "Search for a group" + no_match: "No groups named" + no_match_hint: "Clear the search or check the spelling." + empty_tab_title: "No groups in this tab" + empty_student_title: "You're not in any group yet" + empty_title: "You don't have any groups yet" + empty_tab_body: "Switch tabs to see the other groups." + empty_body: "Got a code from your teacher? Join the group as a participant." + + group_card: + students: + one: "%{count} student" + other: "%{count} students" + + teacher_show: + show_code: "Show code to students" + create_sheet: "Create a sheet" + settings: "Group settings" + summary: "Group summary" + purchases: "Recent purchases" + removed_item: "Item removed from the shop" + all_purchases: "All purchases in the group" + no_purchases: "Nobody has bought anything yet. Purchases will show up here on their own once students start spending currency in the shop." + attention: "Needs attention" + recent_sheets: "Recent sheets" + open: "Open" + no_rewards: "Nobody has rewards from this sheet yet." + no_sheets_title: "You don't have any grade sheets yet" + no_sheets_body: "A sheet is one class: reward columns and the list of students. Create the first one to start awarding currency." + + student_show: + led_by: "Led by %{name}" + balance_label: "Your %{currency}" + to_spend: "To spend" + collected: "Collected in total" + lives: "Lives" + open_shop: "Open the shop" + your_rank: "Your rank" + no_rank: "No rank" + progress: "%{total} of %{needed}" + progress_label: "Progress to the next rank" + top_rank: "%{total} collected. This is the highest rank in this group." + no_ranks: "The teacher hasn't added ranks yet. The currency you collect still counts — ranks will be assigned on their own once they appear." + badges: "Badges" + badges_count: "%{earned} of %{total}" + items: "Your items" + go_to_shop: "Go to the shop" + recent: "Recent changes" + entry_meta: "%{label}, %{when}" + full_history: "Show the full history" + no_entries: "Your first entries will show up after class. The teacher awards currency for attendance, punctuality and activity." + + new: + page_title: "New group" + + edit: + page_title: "Group settings - %{name}" + + created: + page_title: "The group %{name} is ready" + title: "The group “%{name}” is ready" + ranks: + one: "%{count} rank" + other: "%{count} ranks" + badges: + one: "%{count} badge" + other: "%{count} badges" + items: + one: "%{count} item" + other: "%{count} items" + categories: + one: "%{count} category in the sheet template" + other: "%{count} categories in the sheet template" + quick_lead: "Created %{list}." + empty_lead: "The group is empty. You'll find the next steps below." + invites_note_html: "You'll create a join code in the %{tab} tab when you need it. You can have several codes at once, with a limit of people or an expiry date." + invites_tab: "Invites" + next: "What's next" + quick_steps: + - "Invite students: show them the code or the QR code." + - "After the first class, create a grade sheet and grade the students." + - "Look through the shop and fit the item descriptions to your story." + empty_steps: + - "Add ranks so students can see what they are working towards." + - "Add items to the shop." + - "Add badges for special achievements." + - "Create a grade sheet template." + - "Invite students: show them the code or the QR code." + go_to_group: "Go to the group" + create_invite: "Create an invite" + + confirm_destroy: + page_title: "Delete group %{name}" + title: "Delete the group for good?" + students: + one: "%{count} student" + other: "%{count} students" + teachers: + one: "%{count} teacher" + other: "%{count} teachers" + lose_access: + one: "%{students} and %{teachers} will lose access." + other: "%{students} and %{teachers} will lose access." + gone: "The history, items and ranking will be gone." + confirm: "Type the group name to confirm:" + mismatch: "The name you typed doesn't match the group name. The group was not deleted." + remove: "Delete group" + + settings: + title: "Group settings" + lead: "All teachers of this group can change these. Only the owner can delete the group. You set up the ranking in the Ranking tab." + group_section: "Group and story" + name: "Group name" + name_placeholder: "e.g. Space Rabbits" + story: "Story" + story_hint: "Everyone in the group sees it on the group's home page." + art_label: "Group image" + art_hint: "JPG or PNG, ideally at least 1200 px wide." + currency_section: "Currency" + currency_hint: "The new name will show up everywhere, including students' history." + currency_name: "Currency name" + currency_placeholder: "e.g. Carrots" + currency_name_hint: "Use the form that fits after a number — e.g. “Carrots”." + lives_section: "Lives" + lives: "Starting lives" + lives_hint: "Only applies to students who join from now on. You change current students' lives in the student list." + back: "Back to the group" + delete_title: "Delete group" + delete_body: "Deleting takes away everyone's access to the group, its shop, ranking and history. This can't be undone." + delete: "Delete group" + + # Shared by the wizard and the settings form (shared/_image_field). + currency_icon: + label: "Currency icon" + presets: "Ready-made icons" + own: "Your icon" + upload: "Or upload your own icon" + hint: "Ideally a PNG with a transparent background, at least 128 px." + crop: "Crop the icon" + cropped: "Move and resize the selection. We save a square crop, because the icon sits on a round token." + + wizard: + steps: + - "Group and story" + - "Currency and rules" + - "How you start" + - "Summary" + breadcrumb: "Groups" + title: "New group" + lead: "It takes a few minutes. You can change everything later in the group settings." + steps_label: "Steps" + name_section: "Name and story" + name_hint: "Students will see them in the group list and on the group page." + name: "Group name" + name_placeholder: "e.g. Order of Algorithms" + name_missing: "Enter the group name." + story: "Story" + story_placeholder: "Once upon a time…" + story_hint: "A short story intro. You can change it any time." + art_section: "Group image" + art_hint: "Shows on the group card, in the side panel and as the group background." + currency_section: "Currency name" + currency_hint: "It's read after every number, so use the plural form — e.g. “Gold Coins”." + currency_name: "Currency name" + currency_placeholder: "e.g. Gold Coins" + currency_missing: "Enter the currency name." + lives_section: "Lives" + lives: "Starting lives" + fewer_lives: "Fewer lives" + more_lives: "More lives" + lives_hint: "At 0 lives a student can only buy items marked as available at 0 lives." + ranking_section: "Ranking" + ranking: "Show the ranking to students" + ranking_hint: "Off by default. You can turn it on any time in the Ranking tab." + ranking_mode: "What students see" + ranking_mode_hint: "“Podium and own place” shows only the top three and the student's own place. “Full ranking” shows the whole list." + start_section: "How do you want to start?" + start_hint: "Both ways give you a complete group. You can change everything later." + start_label: "How to start" + recommended: "Recommended for your first group" + quick: "Quick start" + quick_body: "Ready-made ranks, badges, items and class categories. You'll review and adjust them before the group is created." + manual: "From scratch" + manual_body: "An empty group. You'll add ranks, badges and items later, step by step." + path_missing: "Choose how you want to start." + pack_section: "Set theme" + pack_hint: "Only changes the names of ranks and badges. The rules are the same everywhere." + pack_label: "Theme" + classes_section: "How many classes are you planning?" + classes_hint: "We'll fit the rank thresholds and prices so the highest rank is reachable within the semester." + classes_label: "Number of classes" + fewer_classes: "Fewer classes" + more_classes: "More classes" + summary: "Summary" + summary_hint: "The group will be created empty. After creating it you'll see a list of steps to complete." + summary_currency: "Currency" + summary_lives: "Starting lives" + summary_ranking: "Ranking" + card_preview: "Card in the group list" + you_lead: "You lead this group" + background_hint: "The background behind the form shows how the image looks as the group background." + currency_preview: "This is how students will see the currency" + back: "Back" + step_counter: "Step %{step} of %{total}" + submit: "Create group" + # group_wizard_controller.ts + js: + review_step: "Set review" + summary_step: "Summary" + counter: "Step %{step} of %{total}" + next: "Next" + create_short: "Create group" + create_quick: "Create group with the set" + create_empty: "Create an empty group" + name_placeholder: "Group name" + currency_placeholder: "Carrots" + ranking_off: "Off" + ranking_on: "Visible to students" + + currency_mini: + to_spend: "to spend" + + preset_review: + title: "The “%{pack}” set for %{classes} classes" + hint: "Remove what you don't need and adjust the numbers. You'll change names and descriptions after the group is created." + ranks: "Ranks" + starting_rank: "Starting rank" + shop_discount: "−%{discount}% in the shop" + from: "from" + from_zero: "from 0" + threshold_label: "%{name}: threshold" + badges: "Badges" + items: "Items in the shop" + price_label: "%{name}: price in %{currency}" + currency_fallback: "currency" + categories: "Class categories" + template: "(the “%{name}” template)" + reward_label: "%{name}: reward in %{currency}" + + preset_row: + remove: "Remove: %{name}" + restore: "Restore: %{name}" + + # StarterPack — written to the database in the teacher's language when the + # group is created. Numbers and icons stay in app/values/starter_pack.rb; + # every list here must keep its length and order. + starter_pack: + template_name: "Classes" + categories: + - "Attendance" + - "Punctuality" + - "Quiz passed" + - "Quiz ≥ group average" + - "Volunteered for a task" + - "Task done correctly" + - "Helping others" + - "Insightful remark" + badge_rules: + - "First quiz without mistakes" + - "Attended every class in a month" + - "Helped others at least 3 times" + - "At least 3 insightful remarks" + - "3 quizzes in a row ≥ group average" + - "Explained a hard task to the whole group" + items: + - name: "+5 minutes on a quiz" + didactic: "Adds 5 minutes to the time for the next quiz." + - name: "Consultation" + didactic: "A quarter-hour consultation with the teacher outside class." + - name: "Quiz retake" + didactic: "Lets you write one quiz again." + - name: "Safe retake" + didactic: "A risk-free quiz retake — the better of the two results counts." + - name: "Excuse note" + didactic: "Excuses one absence, without losing a life." + - name: "1up: get a life back" + didactic: "Restores one lost life." + - name: "+0.5 on the final grade" + didactic: "Raises the final grade by half a step." + packs: + neutral: + label: "Neutral" + description: "No story theme. Fits any subject." + ranks: ["Novice", "Learner", "Adept", "Expert", "Master"] + badges: ["Flawless", "Steady attendance", "Helping hand", "Spark of curiosity", "Winning streak", "Voice of the group"] + fantasy: + label: "Fantasy" + description: "Castles, orders and knightly titles." + ranks: ["Squire", "Knight", "Castellan", "Hetman", "King"] + badges: ["Clean blade", "Loyal guard", "Shield of the party", "Book of wisdom", "String of victories", "Bard of the party"] + badge_stories: + - "Steel without a scratch — the first duel won without a graze." + - "You stood guard every night this month." + - "Three times you shielded a companion with your own shield." + - "Three times you added something the books didn't have yet." + - "Three skirmishes in a row settled in your favour." + - "You explained to the whole party what nobody else could put into words." + item_stories: + - "The hourglass runs slower than it should." + - "An audience with the master of the order." + - "A second go at the duel." + - "A duel with blunt blades — losing costs nothing." + - "A safe-conduct letter explaining your absence." + - "A potion that restores a life." + - "The king's seal on your certificate." + category_stories: + - "You showed up at the muster." + - "In place before the horn sounded." + - "Trial of skill passed." + - "A score above the party average." + - "You volunteered." + - "Task done without fault." + - "You gave a companion a hand." + - "You shed light on something nobody had noticed." + scifi: + label: "Sci-fi" + description: "Ships, crews and space missions." + ranks: ["Cadet", "Pilot", "Navigator", "Commander", "Admiral"] + badges: ["Clean flight", "Always on board", "Crew mechanic", "Signal from space", "Mission streak", "Voice of the fleet"] + badge_stories: + - "The first flight without a single reading in the red." + - "Not a single shift missed this cycle." + - "Three times you got someone else's gear back on its feet." + - "Three times you caught a signal others missed." + - "Three missions in a row above the fleet standard." + - "Your explanation went out on the whole channel and everyone understood." + item_stories: + - "A brief slowdown of the ship's clock." + - "A call with the commander outside duty hours." + - "A second go at the simulation." + - "A training flight — a crash costs nothing." + - "A log entry excusing your absence." + - "Regeneration in the medical pod." + - "A promotion entered into the fleet records." + category_stories: + - "You reported on board." + - "At your station before the countdown." + - "Qualification test passed." + - "A score above the crew average." + - "You volunteered for a task." + - "Task done by the book." + - "You backed up another crew member." + - "You spotted something the sensors missed." + + activerecord: + errors: + models: + story_group: + attributes: + name: + blank: "Enter the group name." + currency_name: + blank: "Enter the currency name." diff --git a/config/locales/views/story_groups.pl.yml b/config/locales/views/story_groups.pl.yml new file mode 100644 index 00000000..001f5cec --- /dev/null +++ b/config/locales/views/story_groups.pl.yml @@ -0,0 +1,451 @@ +pl: + story_groups: + flash: + updated: "Zapisano ustawienia grupy." + destroyed: "Usunięto grupę %{name}." + duplicate_threshold: "Dwie rangi nie mogą mieć tego samego progu: %{first} i %{second} mają %{value}." + + # StoryGroup.ranking_mode_label / #ranking_summary + ranking_modes: + podium_and_own: "Podium i własne miejsce" + full: "Pełny ranking" + ranking_off: "Wyłączony" + + # StoryGroupsListing + listing: + roles: + own: "Prowadzisz" + sup: "Wspierasz" + lrn: "Uczysz się" + tabs: + all: "Wszystkie" + mine: "Moje" + teacher: "Nauczam" + student: "Uczę się" + owner: "Właściciel: %{name}" + no_purchases: "Brak nowych zakupów" + purchases: + one: "%{count} nowy zakup" + few: "%{count} nowe zakupy" + many: "%{count} nowych zakupów" + other: "%{count} nowego zakupu" + balance: "Masz %{balance} %{currency}" + + # TeacherOverview + overview: + stats: + students: "Studenci" + purchases: "Zakupy w tym tygodniu" + purchases_note: "za %{amount}" + rewards: "Nagrody w tym tygodniu" + ranking: "Ranking" + zero_lives_title: "%{name} ma 0 żyć" + zero_lives_detail: "Kupi tylko przedmioty dostępne przy 0 życiach." + open: "Otwórz" + sheet_in_progress: "%{name} w trakcie" + sheet_awarded: + one: "Przyznano %{count} nagrodę" + few: "Przyznano %{count} nagrody" + many: "Przyznano %{count} nagród" + other: "Przyznano %{count} nagrody" + sheet_pending: + one: "%{count} kolumna bez ocen." + few: "%{count} kolumny bez ocen." + many: "%{count} kolumn bez ocen." + other: "%{count} kolumny bez ocen." + grade: "Oceń" + + helpers: + teacher_lead: + one: "%{count} student w tej grupie. Dodaj opis w ustawieniach, żeby powitać nowe osoby." + few: "%{count} studenci w tej grupie. Dodaj opis w ustawieniach, żeby powitać nowe osoby." + many: "%{count} studentów w tej grupie. Dodaj opis w ustawieniach, żeby powitać nowe osoby." + other: "%{count} studenta w tej grupie. Dodaj opis w ustawieniach, żeby powitać nowe osoby." + next_rank: "Jeszcze %{missing} do rangi %{rank}." + next_rank_discount: "Ta ranga daje −%{discount}% w sklepie." + ranking_off: "Ranking jest wyłączony — studenci nie widzą tego podium." + ranking_full: "Studenci widzą pełny ranking, pod pseudonimami." + ranking_podium: "Studenci widzą podium i własne miejsce, pod pseudonimami." + place: "%{place}. miejsce w rankingu grupy" + hand: + one: "Dobierz coś w sklepie. Stać cię teraz na %{count} przedmiot." + few: "Dobierz coś w sklepie. Stać cię teraz na %{count} przedmioty." + many: "Dobierz coś w sklepie. Stać cię teraz na %{count} przedmiotów." + other: "Dobierz coś w sklepie. Stać cię teraz na %{count} przedmiotu." + + index: + title: "Grupy" + student_lead: "Grupy, w których się uczysz." + teacher_lead: "Grupy, które prowadzisz, wspierasz albo w których się uczysz." + join: "Dołącz do grupy" + create: "Utwórz grupę" + filter: "Filtr grup" + search: "Szukaj grupy" + no_match: "Brak grup o nazwie" + no_match_hint: "Wyczyść wyszukiwanie albo sprawdź pisownię." + empty_tab_title: "Brak grup w tej zakładce" + empty_student_title: "Nie należysz jeszcze do żadnej grupy" + empty_title: "Nie masz jeszcze żadnej grupy" + empty_tab_body: "Zmień zakładkę, żeby zobaczyć pozostałe grupy." + empty_body: "Masz kod od prowadzącego? Dołącz do grupy jako uczestnik." + + group_card: + students: + one: "%{count} student" + few: "%{count} studentów" + many: "%{count} studentów" + other: "%{count} studenta" + + teacher_show: + show_code: "Pokaż kod dla studentów" + create_sheet: "Utwórz arkusz" + settings: "Ustawienia grupy" + summary: "Podsumowanie grupy" + purchases: "Ostatnie zakupy" + removed_item: "Przedmiot usunięty z oferty" + all_purchases: "Wszystkie zakupy w grupie" + no_purchases: "Nikt jeszcze niczego nie kupił. Zakupy pojawią się tutaj same, gdy studenci zaczną wydawać walutę w sklepie." + attention: "Wymaga uwagi" + recent_sheets: "Ostatnie arkusze" + open: "Otwórz" + no_rewards: "Nikt nie ma jeszcze nagród z tego arkusza." + no_sheets_title: "Nie masz jeszcze arkuszy ocen" + no_sheets_body: "Arkusz to jedne zajęcia: kolumny z nagrodami i lista studentów. Utwórz pierwszy, żeby zacząć przyznawać walutę." + + student_show: + led_by: "Prowadzi %{name}" + balance_label: "Twoje %{currency}" + to_spend: "Do wydania" + collected: "Zebrane łącznie" + lives: "Życia" + open_shop: "Otwórz sklep" + your_rank: "Twoja ranga" + no_rank: "Brak rangi" + progress: "%{total} z %{needed}" + progress_label: "Postęp do następnej rangi" + top_rank: "%{total} zebranych. To najwyższa ranga w tej grupie." + no_ranks: "Prowadzący nie dodał jeszcze rang. Waluta, którą zbierasz, liczy się mimo to — rangi przyznają się same, gdy się pojawią." + badges: "Odznaki" + badges_count: "%{earned} z %{total}" + items: "Twoje przedmioty" + go_to_shop: "Przejdź do sklepu" + recent: "Ostatnie zmiany" + entry_meta: "%{label}, %{when}" + full_history: "Pokaż całą historię" + no_entries: "Pierwsze wpisy pojawią się po zajęciach. Prowadzący przyznaje walutę za obecność, punktualność i aktywność." + + new: + page_title: "Nowa grupa" + + edit: + page_title: "Ustawienia grupy - %{name}" + + created: + page_title: "Grupa %{name} jest gotowa" + title: "Grupa „%{name}” jest gotowa" + ranks: + one: "%{count} rangę" + few: "%{count} rangi" + many: "%{count} rang" + other: "%{count} rangi" + badges: + one: "%{count} odznakę" + few: "%{count} odznaki" + many: "%{count} odznak" + other: "%{count} odznaki" + items: + one: "%{count} przedmiot" + few: "%{count} przedmioty" + many: "%{count} przedmiotów" + other: "%{count} przedmiotu" + categories: + one: "%{count} kategorię w szablonie arkusza" + few: "%{count} kategorie w szablonie arkusza" + many: "%{count} kategorii w szablonie arkusza" + other: "%{count} kategorii w szablonie arkusza" + quick_lead: "Utworzono %{list}." + empty_lead: "Grupa jest pusta. Poniżej znajdziesz kolejne kroki." + invites_note_html: "Kod do dołączenia utworzysz w zakładce %{tab}, kiedy będziesz go potrzebować. Możesz mieć kilka kodów naraz, z limitem osób albo datą ważności." + invites_tab: "Zaproszenia" + next: "Co dalej" + quick_steps: + - "Zaproś studentów: pokaż im kod albo kod QR." + - "Po pierwszych zajęciach utwórz arkusz ocen i oceń studentów." + - "Zajrzyj do sklepu i dopasuj opisy przedmiotów do swojej opowieści." + empty_steps: + - "Dodaj rangi, żeby studenci widzieli, do czego dążą." + - "Dodaj przedmioty do sklepu." + - "Dodaj odznaki za szczególne osiągnięcia." + - "Utwórz szablon arkusza ocen." + - "Zaproś studentów: pokaż im kod albo kod QR." + go_to_group: "Przejdź do grupy" + create_invite: "Utwórz zaproszenie" + + confirm_destroy: + page_title: "Usuń grupę %{name}" + title: "Usunąć grupę na zawsze?" + students: + one: "%{count} student" + few: "%{count} studenci" + many: "%{count} studentów" + other: "%{count} studenta" + teachers: + one: "%{count} nauczyciel" + few: "%{count} nauczycieli" + many: "%{count} nauczycieli" + other: "%{count} nauczyciela" + lose_access: + one: "%{students} i %{teachers} straci dostęp." + few: "%{students} i %{teachers} stracą dostęp." + many: "%{students} i %{teachers} straci dostęp." + other: "%{students} i %{teachers} straci dostęp." + gone: "Historia, przedmioty i ranking znikną." + confirm: "Wpisz nazwę grupy, żeby potwierdzić:" + mismatch: "Wpisana nazwa nie zgadza się z nazwą grupy. Grupa nie została usunięta." + remove: "Usuń grupę" + + settings: + title: "Ustawienia grupy" + lead: "Zmienią je wszyscy prowadzący tę grupę. Tylko właściciel może usunąć grupę. Ranking ustawiasz w zakładce Ranking." + group_section: "Grupa i opowieść" + name: "Nazwa grupy" + name_placeholder: "np. Kosmiczne króliki" + story: "Opowieść" + story_hint: "Widzą ją wszyscy w grupie, na stronie głównej grupy." + art_label: "Grafika grupy" + art_hint: "JPG lub PNG, najlepiej co najmniej 1200 px szerokości." + currency_section: "Waluta" + currency_hint: "Nowa nazwa pojawi się wszędzie, także w historii studentów." + currency_name: "Nazwa waluty" + currency_placeholder: "np. Marchewek" + currency_name_hint: "Podaj ją w formie, która pasuje do liczby — np. „Marchewek”." + lives_section: "Życia" + lives: "Życia na start" + lives_hint: "Dotyczy tylko studentów, którzy dołączą od teraz. Obecnym zmieniasz życia na liście studentów." + back: "Wróć do grupy" + delete_title: "Usuń grupę" + delete_body: "Usunięcie zabiera wszystkim dostęp do grupy, jej sklepu, rankingu i historii. Tego nie da się cofnąć." + delete: "Usuń grupę" + + # Shared by the wizard and the settings form (shared/_image_field). + currency_icon: + label: "Ikona waluty" + presets: "Gotowe ikony" + own: "Twoja ikona" + upload: "Albo wgraj własną ikonę" + hint: "Najlepiej PNG z przezroczystym tłem, co najmniej 128 px." + crop: "Przytnij ikonę" + cropped: "Przesuń i przeskaluj zaznaczenie. Zapisujemy kwadratowy kadr, bo ikona siada na okrągłym liczmanie." + + wizard: + steps: + - "Grupa i opowieść" + - "Waluta i zasady" + - "Jak zaczynasz" + - "Podsumowanie" + breadcrumb: "Grupy" + title: "Nowa grupa" + lead: "Zajmie to kilka minut. Wszystko zmienisz później w ustawieniach grupy." + steps_label: "Kroki" + name_section: "Nazwa i opowieść" + name_hint: "Studenci zobaczą je na liście grup i na stronie grupy." + name: "Nazwa grupy" + name_placeholder: "np. Zakon Algorytmów" + name_missing: "Podaj nazwę grupy." + story: "Opowieść" + story_placeholder: "Dawno, dawno temu…" + story_hint: "Krótki wstęp fabularny. Zmienisz go w każdej chwili." + art_section: "Grafika grupy" + art_hint: "Pojawia się na karcie grupy, w panelu bocznym i jako tło grupy." + currency_section: "Nazwa waluty" + currency_hint: "Czytamy ją po każdej liczbie, więc podaj formę, która pasuje do liczby mnogiej — np. „Marchewek”." + currency_name: "Nazwa waluty" + currency_placeholder: "np. Złotych Monet" + currency_missing: "Podaj nazwę waluty." + lives_section: "Życia" + lives: "Życia na start" + fewer_lives: "Mniej żyć" + more_lives: "Więcej żyć" + lives_hint: "Przy 0 życiach student kupi tylko przedmioty oznaczone jako dostępne przy 0 życiach." + ranking_section: "Ranking" + ranking: "Pokazuj ranking studentom" + ranking_hint: "Domyślnie wyłączony. Możesz go włączyć w każdej chwili w zakładce Ranking." + ranking_mode: "Co widzą studenci" + ranking_mode_hint: "„Podium i własne miejsce” pokazuje tylko najlepszą trójkę i miejsce studenta. „Pełny ranking” pokazuje całą listę." + start_section: "Jak chcesz zacząć?" + start_hint: "Obie drogi dają pełną grupę. Wszystko zmienisz później." + start_label: "Sposób startu" + recommended: "Polecane na pierwszą grupę" + quick: "Szybki start" + quick_body: "Gotowe rangi, odznaki, przedmioty i kategorie zajęć. Przejrzysz je i poprawisz przed utworzeniem grupy." + manual: "Od zera" + manual_body: "Pusta grupa. Rangi, odznaki i przedmioty dodasz później, krok po kroku." + path_missing: "Wybierz, jak chcesz zacząć." + pack_section: "Klimat zestawu" + pack_hint: "Zmienia tylko nazwy rang i odznak. Zasady są wszędzie takie same." + pack_label: "Klimat" + classes_section: "Ile zajęć planujesz?" + classes_hint: "Dopasujemy progi rang i ceny tak, żeby najwyższa ranga była osiągalna w semestrze." + classes_label: "Liczba zajęć" + fewer_classes: "Mniej zajęć" + more_classes: "Więcej zajęć" + summary: "Podsumowanie" + summary_hint: "Grupa powstanie pusta. Po utworzeniu zobaczysz listę kroków do uzupełnienia." + summary_currency: "Waluta" + summary_lives: "Życia na start" + summary_ranking: "Ranking" + card_preview: "Karta na liście grup" + you_lead: "Prowadzisz tę grupę" + background_hint: "Tło za formularzem pokazuje, jak grafika wygląda jako tło grupy." + currency_preview: "Tak zobaczą walutę studenci" + back: "Wstecz" + step_counter: "Krok %{step} z %{total}" + submit: "Utwórz grupę" + # group_wizard_controller.ts + js: + review_step: "Przegląd zestawu" + summary_step: "Podsumowanie" + counter: "Krok %{step} z %{total}" + next: "Dalej" + create_short: "Utwórz grupę" + create_quick: "Utwórz grupę z zestawem" + create_empty: "Utwórz pustą grupę" + name_placeholder: "Nazwa grupy" + currency_placeholder: "Marchewek" + ranking_off: "Wyłączony" + ranking_on: "Widoczny dla studentów" + + currency_mini: + to_spend: "do wydania" + + preset_review: + title: "Zestaw „%{pack}” dla %{classes} zajęć" + hint: "Usuń, czego nie potrzebujesz, i popraw liczby. Nazwy i opisy zmienisz po utworzeniu grupy." + ranks: "Rangi" + starting_rank: "Ranga startowa" + shop_discount: "−%{discount}% w sklepie" + from: "od" + from_zero: "od 0" + threshold_label: "%{name}: próg" + badges: "Odznaki" + items: "Przedmioty w sklepie" + price_label: "%{name}: cena w %{currency}" + currency_fallback: "waluty" + categories: "Kategorie zajęć" + template: "(szablon „%{name}”)" + reward_label: "%{name}: nagroda w %{currency}" + + preset_row: + remove: "Usuń: %{name}" + restore: "Przywróć: %{name}" + + # StarterPack — written to the database in the teacher's language when the + # group is created. Numbers and icons stay in app/values/starter_pack.rb; + # every list here must keep its length and order. + starter_pack: + template_name: "Zajęcia" + categories: + - "Obecność" + - "Punktualność" + - "Wejściówka zaliczona" + - "Wejściówka ≥ średnia grupy" + - "Zgłoszenie do zadania" + - "Zadanie zrobione poprawnie" + - "Pomoc innym" + - "Ciekawa uwaga" + badge_rules: + - "Pierwsza wejściówka bez błędów" + - "Obecność na wszystkich zajęciach w miesiącu" + - "Co najmniej 3 razy pomógł innym" + - "Co najmniej 3 ciekawe uwagi" + - "3 wejściówki z rzędu ≥ średniej grupy" + - "Wyjaśnienie trudnego zadania całej grupie" + items: + - name: "+5 minut do wejściówki" + didactic: "Dodaje 5 minut do czasu na najbliższą wejściówkę." + - name: "Konsultacja" + didactic: "Kwadrans konsultacji z prowadzącym poza zajęciami." + - name: "Poprawa wejściówki" + didactic: "Pozwala napisać jedną wejściówkę jeszcze raz." + - name: "Bezpieczna poprawa" + didactic: "Poprawa wejściówki bez ryzyka — liczy się lepszy z dwóch wyników." + - name: "Usprawiedliwienie" + didactic: "Usprawiedliwia jedną nieobecność, bez utraty życia." + - name: "1up: odzyskanie życia" + didactic: "Przywraca jedno utracone życie." + - name: "+0.5 oceny końcowej" + didactic: "Podnosi ocenę końcową o pół stopnia." + packs: + neutral: + label: "Neutralny" + description: "Bez fabularnego klimatu. Pasuje do każdego przedmiotu." + ranks: ["Nowicjusz", "Uczeń", "Adept", "Ekspert", "Mistrz"] + badges: ["Bez skazy", "Stała obecność", "Pomocna dłoń", "Iskra ciekawości", "Seria sukcesów", "Głos grupy"] + fantasy: + label: "Fantasy" + description: "Zamki, zakony i rycerskie tytuły." + ranks: ["Giermek", "Rycerz", "Kasztelan", "Hetman", "Król"] + badges: ["Czysta klinga", "Wierna straż", "Tarcza drużyny", "Księga mądrości", "Pasmo zwycięstw", "Bard drużyny"] + badge_stories: + - "Stal bez jednej rysy — pierwszy pojedynek wygrany bez draśnięcia." + - "Stałeś na posterunku każdej nocy tego miesiąca." + - "Trzykrotnie zasłoniłeś towarzysza własną tarczą." + - "Trzy razy dopisałeś coś, czego w księgach jeszcze nie było." + - "Trzy potyczki z rzędu rozstrzygnięte na twoją korzyść." + - "Wytłumaczyłeś całej drużynie to, czego nikt inny nie potrafił nazwać." + item_stories: + - "Klepsydra sypie piaskiem wolniej, niż powinna." + - "Audiencja u mistrza zakonu." + - "Drugie podejście do pojedynku." + - "Pojedynek na tępe klingi — przegrana nic nie kosztuje." + - "List żelazny tłumaczący twoją nieobecność." + - "Eliksir przywracający życie." + - "Pieczęć króla na twoim świadectwie." + category_stories: + - "Stawiłeś się na zbiórce." + - "Na miejscu, zanim zabrzmiał róg." + - "Próba sprawności zaliczona." + - "Wynik powyżej średniej drużyny." + - "Zgłosiłeś się na ochotnika." + - "Zadanie wykonane bez zarzutu." + - "Podałeś rękę towarzyszowi." + - "Rzuciłeś światło na rzecz, której nikt nie dostrzegł." + scifi: + label: "Sci-fi" + description: "Statki, załogi i kosmiczne misje." + ranks: ["Kadet", "Pilot", "Nawigator", "Komandor", "Admirał"] + badges: ["Czysty lot", "Zawsze na pokładzie", "Mechanik załogi", "Sygnał z kosmosu", "Seria misji", "Głos floty"] + badge_stories: + - "Pierwszy lot bez jednego odczytu w czerwieni." + - "Ani jednej zmiany opuszczonej w tym cyklu." + - "Trzy razy postawiłeś na nogi cudzy sprzęt." + - "Trzy razy wychwyciłeś sygnał, który inni przegapili." + - "Trzy misje z rzędu zaliczone powyżej normy floty." + - "Twoje wyjaśnienie poszło na cały kanał i wszyscy zrozumieli." + item_stories: + - "Chwilowe spowolnienie zegara pokładowego." + - "Połączenie z komandorem poza godzinami służby." + - "Drugie podejście do symulacji." + - "Lot treningowy — awaria nic nie kosztuje." + - "Wpis do dziennika usprawiedliwiający nieobecność." + - "Regeneracja w kapsule medycznej." + - "Awans wpisany do akt floty." + category_stories: + - "Zameldowałeś się na pokładzie." + - "Na stanowisku przed odliczaniem." + - "Test kwalifikacyjny zaliczony." + - "Wynik powyżej średniej załogi." + - "Zgłosiłeś się do zadania." + - "Zadanie wykonane zgodnie z procedurą." + - "Wsparłeś innego członka załogi." + - "Zauważyłeś coś, co umknęło czujnikom." + + activerecord: + errors: + models: + story_group: + attributes: + name: + blank: "Podaj nazwę grupy." + currency_name: + blank: "Podaj nazwę waluty." diff --git a/config/locales/views/students.en.yml b/config/locales/views/students.en.yml new file mode 100644 index 00000000..e6d6b3fa --- /dev/null +++ b/config/locales/views/students.en.yml @@ -0,0 +1,117 @@ +en: + students: + flash: + lives_notice: + one: "%{name} now has %{count} life." + other: "%{name} now has %{count} lives." + cannot_take_life: "Can't take a life — the student already has 0." + removed: "%{name} is no longer in the group." + + helpers: + list_lead: "You take a life for an unexcused absence." + zero_lives: + one: "%{count} student has 0 lives" + other: "%{count} students have 0 lives" + zero_lives_tail: "and can only buy items available at 0 lives." + nickname_quoted: "“%{nickname}”" + nickname: "Nickname: %{nickname}" + university_number: "student no. %{number}" + rank_progress: "%{total} of %{required} to the %{rank} rank" + losses: + badges: + one: "%{count} badge" + other: "%{count} badges" + items: + one: "%{count} item" + other: "%{count} items" + ledger: + one: "the whole currency history (%{count} entry)" + other: "the whole currency history (%{count} entries)" + + index: + title: "Students" + invites: "Codes and invites" + show_code: "Show code to students" + search: "Search for a student" + of: "of" + count: "%{shown} of %{total}" + columns: + student: "Student" + rank: "Rank" + lives: "Lives" + balance: "To spend" + total: "Collected" + badges: "Badges" + actions: "Actions" + no_match: "No students named" + no_match_hint: "Clear the search or check the spelling." + empty_title: "Nobody has joined the group yet" + empty_body: "Send students a code or an invite — once they join, they will show up here with their rank, currency and lives." + + row: + rank: "Rank" + lives: "Lives" + balance: "To spend" + total: "Collected" + badges: "Badges" + take_life: "Take a life: %{name}" + add_life: "Add a life: %{name}" + award_badge: "Award a badge" + award_badge_for: "Award a badge: %{name}" + adjust_currency: "Adjust currency" + adjust_currency_for: "Adjust currency: %{name}" + history: "Transaction history" + history_for: "History: %{name}" + + show: + breadcrumb: "Students" + award_badge: "Award a badge" + adjust_currency: "Adjust currency" + rank: "Rank" + rank_progress_label: "Progress to the %{rank} rank" + balance: "To spend" + total: "Collected in total" + lives: "Lives" + sections: "Student sections" + tabs: + badges: "Badges" + items: "Items" + hist: "Currency history" + + edit: + page_title: "Edit student - %{name}" + + form: + title: "Edit student" + account_note: "The name and e-mail come from the student's account." + lives: "Lives" + take_life: "Take a life" + add_life: "Add a life" + lives_hint: "You take a life for an unexcused absence. At 0 lives the student can only buy items available at 0 lives." + remove_label: "Remove from group" + remove_hint: "The student will lose access to the group, and their badges, items and currency history will be lost. They can only rejoin with a new code." + remove: "Remove from group" + + confirm_destroy: + page_title: "Remove from group - %{name}" + title: "Remove the student from the group?" + access_lost: "%{name} will lose access to the group and disappear from lists, the ranking and grade sheets." + losses: "Lost: %{list}." + irreversible: "This can't be undone." + nothing_to_lose: "This student has no badges, items or currency history yet." + rejoin_note: "If they rejoin the group, they will get a new member account and start from scratch." + remove: "Remove from group" + + tab_badges: + removed: "Removed from the list" + awarded: "Awarded %{when}" + revoke: "Revoke" + revoke_label: "Revoke badge %{name}" + empty: "The student has no badges yet." + + tab_history: + empty: "No entries of this type." + + tab_items: + intro: "Items bought in the shop. You settle the use of an item (e.g. a quiz retake) in class." + empty: "The student hasn't bought anything yet. Items will show up here with the price they paid." diff --git a/config/locales/views/students.pl.yml b/config/locales/views/students.pl.yml new file mode 100644 index 00000000..8e7ef7a7 --- /dev/null +++ b/config/locales/views/students.pl.yml @@ -0,0 +1,127 @@ +pl: + students: + flash: + lives_notice: + one: "%{name} ma teraz %{count} życie." + few: "%{name} ma teraz %{count} życia." + many: "%{name} ma teraz %{count} żyć." + other: "%{name} ma teraz %{count} życia." + cannot_take_life: "Nie można odebrać życia — student ma już 0." + removed: "%{name} nie należy już do grupy." + + helpers: + list_lead: "Życie odbierasz za nieusprawiedliwioną nieobecność." + zero_lives: + one: "%{count} student ma 0 żyć" + few: "%{count} studentów ma 0 żyć" + many: "%{count} studentów ma 0 żyć" + other: "%{count} studentów ma 0 żyć" + zero_lives_tail: "i kupi tylko przedmioty dostępne przy 0 życiach." + nickname_quoted: "„%{nickname}”" + nickname: "Pseudonim: %{nickname}" + university_number: "indeks %{number}" + rank_progress: "%{total} z %{required} do rangi %{rank}" + losses: + badges: + one: "%{count} odznakę" + few: "%{count} odznaki" + many: "%{count} odznak" + other: "%{count} odznaki" + items: + one: "%{count} przedmiot" + few: "%{count} przedmioty" + many: "%{count} przedmiotów" + other: "%{count} przedmiotu" + ledger: + one: "całą historię waluty (%{count} wpis)" + few: "całą historię waluty (%{count} wpisy)" + many: "całą historię waluty (%{count} wpisów)" + other: "całą historię waluty (%{count} wpisu)" + + index: + title: "Studenci" + invites: "Kody i zaproszenia" + show_code: "Pokaż kod dla studentów" + search: "Szukaj studenta" + of: "z" + count: "%{shown} z %{total}" + columns: + student: "Student" + rank: "Ranga" + lives: "Życia" + balance: "Do wydania" + total: "Zebrane" + badges: "Odznaki" + actions: "Akcje" + no_match: "Brak studentów o nazwie" + no_match_hint: "Wyczyść wyszukiwanie albo sprawdź pisownię." + empty_title: "Nie ma jeszcze nikogo w grupie" + empty_body: "Wyślij studentom kod albo zaproszenie — kiedy dołączą, pojawią się tutaj razem z rangą, walutą i życiami." + + row: + rank: "Ranga" + lives: "Życia" + balance: "Do wydania" + total: "Zebrane" + badges: "Odznaki" + take_life: "Odbierz życie: %{name}" + add_life: "Dodaj życie: %{name}" + award_badge: "Przyznaj odznakę" + award_badge_for: "Przyznaj odznakę: %{name}" + adjust_currency: "Koryguj walutę" + adjust_currency_for: "Koryguj walutę: %{name}" + history: "Historia transakcji" + history_for: "Historia: %{name}" + + show: + breadcrumb: "Studenci" + award_badge: "Przyznaj odznakę" + adjust_currency: "Koryguj walutę" + rank: "Ranga" + rank_progress_label: "Postęp do rangi %{rank}" + balance: "Do wydania" + total: "Zebrane łącznie" + lives: "Życia" + sections: "Sekcje studenta" + tabs: + badges: "Odznaki" + items: "Przedmioty" + hist: "Historia waluty" + + edit: + page_title: "Edytuj studenta - %{name}" + + form: + title: "Edytuj studenta" + account_note: "Imię, nazwisko i e-mail pochodzą z konta studenta." + lives: "Życia" + take_life: "Odbierz życie" + add_life: "Dodaj życie" + lives_hint: "Życie odbierasz za nieusprawiedliwioną nieobecność. Przy 0 życiach student kupi tylko przedmioty dostępne przy 0 życiach." + remove_label: "Usuń z grupy" + remove_hint: "Student straci dostęp do grupy, a jego odznaki, przedmioty i historia waluty przepadną. Dołączy ponownie tylko z nowym kodem." + remove: "Usuń z grupy" + + confirm_destroy: + page_title: "Usuń z grupy - %{name}" + title: "Usunąć studenta z grupy?" + access_lost: "%{name} straci dostęp do grupy i zniknie z list, rankingu i arkuszy ocen." + losses: "Przepadną: %{list}." + irreversible: "Tego nie da się cofnąć." + nothing_to_lose: "Ten student nie ma jeszcze odznak, przedmiotów ani historii waluty." + rejoin_note: "Jeśli wróci do grupy, dostanie nowe konto uczestnika i zacznie od zera." + remove: "Usuń z grupy" + + tab_badges: + removed: "Usunięta z listy" + awarded: "Przyznana %{when}" + revoke: "Odbierz" + revoke_label: "Odbierz odznakę %{name}" + empty: "Student nie ma jeszcze żadnej odznaki." + + tab_history: + empty: "Brak wpisów tego typu." + + tab_items: + intro: "Przedmioty kupione w sklepie. Wykorzystanie przedmiotu (np. poprawę wejściówki) rozliczasz na zajęciach." + empty: "Student nic jeszcze nie kupił. Przedmioty pojawią się tu razem z ceną, jaką zapłacił." diff --git a/config/locales/views/students_activity_group_categories.en.yml b/config/locales/views/students_activity_group_categories.en.yml new file mode 100644 index 00000000..fd461f0a --- /dev/null +++ b/config/locales/views/students_activity_group_categories.en.yml @@ -0,0 +1,65 @@ +en: + # The grading table ("Ocenianie"). + students_activity_group_categories: + flash: + nothing: "No cells were marked." + awarded: + one: "Awarded %{total} %{currency} to %{count} student." + other: "Awarded %{total} %{currency} to %{count} students." + + edit: + lead: + one: "%{categories}, %{count} student. Mark who earned rewards — you'll review everything once more before awarding." + other: "%{categories}, %{count} students. Mark who earned rewards — you'll review everything once more before awarding." + categories: + one: "%{count} category" + other: "%{count} categories" + search: "Search for a student" + legend: "Legend" + empty: "Empty" + marked_now: "Marked now" + awarded_locked: "Awarded, can't be undone" + student: "Student" + select_column: "Mark the whole column" + everyone: "Everyone" + total: "Total" + locked_title: "Awarded, can't be undone" + cell_awarded: "%{label}: awarded" + cell_empty: "%{label}: empty" + nothing_marked: "Nothing is marked. Click the cells of students who earned rewards." + clear: "Clear marks" + review: "Review and award" + no_columns_title: "This sheet has no columns" + no_columns_body: "Add columns in the sheet settings so there's something to reward." + sheet_settings: "Sheet settings" + no_students_title: "There are no students in this group yet" + no_students_body: "Invite students to the group — the sheet is ready and nothing in it gets lost." + invites: "Invites" + mobile_body: "Grading in the table works on a computer. This table has %{categories} and %{students}, so it would be unreadable on a phone." + mobile_categories: + one: "%{count} category" + other: "%{count} categories" + mobile_students: + one: "%{count} student" + other: "%{count} students" + mobile_hint: "On your phone you can check recent purchases and notifications." + back_home: "Back to home" + # grading_controller.ts + js: + cells: + one: "Marked now: %{count} cell" + other: "Marked now: %{count} cells" + students: + one: "for %{count} student, total" + other: "for %{count} students, total" + recipients: + one: "to %{count} student" + other: "to %{count} students" + cell_empty: "%{label}: empty" + cell_marked: "%{label}: marked" + locked: "This cell is already awarded and can't be undone." + + review_dialog: + awarding: "You're awarding" + warning: "Once confirmed, this can't be undone. Check the list before awarding rewards." + confirm: "Award" diff --git a/config/locales/views/students_activity_group_categories.pl.yml b/config/locales/views/students_activity_group_categories.pl.yml new file mode 100644 index 00000000..48a025c2 --- /dev/null +++ b/config/locales/views/students_activity_group_categories.pl.yml @@ -0,0 +1,81 @@ +pl: + # The grading table ("Ocenianie"). + students_activity_group_categories: + flash: + nothing: "Nie zaznaczono żadnego pola." + awarded: + one: "Przyznano %{total} %{currency} %{count} studentowi." + few: "Przyznano %{total} %{currency} %{count} studentom." + many: "Przyznano %{total} %{currency} %{count} studentom." + other: "Przyznano %{total} %{currency} %{count} studenta." + + edit: + lead: + one: "%{categories}, %{count} student. Zaznacz, kto zdobył nagrody, a przed przyznaniem przejrzysz wszystko jeszcze raz." + few: "%{categories}, %{count} studentów. Zaznacz, kto zdobył nagrody, a przed przyznaniem przejrzysz wszystko jeszcze raz." + many: "%{categories}, %{count} studentów. Zaznacz, kto zdobył nagrody, a przed przyznaniem przejrzysz wszystko jeszcze raz." + other: "%{categories}, %{count} studenta. Zaznacz, kto zdobył nagrody, a przed przyznaniem przejrzysz wszystko jeszcze raz." + categories: + one: "%{count} kategoria" + few: "%{count} kategorie" + many: "%{count} kategorii" + other: "%{count} kategorii" + search: "Szukaj studenta" + legend: "Legenda" + empty: "Puste" + marked_now: "Zaznaczone teraz" + awarded_locked: "Przyznane, nie do cofnięcia" + student: "Student" + select_column: "Zaznacz całą kolumnę" + everyone: "Wszyscy" + total: "Razem" + locked_title: "Przyznane, nie można cofnąć" + cell_awarded: "%{label}: przyznane" + cell_empty: "%{label}: puste" + nothing_marked: "Nic nie jest zaznaczone. Kliknij pola studentów, którzy zdobyli nagrody." + clear: "Wyczyść zaznaczenia" + review: "Przejrzyj i przyznaj" + no_columns_title: "Ten arkusz nie ma żadnej kolumny" + no_columns_body: "Dodaj kolumny w ustawieniach arkusza, żeby było za co nagradzać." + sheet_settings: "Ustawienia arkusza" + no_students_title: "W tej grupie nie ma jeszcze studentów" + no_students_body: "Zaproś studentów do grupy — arkusz czeka gotowy i nic w nim nie przepada." + invites: "Zaproszenia" + mobile_body: "Ocenianie w tabeli działa na komputerze. Ta tabela ma %{categories} i %{students}, więc na telefonie byłaby nieczytelna." + mobile_categories: + one: "%{count} kategorię" + few: "%{count} kategorie" + many: "%{count} kategorii" + other: "%{count} kategorii" + mobile_students: + one: "%{count} studenta" + few: "%{count} studentów" + many: "%{count} studentów" + other: "%{count} studenta" + mobile_hint: "Na telefonie sprawdzisz ostatnie zakupy i powiadomienia." + back_home: "Wróć na start" + # grading_controller.ts + js: + cells: + one: "Zaznaczone teraz: %{count} pole" + few: "Zaznaczone teraz: %{count} pola" + many: "Zaznaczone teraz: %{count} pól" + other: "Zaznaczone teraz: %{count} pola" + students: + one: "u %{count} studenta, razem" + few: "u %{count} studentów, razem" + many: "u %{count} studentów, razem" + other: "u %{count} studenta, razem" + recipients: + one: "%{count} studentowi" + few: "%{count} studentom" + many: "%{count} studentom" + other: "%{count} studenta" + cell_empty: "%{label}: puste" + cell_marked: "%{label}: zaznaczone" + locked: "To pole jest już przyznane i nie można go cofnąć." + + review_dialog: + awarding: "Przyznajesz" + warning: "Po zatwierdzeniu nie da się tego cofnąć. Sprawdź listę, zanim przyznasz nagrody." + confirm: "Przyznaj" diff --git a/config/locales/views/students_badges.en.yml b/config/locales/views/students_badges.en.yml new file mode 100644 index 00000000..2cdd9606 --- /dev/null +++ b/config/locales/views/students_badges.en.yml @@ -0,0 +1,33 @@ +en: + students_badges: + flash: + awarded: "Awarded the “%{name}” badge." + revoked: "Revoked the “%{name}” badge." + + helpers: + assign_lead: + one: "%{name} has %{held} of %{count} badge." + other: "%{name} has %{held} of %{count} badges." + owned: "Already has it" + effect_html: "Once awarded: %{discount} on items that count this badge%{discounting}.%{unlocks} They will see it in their deck." + effect_discounting: " (%{items})" + effect_unlocks_html: " Unlocks buying: %{items}." + revoke_html: "%{name} will lose the %{discount} discount%{unlocking}. Items bought earlier stay." + revoke_unlocking: " and the option to buy: %{items}" + + new: + page_title: "Award a badge - %{name}" + + form: + pick: "Award" + blank: "Choose a badge" + title: "Award a badge" + search: "Search for a badge" + no_match: "No badges with that name." + empty: "This group has no badges yet. Add them in the Badges tab, then come back here." + submit: "Choose a badge" + + confirm_destroy: + page_title: "Revoke badge %{name}" + title: "Revoke the “%{name}” badge?" + revoke: "Revoke badge" diff --git a/config/locales/views/students_badges.pl.yml b/config/locales/views/students_badges.pl.yml new file mode 100644 index 00000000..8de294f0 --- /dev/null +++ b/config/locales/views/students_badges.pl.yml @@ -0,0 +1,36 @@ +pl: + students_badges: + flash: + awarded: "Przyznano odznakę „%{name}”." + revoked: "Odebrano odznakę „%{name}”." + + helpers: + # The form follows the total: "z 1 odznaki", "z 3 odznak". + assign_lead: + one: "%{name} ma %{held} z %{count} odznaki." + few: "%{name} ma %{held} z %{count} odznak." + many: "%{name} ma %{held} z %{count} odznak." + other: "%{name} ma %{held} z %{count} odznaki." + owned: "Ma już" + effect_html: "Po przyznaniu: %{discount} na przedmioty, które uwzględniają tę odznakę%{discounting}.%{unlocks} Zobaczy ją w swojej talii." + effect_discounting: " (%{items})" + effect_unlocks_html: " Odblokuje zakup: %{items}." + revoke_html: "%{name} straci zniżkę %{discount}%{unlocking}. Kupione wcześniej przedmioty zostają." + revoke_unlocking: " i możliwość zakupu: %{items}" + + new: + page_title: "Przyznaj odznakę - %{name}" + + form: + pick: "Przyznaj" + blank: "Wybierz odznakę" + title: "Przyznaj odznakę" + search: "Szukaj odznaki" + no_match: "Brak odznak o tej nazwie." + empty: "W tej grupie nie ma jeszcze odznak. Dodaj je w zakładce Odznaki, a potem wróć tutaj." + submit: "Wybierz odznakę" + + confirm_destroy: + page_title: "Odbierz odznakę %{name}" + title: "Odebrać odznakę „%{name}”?" + revoke: "Odbierz odznakę" diff --git a/config/locales/views/students_items.en.yml b/config/locales/views/students_items.en.yml new file mode 100644 index 00000000..a310423b --- /dev/null +++ b/config/locales/views/students_items.en.yml @@ -0,0 +1,28 @@ +en: + students_items: + helpers: + inventory_lead: + one: "%{count} item. Tell the teacher in class when you want to use an item." + other: "%{count} items. Tell the teacher in class when you want to use an item." + purchase_line: "Bought %{when}, for %{price}" + purchase_teacher_line: "Bought %{when}" + purchase_teacher_discount: "Bought %{when}, discounted from %{list}" + shop_line: + one: "You can afford %{count} item in the shop right now." + other: "You can afford %{count} items in the shop right now." + + index: + own_title: "My items" + title: "Items — %{name}" + teacher_lead: + one: "%{count} item. The student settles using an item with you in class." + other: "%{count} items. The student settles using an item with you in class." + go_to_shop: "Go to the shop" + see_shop: "See the shop" + own_empty_title: "You don't have any items yet" + own_empty_body: "Collect currency in class and spend it in the shop — the items you buy will stay here with their purchase date." + empty_title: "The student hasn't bought anything yet" + empty_body: "Items will show up here with the price they paid and the purchase date." + + card: + removed: "Removed from the shop" diff --git a/config/locales/views/students_items.pl.yml b/config/locales/views/students_items.pl.yml new file mode 100644 index 00000000..1f3f6150 --- /dev/null +++ b/config/locales/views/students_items.pl.yml @@ -0,0 +1,34 @@ +pl: + students_items: + helpers: + inventory_lead: + one: "%{count} przedmiot. Wykorzystanie przedmiotu zgłaszasz prowadzącemu na zajęciach." + few: "%{count} przedmioty. Wykorzystanie przedmiotu zgłaszasz prowadzącemu na zajęciach." + many: "%{count} przedmiotów. Wykorzystanie przedmiotu zgłaszasz prowadzącemu na zajęciach." + other: "%{count} przedmiotu. Wykorzystanie przedmiotu zgłaszasz prowadzącemu na zajęciach." + purchase_line: "Kupione %{when}, za %{price}" + purchase_teacher_line: "Kupione %{when}" + purchase_teacher_discount: "Kupione %{when}, ze zniżką z %{list}" + shop_line: + one: "Stać cię teraz na %{count} przedmiot w sklepie." + few: "Stać cię teraz na %{count} przedmioty w sklepie." + many: "Stać cię teraz na %{count} przedmiotów w sklepie." + other: "Stać cię teraz na %{count} przedmiotu w sklepie." + + index: + own_title: "Moje przedmioty" + title: "Przedmioty — %{name}" + teacher_lead: + one: "%{count} przedmiot. Wykorzystanie przedmiotu student rozlicza z Tobą na zajęciach." + few: "%{count} przedmioty. Wykorzystanie przedmiotu student rozlicza z Tobą na zajęciach." + many: "%{count} przedmiotów. Wykorzystanie przedmiotu student rozlicza z Tobą na zajęciach." + other: "%{count} przedmiotu. Wykorzystanie przedmiotu student rozlicza z Tobą na zajęciach." + go_to_shop: "Przejdź do sklepu" + see_shop: "Zobacz sklep" + own_empty_title: "Nie masz jeszcze żadnego przedmiotu" + own_empty_body: "Zbieraj walutę na zajęciach i wydaj ją w sklepie — kupione przedmioty zostaną tutaj razem z datą zakupu." + empty_title: "Student nic jeszcze nie kupił" + empty_body: "Przedmioty pojawią się tu razem z ceną, jaką zapłacił, i datą zakupu." + + card: + removed: "Usunięty z oferty" diff --git a/config/locales/views/teachers.en.yml b/config/locales/views/teachers.en.yml new file mode 100644 index 00000000..cd77f1f6 --- /dev/null +++ b/config/locales/views/teachers.en.yml @@ -0,0 +1,64 @@ +en: + teachers: + flash: + added: "Added: %{name}." + removed: "Removed from the group: %{name}." + + helpers: + lead: "Assistant teachers help run this group." + lead_manage: "Only the owner can delete the group." + lead_read_only: "Only the group owner can add and remove teachers." + pool_hint: + one: "Type at least 2 characters of a name or e-mail. There is %{count} teacher at your university." + other: "Type at least 2 characters of a name or e-mail. There are %{count} teachers at your university." + candidate: "%{email}, %{university}" + + index: + title: "Teachers" + add: "Add a teacher" + teacher: "Teacher" + role: "Role" + added: "Added" + + row: + owner: "Owner" + created_group: "Created the group" + assistant: "Assistant teacher" + added: "Added" + remove_label: "Remove from the group: %{name}" + + new: + page_title: "Add a teacher" + + form: + title: "Add a teacher" + hint: "The teacher gets access to the group as soon as they're added." + placeholder: "Name or e-mail" + search: "Search for a teacher" + in_group: "Already in the group" + add_label: "Add %{name}" + overflow_hint: "Can't see the person? Type more letters or the e-mail." + no_results: "No results for" + no_results_hint: "Check the spelling or type the e-mail." + nobody: "Nobody to add: there are no other teacher accounts at your university yet." + # teacher_picker_controller.ts + js: + results: + one: "%{count} result" + other: "%{count} results" + overflow: ", showing the first %{max}" + query: "“%{query}”." + + confirm_destroy: + page_title: "Remove %{name} from the group?" + title: "Remove %{name} from the group?" + body: "They'll lose access to the group. Rewards they awarded stay with the students." + remove: "Remove from group" + + activerecord: + errors: + models: + story_group_teacher: + attributes: + user_id: + taken: "This person is already in the group." diff --git a/config/locales/views/teachers.pl.yml b/config/locales/views/teachers.pl.yml new file mode 100644 index 00000000..a3828d28 --- /dev/null +++ b/config/locales/views/teachers.pl.yml @@ -0,0 +1,68 @@ +pl: + teachers: + flash: + added: "Dodano: %{name}." + removed: "Usunięto z grupy: %{name}." + + helpers: + lead: "Nauczyciele wspomagający pomagają prowadzić tę grupę." + lead_manage: "Grupę może usunąć tylko właściciel." + lead_read_only: "Dodawać i usuwać nauczycieli może tylko właściciel grupy." + pool_hint: + one: "Wpisz co najmniej 2 znaki imienia, nazwiska albo e-maila. W Twojej uczelni jest %{count} nauczyciel." + few: "Wpisz co najmniej 2 znaki imienia, nazwiska albo e-maila. W Twojej uczelni są %{count} nauczyciele." + many: "Wpisz co najmniej 2 znaki imienia, nazwiska albo e-maila. W Twojej uczelni jest %{count} nauczycieli." + other: "Wpisz co najmniej 2 znaki imienia, nazwiska albo e-maila. W Twojej uczelni jest %{count} nauczyciela." + candidate: "%{email}, %{university}" + + index: + title: "Nauczyciele" + add: "Dodaj nauczyciela" + teacher: "Nauczyciel" + role: "Rola" + added: "Dodano" + + row: + owner: "Właściciel" + created_group: "Utworzył grupę" + assistant: "Nauczyciel wspomagający" + added: "Dodano" + remove_label: "Usuń z grupy: %{name}" + + new: + page_title: "Dodaj nauczyciela" + + form: + title: "Dodaj nauczyciela" + hint: "Nauczyciel dostanie dostęp do grupy od razu po dodaniu." + placeholder: "Imię, nazwisko albo e-mail" + search: "Szukaj nauczyciela" + in_group: "Już w grupie" + add_label: "Dodaj %{name}" + overflow_hint: "Nie widzisz tej osoby? Wpisz więcej liter albo e-mail." + no_results: "Brak wyników dla" + no_results_hint: "Sprawdź pisownię albo wpisz e-mail." + nobody: "Nie ma kogo dodać: w Twojej uczelni nie ma jeszcze innych kont nauczycieli." + # teacher_picker_controller.ts + js: + results: + one: "%{count} wynik" + few: "%{count} wyniki" + many: "%{count} wyników" + other: "%{count} wyniku" + overflow: ", pokazujemy %{max} pierwszych" + query: "„%{query}”." + + confirm_destroy: + page_title: "Usunąć %{name} z grupy?" + title: "Usunąć %{name} z grupy?" + body: "Straci dostęp do grupy. Nagrody, które przyznał, zostają u studentów." + remove: "Usuń z grupy" + + activerecord: + errors: + models: + story_group_teacher: + attributes: + user_id: + taken: "Ta osoba jest już w grupie." diff --git a/config/routes.rb b/config/routes.rb index 17458dfa..5797c30a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -3,6 +3,8 @@ Rails.application.routes.draw do mount LetterOpenerWeb::Engine, at: '/letter_opener' if Rails.env.development? + resource :locale, only: :update + resources :join, param: :code, only: %i[show new] do collection do post :create diff --git a/test/controllers/locales_controller_test.rb b/test/controllers/locales_controller_test.rb new file mode 100644 index 00000000..641c3046 --- /dev/null +++ b/test/controllers/locales_controller_test.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require 'test_helper' + +class LocalesControllerTest < ActionDispatch::IntegrationTest + test 'renders in Polish by default' do + get login_path + + assert_select 'html[lang=pl]' + end + + test 'uses browser language when no locale was chosen' do + get login_path, headers: { 'Accept-Language' => 'en-US,en;q=0.9,pl;q=0.8' } + + assert_select 'html[lang=en]' + end + + test 'falls back to Polish for unsupported browser language' do + get login_path, headers: { 'Accept-Language' => 'de-DE,de;q=0.9' } + + assert_select 'html[lang=pl]' + end + + test 'switches locale and redirects back' do + patch locale_path, params: { locale: 'en' }, headers: { 'HTTP_REFERER' => login_url } + + assert_redirected_to login_url + assert_equal 'en', cookies[:locale] + + get login_path + assert_select 'html[lang=en]' + end + + test 'chosen locale wins over browser language' do + patch locale_path, params: { locale: 'pl' } + + get login_path, headers: { 'Accept-Language' => 'en-US,en;q=0.9' } + assert_select 'html[lang=pl]' + end + + test 'ignores unsupported locale' do + patch locale_path, params: { locale: 'de' } + + assert_redirected_to root_path + assert_nil cookies[:locale] + end + + test 'logged in user can switch locale' do + sign_in FactoryBot.create(:user, :student) + + patch locale_path, params: { locale: 'en' }, headers: { 'HTTP_REFERER' => home_url } + + assert_redirected_to home_url + get home_path + assert_select 'html[lang=en]' + end +end diff --git a/test/controllers/students_badges_controller_test.rb b/test/controllers/students_badges_controller_test.rb index 83425b8f..d570df56 100644 --- a/test/controllers/students_badges_controller_test.rb +++ b/test/controllers/students_badges_controller_test.rb @@ -39,4 +39,5 @@ class StudentsBadgesControllerTest < ActionDispatch::IntegrationTest assert_turbo_redirected_to story_group_student_url(@story_group, @story_group_student) end + end diff --git a/test/controllers/students_controller_test.rb b/test/controllers/students_controller_test.rb index 74203898..939aadc4 100644 --- a/test/controllers/students_controller_test.rb +++ b/test/controllers/students_controller_test.rb @@ -44,4 +44,23 @@ class StudentsControllerTest < ActionDispatch::IntegrationTest assert_turbo_redirected_to story_group_students_url(@story_group) end + + test 'lives notice uses Polish plural forms' do + @story_group_student.update!(lives: 1) + + post update_lives_story_group_student_url(@story_group, @story_group_student, change: 1) + assert_equal "#{@story_group_student.display_name} ma teraz 2 życia.", flash[:notice] + + @story_group_student.update!(lives: 4) + post update_lives_story_group_student_url(@story_group, @story_group_student, change: 1) + assert_equal "#{@story_group_student.display_name} ma teraz 5 żyć.", flash[:notice] + end + + test 'lives notice in English' do + patch locale_path, params: { locale: 'en' } + @story_group_student.update!(lives: 0) + + post update_lives_story_group_student_url(@story_group, @story_group_student, change: 1) + assert_equal "#{@story_group_student.display_name} now has 1 life.", flash[:notice] + end end diff --git a/test/integration/translations_test.rb b/test/integration/translations_test.rb new file mode 100644 index 00000000..d887a4ff --- /dev/null +++ b/test/integration/translations_test.rb @@ -0,0 +1,535 @@ +# frozen_string_literal: true + +require 'test_helper' + +# Opens every screen in both locales. raise_on_missing_translations is on in +# test, so a missing key fails the render; on top of that an English page must +# not contain Polish letters — a sign of wording that never went through I18n. +class TranslationsTest < ActionDispatch::IntegrationTest + POLISH_LETTERS = /[ąćęłńóśźżĄĆĘŁŃÓŚŹŻ]/ + + setup do + @teacher = create(:user, :teacher, full_name: 'Anna Smith') + @group = create(:story_group, owner: @teacher, name: 'Algorithms', currency_name: 'Coins', + ranking_enabled: true,) + + @start_rank = create(:rank, story_group: @group, name: 'Novice', required_currency_value: 0, discount: 0) + @gold = create(:rank, story_group: @group, name: 'Gold', required_currency_value: 50, discount: 10) + @badge = create(:badge, story_group: @group, name: 'Helper', discount: 5) + @badge2 = create(:badge, story_group: @group, name: 'Speaker', discount: 7) + + @item = create(:item, story_group: @group, name: 'Retake', price: 40, unlock_rank: @gold, + min_rank_for_discount: @start_rank,) + @item.unlock_badges << @badge + @item.discount_badges << @badge2 + @cheap = create(:item, story_group: @group, name: 'Hint', price: 1, can_buy_at_0_lives: true) + + @invite = create(:story_group_invite, story_group: @group) + @template = create(:activity_group_template, story_group: @group, base_name: 'Lab') + create(:activity_group_template_category, activity_group_template: @template) + @sheet = create(:activity_group, story_group: @group, activity_group_template: @template, name: 'Lab 1') + @category = create(:activity_group_category, activity_group: @sheet) + @attendance = create(:activity_group_category, activity_group: @sheet, didactic_description: 'Attendance', + position: 1,) + + @assistant = create(:user, :teacher, full_name: 'Bob Brown') + @assistance = create(:story_group_teacher, story_group: @group, user: @assistant) + + @student = create(:user, :student, full_name: 'Carl Green') + @membership = create(:story_group_student, story_group: @group, user: @student, nickname: 'Comet', + lives: 2, current_currency: 30, total_currency: 30,) + create(:students_activity_group_category, student: @membership, activity_group_category: @category) + @awarded = create(:students_badge, story_group_student: @membership, badge: @badge) + create(:students_item, story_group_student: @membership, item: @cheap, price_paid: 1) + create(:currency_transaction, student: @membership, amount: 10, kind: :reward, + transactionable: @category,) + create(:currency_transaction, student: @membership, amount: -1, kind: :purchase, transactionable: @cheap) + CurrencyAdjusterService.new(student: @membership, granted_by_user: @teacher).adjust(3) + create(:notification, user: @teacher, story_group: @group, story_group_student: @membership, item: @cheap) + + @other_group = create(:story_group, owner: @teacher, name: 'Databases', currency_name: 'Gems') + @other_invite = create(:story_group_invite, story_group: @other_group) + end + + %w[pl en].each do |locale| + test "teacher screens render in #{locale}" do + choose(locale) + sign_in @teacher + + visit_all(locale, teacher_paths) + end + + test "student screens render in #{locale}" do + choose(locale) + sign_in @student + + visit_all(locale, student_paths) + end + + test "logged-out screens render in #{locale}" do + choose(locale) + + visit_all(locale, [login_path, new_auth_passwordless_path]) + + post auth_passwordless_path, params: { email: 'nobody@example.com' } + visit_all(locale, [auth_passwordless_inbox_path]) + end + + test "the login e-mail is written in #{locale}" do + mail = I18n.with_locale(locale) do + PasswordlessMailer.with(token_link: 'https://example.com/t', email: @student.email, + user_name: @student.full_name,).token_email.message + end + + assert_equal I18n.t('passwordless_mailer.token_email.subject', locale: locale), mail.subject + assert_no_polish(mail.body.encoded, 'login e-mail') if locale == 'en' + end + + # The tests below submit forms: a success must leave a translated notice + # (checked as a string and again on the page it lands on), a rejected form + # must re-render with translated validation messages. + test "group settings forms answer in #{locale}" do + choose(locale) + sign_in @teacher + + patch story_group_path(@group), params: { story_group: { name: 'Algorithms II' } } + assert_flash_shown(locale) + patch story_group_path(@group), params: { story_group: { name: '' } } + assert_form_rejected(locale) + post story_groups_path, params: { story_group: { name: '' }, setup: { path: 'custom' } } + assert_form_rejected(locale) + + patch story_group_ranking_path(@group), params: { story_group: { ranking_enabled: '1', ranking_mode: 'full' } } + assert_flash_shown(locale) + patch story_group_ranking_path(@group), params: { story_group: { ranking_enabled: '0' } } + assert_flash_shown(locale) + + post story_group_teachers_path(@group), + params: { story_group_teacher: { user_id: create(:user, :teacher, full_name: 'Dana White').id } } + assert_flash_shown(locale) + delete story_group_teacher_path(@group, @assistance) + assert_flash_shown(locale) + + post story_group_invites_path(@group), + params: { story_group_invite: { limit_enabled: '1', max_uses: '10' } } + assert_flash_shown(locale) + post story_group_invites_path(@group), + params: { story_group_invite: { expiry_enabled: '1', expires_on: '2000-01-01' } } + assert_form_rejected(locale) + patch story_group_invite_path(@group, @invite), params: { story_group_invite: { limit_enabled: '0' } } + assert_flash_shown(locale) + delete story_group_invite_path(@group, @invite) + assert_flash_shown(locale) + + delete story_group_path(@group), params: { confirm: 'wrong name' } + assert_form_rejected(locale) + delete story_group_path(@group), params: { confirm: @group.reload.name } + assert_flash_shown(locale) + end + + test "rank, badge and item forms answer in #{locale}" do + choose(locale) + sign_in @teacher + + post story_group_ranks_path(@group), params: { rank: rank_params(name: 'Silver', required_currency_value: 20) } + assert_flash_shown(locale) + post story_group_ranks_path(@group), params: { rank: rank_params(name: 'Copy', required_currency_value: 50) } + assert_form_rejected(locale) + patch story_group_rank_path(@group, @gold), params: { rank: { name: 'Platinum' } } + assert_flash_shown(locale) + delete story_group_rank_path(@group, @gold) # Retake unlocks at this rank, so it is blocked + assert_flash_shown(locale) + + post story_group_badges_path(@group), params: { badge: badge_params(name: 'Mentor') } + assert_flash_shown(locale) + post story_group_badges_path(@group), params: { badge: badge_params(name: '') } + assert_form_rejected(locale) + patch story_group_badge_path(@group, @badge), params: { badge: { name: 'Big helper' } } + assert_flash_shown(locale) + delete story_group_badge_path(@group, @badge2) + assert_flash_shown(locale) + + post story_group_items_path(@group), params: { item: item_params(name: 'Pass') } + assert_flash_shown(locale) + post story_group_items_path(@group), params: { item: item_params(name: '', price: -1) } + assert_form_rejected(locale) + patch story_group_item_path(@group, @item), params: { item: { name: 'Second try' } } + assert_flash_shown(locale) + delete story_group_item_path(@group, @item) + assert_flash_shown(locale) + end + + test "sheet and grading forms answer in #{locale}" do + choose(locale) + sign_in @teacher + + post story_group_activity_group_templates_path(@group), params: { + activity_group_template: { + base_name: 'Quiz', + categories_attributes: { '0' => { didactic_description: 'Score', reward: 5, position: 0 } }, + }, + } + assert_flash_shown(locale) + post story_group_activity_group_templates_path(@group), + params: { activity_group_template: { base_name: 'Empty' } } + assert_form_rejected(locale) + patch story_group_activity_group_template_path(@group, @template), + params: { activity_group_template: { base_name: 'Lab+' } } + assert_flash_shown(locale) + + post story_group_activity_groups_path(@group), + params: { activity_group: { activity_group_template_id: @template.id, mode: 'single', name: 'Lab 2' } } + assert_flash_shown(locale) + post story_group_activity_groups_path(@group, count: 3), + params: { activity_group: { activity_group_template_id: @template.id, mode: 'many' } } + assert_flash_shown(locale) + + grading = story_group_activity_group_students_activity_group_categories_path(@group, @sheet) + patch grading + assert_flash_shown(locale) + patch grading, params: { completions: { @membership.id => { @attendance.id => '1' } } } + assert_flash_shown(locale) + + delete story_group_activity_group_path(@group, @sheet) + assert_flash_shown(locale) + delete story_group_activity_group_template_path(@group, @template) + assert_flash_shown(locale) + end + + test "actions on a student answer in #{locale}" do + choose(locale) + sign_in @teacher + student = story_group_student_path(@group, @membership) + + patch student, params: { story_group_student: { lives: 3 } } + assert_flash_shown(locale) + patch student, params: { story_group_student: { lives: -1 } } + assert_form_rejected(locale) + post update_lives_story_group_student_path(@group, @membership), params: { change: -1 } + assert_flash_shown(locale) + patch student, params: { story_group_student: { lives: 0 } } + post update_lives_story_group_student_path(@group, @membership), params: { change: -1 } + assert_flash_shown(locale) + + adjustment = story_group_student_currency_adjustment_path(@group, @membership) + post adjustment, params: { currency_adjustment: { sign: '1', amount: '5' } } + assert_flash_shown(locale) + post adjustment, params: { currency_adjustment: { sign: '1', amount: '' } } + assert_form_rejected(locale) + post adjustment, params: { currency_adjustment: { sign: '-1', amount: '9999' } } + assert_form_rejected(locale) + + post story_group_student_badges_path(@group, @membership), params: { students_badge: { badge_id: @badge2.id } } + assert_flash_shown(locale) + post story_group_student_badges_path(@group, @membership), params: { students_badge: { badge_id: @badge2.id } } + assert_form_rejected(locale) + delete story_group_student_badge_path(@group, @membership, @awarded) + assert_flash_shown(locale) + + delete student + assert_flash_shown(locale) + end + + test "student actions answer in #{locale}" do + choose(locale) + sign_in @student + + post buy_story_group_shop_path(@group, @cheap) + assert_flash_shown(locale) + post buy_story_group_shop_path(@group, @item) # locked behind the Gold rank and a badge + assert_flash_shown(locale) + + patch story_group_membership_path(@group), params: { story_group_student: { nickname: 'Rocket' } } + assert_flash_shown(locale) + patch story_group_membership_path(@group), params: { story_group_student: { nickname: 'x' * 200 } } + assert_form_rejected(locale) + + post join_index_path, params: { code: 'NOPE00' } + assert_form_rejected(locale) + post join_index_path, params: { code: @other_invite.code } + follow_redirect! + assert_response :success + assert_translated(locale, response.body, 'join done') + + delete story_group_membership_path(@group) + assert_flash_shown(locale) + end + + test "sign-in and access alerts are in #{locale}" do + choose(locale) + + get home_path + assert_flash_shown(locale) + get auth_passwordless_verify_path(token: 'bogus') + assert_flash_shown(locale) + post auth_passwordless_path, params: { email: @teacher.email } + post auth_passwordless_path, params: { email: @teacher.email } # only a resend leaves a notice + assert_flash_shown(locale) + + sign_in @teacher + get login_path + assert_flash_shown(locale) + get story_group_path(id: 0) + assert_flash_shown(locale) + end + + test "global admin screens render in #{locale}" do + choose(locale) + sign_in create(:user, :global_admin) + org, org_admin, = organization_with_people + + visit_all(locale, [ + home_path, + organizations_path, + organization_path(org), + new_organization_path, + edit_organization_path(org), + confirm_destroy_organization_path(org), + new_admin_organization_path(org), + confirm_destroy_organization_admin_path(org, org_admin), + ],) + end + + test "organization admin screens render in #{locale}" do + choose(locale) + org, org_admin, member = organization_with_people + org_admin.update!(first_login: false) # an invited admin is sent to account setup first + sign_in org_admin + + visit_all(locale, [ + home_path, + organization_path(org), + new_user_organization_path(org), + edit_organization_user_path(org, member), + confirm_destroy_organization_user_path(org, member), + ],) + end + + test "organization forms answer in #{locale}" do + choose(locale) + sign_in create(:user, :global_admin) + org, org_admin, member = organization_with_people + + post organizations_path, params: { organization: { name: org.name, max_members: 5 } } # names are unique + assert_form_rejected(locale) + post organizations_path, params: { organization: { name: 'Polytechnic', max_members: 5 } } + assert_flash_shown(locale) + patch organization_path(org), params: { organization: { max_members: 1 } } # below the 3 accounts + assert_form_rejected(locale) + patch organization_path(org), params: { organization: { max_members: 10 } } + assert_flash_shown(locale) + + post add_admin_organization_path(org), params: { user: { email: 'new.admin@example.com' } } + assert_flash_shown(locale) + post add_admin_organization_path(org), params: { user: { email: '' } } + assert_form_rejected(locale) + delete organization_admin_path(org, org_admin) + assert_flash_shown(locale) + + sign_in create(:user, :organization_admin, organization: org.reload) + post add_user_organization_path(org), params: { + user: { + email: 'new.user@example.com', + full_name: 'Eve Black', + role: 'student', + }, + } + assert_flash_shown(locale) + post add_user_organization_path(org), params: { user: { email: '', full_name: '', role: 'student' } } + assert_form_rejected(locale) + patch organization_user_path(org, member), params: { user: { full_name: 'Fred Gray' } } + assert_flash_shown(locale) + delete organization_user_path(org, member) + assert_flash_shown(locale) + + sign_in create(:user, :global_admin) + delete organization_path(org), params: { confirm: 'wrong' } + assert_form_rejected(locale) + delete organization_path(org), params: { confirm: org.name } + assert_flash_shown(locale) + end + + test "account setup answers in #{locale}" do + choose(locale) + sign_in create(:user, :organization_admin, first_login: true, full_name: nil, organization: create(:organization)) + + visit_all(locale, [edit_account_setup_path]) + patch account_setup_path, params: { user: { full_name: '' } } + assert_form_rejected(locale) + patch account_setup_path, params: { user: { full_name: 'Grace Hall' } } + assert_flash_shown(locale) + end + + test "the invitation e-mail is written in #{locale}" do + mail = I18n.with_locale(locale) do + InvitationMailer.with(token_link: 'https://example.com/t', email: 'a@example.com', + organization_name: 'Polytechnic', role: :admin,).invitation_email.message + end + + assert_equal I18n.t('invitation_mailer.invitation_email.subject', locale: locale), mail.subject + mail.parts.each { |part| assert_no_polish(part.body.decoded, 'invitation e-mail') } if locale == 'en' + end + end + + test 'a quick-start group is created in the teacher’s language' do + choose('en') + sign_in @teacher + + post story_groups_path, params: { + story_group: { name: 'Physics', currency_name: 'Stars', default_lives: 3 }, + setup: { path: 'quick', pack: 'scifi', classes: 12 }, + } + + group = StoryGroup.find_by!(name: 'Physics') + assert_equal %w[Cadet Pilot Navigator Commander Admiral], group.ranks.by_threshold.map(&:name) + assert_equal 'Classes', group.activity_group_templates.first.base_name + end + + private + + def choose(locale) + patch locale_path, params: { locale: locale } + end + + def visit_all(locale, paths) + paths.each do |path| + get path + assert_response :success, "#{path} (#{locale})" + assert_select "html[lang=#{locale}]" if response.body.include?(']*target="([^"]+)"/, 1] + flunk "no redirect after #{where}" unless target + get target + follow_redirect! while response.redirect? + assert_response :success, "page after #{where}" + assert_translated(locale, response.body, "page after #{where}") + end + + def assert_form_rejected(locale) + where = "#{request.request_method} #{request.path}" + assert_response :unprocessable_content, where + assert_translated(locale, response.body, where) + end + + def assert_translated(locale, html, where) + assert_no_match(/translation missing/i, html, "missing translation in #{where}") + assert_no_polish(html, where) if locale == 'en' + end + + # An organization with an invited administrator and a student, so the + # lists, the "invitation sent" markers and the capacity text all render. + def organization_with_people + org = create(:organization, max_members: 3) + admin = create(:user, :organization_admin, organization: org, first_login: true) + member = create(:user, :student, organization: org, first_login: true) + create(:story_group, owner: create(:user, :teacher, organization: org)) + + [org, admin, member] + end + + def rank_params(**attrs) + { discount: 5, icon_glyph: 'chev1' }.merge(attrs) + end + + def badge_params(**attrs) + { didactic_description: 'Helps others', discount: 3, icon_glyph: 'rabbit' }.merge(attrs) + end + + def item_params(**attrs) + { didactic_description: 'Skip one task', price: 5, icon_glyph: 'shield' }.merge(attrs) + end + + def assert_no_polish(html, where) + text = Nokogiri::HTML(html).text + match = text.match(POLISH_LETTERS) + return pass unless match + + snippet = text[[match.begin(0) - 40, 0].max, 80].squish + flunk "Polish text left in #{where}: …#{snippet}…" + end + + def teacher_paths + [ + home_path, + story_groups_path, + new_story_group_path, + preset_preview_story_groups_path(pack: 'fantasy', classes: 12, currency_name: 'Coins'), + story_group_path(@group), + edit_story_group_path(@group), + confirm_destroy_story_group_path(@group), + created_story_group_path(@group), + story_group_students_path(@group), + *StudentSheet::TABS.map { |tab| story_group_student_path(@group, @membership, tab: tab) }, + edit_story_group_student_path(@group, @membership), + confirm_destroy_story_group_student_path(@group, @membership), + story_group_student_students_items_path(@group, @membership), + new_story_group_student_badge_path(@group, @membership), + confirm_destroy_story_group_student_badge_path(@group, @membership, @awarded), + new_story_group_student_currency_adjustment_path(@group, @membership), + story_group_items_path(@group), + new_story_group_item_path(@group), + edit_story_group_item_path(@group, @item), + confirm_destroy_story_group_item_path(@group, @item), + story_group_ranks_path(@group), + new_story_group_rank_path(@group), + edit_story_group_rank_path(@group, @gold), + confirm_destroy_story_group_rank_path(@group, @gold), + story_group_badges_path(@group), + new_story_group_badge_path(@group), + edit_story_group_badge_path(@group, @badge), + confirm_destroy_story_group_badge_path(@group, @badge), + story_group_invites_path(@group), + new_story_group_invite_path(@group), + edit_story_group_invite_path(@group, @invite), + story_group_invite_path(@group, @invite), + quick_story_group_invites_path(@group), + confirm_destroy_story_group_invite_path(@group, @invite), + story_group_teachers_path(@group), + new_story_group_teacher_path(@group), + confirm_destroy_story_group_teacher_path(@group, @assistance), + story_group_activity_groups_path(@group), + new_story_group_activity_group_path(@group, template_id: @template.id), + edit_story_group_activity_group_path(@group, @sheet), + confirm_destroy_story_group_activity_group_path(@group, @sheet), + new_story_group_activity_group_template_path(@group), + edit_story_group_activity_group_template_path(@group, @template), + confirm_destroy_story_group_activity_group_template_path(@group, @template), + edit_story_group_activity_group_students_activity_group_categories_path(@group, @sheet), + story_group_ranking_path(@group), + confirm_visibility_story_group_ranking_path(@group, enabled: false), + confirm_mode_story_group_ranking_path(@group, mode: 'full'), + notifications_path, + ] + end + + def student_paths + [ + home_path, + story_groups_path, + story_group_path(@group), + story_group_shop_index_path(@group), + confirm_buy_story_group_shop_path(@group, @cheap), + story_group_student_students_items_path(@group, @membership), + story_group_student_currency_transactions_path(@group, @membership), + story_group_badges_path(@group), + story_group_ranks_path(@group), + story_group_ranking_path(@group), + edit_story_group_membership_path(@group), + nickname_story_group_membership_path(@group), + confirm_leave_story_group_membership_path(@group), + new_join_path, + join_path(code: @other_invite.code), + ] + end +end diff --git a/test/models/item_test.rb b/test/models/item_test.rb index 9ffb23b1..d8c5009f 100644 --- a/test/models/item_test.rb +++ b/test/models/item_test.rb @@ -140,4 +140,13 @@ class ItemTest < ActiveSupport::TestCase assert_equal %w[Tani Drogi Item], @story_group.items.by_price.pluck(:name) end + + test 'destroying an item takes its purchase notifications with it' do + notification = FactoryBot.create(:notification, story_group: @story_group, story_group_student: @student, + item: @item,) + + @item.destroy! + + assert_not Notification.exists?(notification.id) + end end diff --git a/test/models/organization_test.rb b/test/models/organization_test.rb index e314cd99..a41ecf83 100644 --- a/test/models/organization_test.rb +++ b/test/models/organization_test.rb @@ -30,7 +30,7 @@ class OrganizationTest < ActiveSupport::TestCase organization.max_members = 1 assert_not organization.valid? - assert_includes organization.errors[:max_members], "can't be lower than the current number of members (2)" + assert organization.errors.added?(:max_members, :below_members, count: 2) end test 'requires unique name' do diff --git a/test/models/story_group_student_test.rb b/test/models/story_group_student_test.rb index e337aa34..63c46dac 100644 --- a/test/models/story_group_student_test.rb +++ b/test/models/story_group_student_test.rb @@ -3,7 +3,6 @@ require 'test_helper' class StoryGroupStudentTest < ActiveSupport::TestCase - setup do @owner = ::FactoryBot.create(:user) @student_user = ::FactoryBot.create(:user) @@ -60,16 +59,14 @@ class StoryGroupStudentTest < ActiveSupport::TestCase assert_nil student.next_rank end - test 'removing a student who bought something clears their purchase notifications' do - story_group = FactoryBot.create(:story_group) - student = FactoryBot.create(:story_group_student, story_group: story_group, - user: FactoryBot.create(:user, :student),) - item = FactoryBot.create(:item, story_group: story_group) - FactoryBot.create(:notification, user: story_group.owner, story_group: story_group, - story_group_student: student, item: item,) - - assert_difference 'Notification.count', -1 do - student.destroy! - end + test 'a student who bought something can still be removed, with the purchase notifications' do + group = StoryGroup.create!(name: 'Shop Group', currency_name: 'Gold', owner: @owner) + student = StoryGroupStudent.create!(user: @student_user, story_group: group) + notification = ::FactoryBot.create(:notification, user: @owner, story_group: group, story_group_student: student, + item: ::FactoryBot.create(:item, story_group: group),) + + student.destroy! + + assert_not Notification.exists?(notification.id) end end diff --git a/test/models/story_group_test.rb b/test/models/story_group_test.rb index 73edc248..69eb7240 100644 --- a/test/models/story_group_test.rb +++ b/test/models/story_group_test.rb @@ -12,4 +12,16 @@ class StoryGroupTest < ActiveSupport::TestCase assert_equal true, story_group.invalid? assert_equal true, story_group.errors[:name].any? end + + test 'a group with purchases can be destroyed, notifications included' do + story_group = FactoryBot.create(:story_group) + student = FactoryBot.create(:story_group_student, story_group: story_group, user: FactoryBot.create(:user)) + item = FactoryBot.create(:item, story_group: story_group) + notification = FactoryBot.create(:notification, story_group: story_group, story_group_student: student, + item: item,) + + story_group.destroy! + + assert_not Notification.exists?(notification.id) + end end diff --git a/test/models/user_test.rb b/test/models/user_test.rb index d2ddf890..4a262bb1 100644 --- a/test/models/user_test.rb +++ b/test/models/user_test.rb @@ -16,7 +16,7 @@ class UserTest < ActiveSupport::TestCase user = FactoryBot.build(:user, organization: organization) assert_not user.valid? - assert_includes user.errors[:organization], 'has reached its member limit' + assert user.errors.added?(:organization, :member_limit) end test 'existing member can be updated in a full organization' do diff --git a/test/services/dashboard/teacher_test.rb b/test/services/dashboard/teacher_test.rb index 0ee04683..576289c2 100644 --- a/test/services/dashboard/teacher_test.rb +++ b/test/services/dashboard/teacher_test.rb @@ -4,6 +4,9 @@ class Dashboard::TeacherTest < ActiveSupport::TestCase setup do + # Purchases are bucketed by calendar day, so "1.hour.ago" must not cross + # midnight — pin the clock to midday. travel_back runs after each test. + travel_to Time.current.noon @teacher = FactoryBot.create(:user, role: :teacher) end @@ -30,9 +33,9 @@ class Dashboard::TeacherTest < ActiveSupport::TestCase dashboard = Dashboard::Teacher.new(user: @teacher).load - assert_equal 1, dashboard.purchases_on('Dziś').size - assert_equal 1, dashboard.purchases_on('Wczoraj').size - assert_equal 1, dashboard.purchases_on('Wcześniej').size + assert_equal 1, dashboard.purchases_on(:today).size + assert_equal 1, dashboard.purchases_on(:yesterday).size + assert_equal 1, dashboard.purchases_on(:earlier).size assert_equal 2, dashboard.recent_purchases.size assert_equal 1, dashboard.recent_group_count end diff --git a/test/services/starter_pack_builder_test.rb b/test/services/starter_pack_builder_test.rb index 9518abb0..2a3cf4c0 100644 --- a/test/services/starter_pack_builder_test.rb +++ b/test/services/starter_pack_builder_test.rb @@ -20,7 +20,7 @@ def build(pack: 'neutral', classes: 12, selection: {}) assert_equal 7, @story_group.items.count assert_equal 1, @story_group.activity_group_templates.count assert_equal 8, @story_group.activity_group_templates.first.categories.count - assert_equal StarterPack::TEMPLATE_NAME, + assert_equal StarterPack.template_name, @story_group.activity_group_templates.first.base_name assert_equal 5, result.ranks.size end @@ -34,7 +34,7 @@ def build(pack: 'neutral', classes: 12, selection: {}) test 'categories keep the preset order' do build - assert_equal StarterPack::CATEGORIES.map(&:first), + assert_equal StarterPack.category_names, @story_group.activity_group_templates.first.categories.map(&:didactic_description) end