Compare commits

...
Author SHA1 Message Date
eminuxandCursor 3ed70b6999 Aggiorna marketing: prezzi lancio, Funzionalità narrativa e link store.
Allinea i piani Light/Full ai prezzi di lancio, ripensa /funzionalita per vendere il prodotto e aggiunge i badge App Store/Play nella home.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-14 23:00:17 +02:00
eminuxandCursor bc2257efd2 Aggiunge la pagina pubblica /support compatibile con App Store Review.
Fornisce assistenza multilingua senza CTA commerciali e riusa l’email di supporto configurabile.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-11 19:37:20 +02:00
Emiliano FrascaroandCursor 4af4fa68ac Prepara iOS build 32 per App Store: icona opaca e orientamenti iPad.
Rimuove l'alpha dall'AppIcon e dichiara tutte le orientazioni richieste dal multitasking iPad, così l'upload ASC non fallisce più.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-08 22:58:27 +02:00
Emiliano FrascaroandCursor 45bdda7c95 Semplifica la home partite a un solo CTA e sistema la nav iOS.
Rimuove il pulsante ridondante «Partita programmata», migliora il padding dei bottoni e fa di splash/login/matches root vere così non compare più il chevron indietro spurio.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-08 18:54:44 +02:00
Emiliano FrascaroandCursor b926531447 Allinea la versione iOS a Android 2.0.10 e aggiorna la doc di gap.
Bump marketing/build a 2.0.10/31, copy EN forgot password e checklist password policy.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-08 18:39:43 +02:00
eminux da6fc1d523 Merge branch 'feature/password-policy'
Policy password, errori API localizzati, fix UI mobile marketing e handoff iOS.
2026-08-08 14:19:35 +02:00
eminuxandCursor 1d1cbf9f3f Localizza errori API, sistema layout mobile e documenta allineamento iOS.
Gli header Accept-Language dalle app e i fix di padding/menu sul web chiudono il giro password-policy; il doc guida il lavoro su Mac.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-08 14:14:42 +02:00
eminuxandCursor dd9901519a Rifiuta il riuso della password attuale in cambio e reset.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-08 11:31:47 +02:00
eminuxandCursor 53449c5d3c Allinea le password di seed alla nuova policy di complessità.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-08 11:30:51 +02:00
eminuxandCursor db908e3109 Rafforza i requisiti password con regole di complessità di mercato.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-08 11:30:37 +02:00
eminux 5857f60d79 Merge branch 'feature/account-management'
Gestione account MVP: profilo, cambio password e forgot password su web/Android/iOS.
2026-08-08 10:39:08 +02:00
108 changed files with 4115 additions and 795 deletions
@@ -9,13 +9,23 @@ module Admin
return render :edit, status: :unprocessable_entity return render :edit, status: :unprocessable_entity
end end
if params[:password].blank? || params[:password].length < 8 if params[:password] != params[:password_confirmation]
flash.now[:alert] = t("admin.flash.password_too_short") flash.now[:alert] = t("admin.flash.password_mismatch")
return render :edit, status: :unprocessable_entity return render :edit, status: :unprocessable_entity
end end
if params[:password] != params[:password_confirmation] if (code = PasswordComplexity.violation(params[:password]))
flash.now[:alert] = t("admin.flash.password_mismatch") key = case code
when :blank, :too_short then :password_too_short
when :too_long then :password_too_long
else :password_too_weak
end
flash.now[:alert] = t("admin.flash.#{key}")
return render :edit, status: :unprocessable_entity
end
if PasswordComplexity.same_as_current?(current_admin_account, params[:password])
flash.now[:alert] = t("admin.flash.password_same_as_current")
return render :edit, status: :unprocessable_entity return render :edit, status: :unprocessable_entity
end end
@@ -8,7 +8,7 @@ module Api
def update def update
name = params[:name].to_s.strip name = params[:name].to_s.strip
if name.blank? if name.blank?
return render json: { error: "Name is required" }, status: :unprocessable_entity return render json: { error: I18n.t("flash.accounts.name_required") }, status: :unprocessable_entity
end end
if current_user.update(name: name) if current_user.update(name: name)
@@ -30,7 +30,7 @@ module Api
return render json: { error: password_error_message(result.error) }, status: :unprocessable_entity return render json: { error: password_error_message(result.error) }, status: :unprocessable_entity
end end
render json: { message: "Password updated" } render json: { message: I18n.t("flash.accounts.password_updated") }
end end
private private
@@ -46,10 +46,13 @@ module Api
def password_error_message(code) def password_error_message(code)
case code case code
when :current_incorrect then "Current password is incorrect" when :current_incorrect then I18n.t("flash.accounts.password_current_incorrect")
when :too_short then "Password must be at least 8 characters" when :too_short then I18n.t("flash.accounts.password_too_short")
when :mismatch then "Passwords do not match" when :too_long then I18n.t("flash.accounts.password_too_long")
else "Unable to update password" when :too_weak then I18n.t("flash.accounts.password_too_weak")
when :same_as_current then I18n.t("flash.accounts.password_same_as_current")
when :mismatch then I18n.t("flash.accounts.password_mismatch")
else I18n.t("flash.accounts.password_update_failed")
end end
end end
end end
@@ -22,18 +22,18 @@ module Api
if user&.authenticate(params[:password]) if user&.authenticate(params[:password])
render json: token_response(user), status: :ok render json: token_response(user), status: :ok
else else
render json: { error: "Invalid credentials" }, status: :unauthorized render json: { error: I18n.t("flash.sessions.invalid_credentials") }, status: :unauthorized
end end
end end
def logout def logout
render json: { message: "Logged out" } render json: { message: I18n.t("flash.sessions.logged_out") }
end end
def refresh def refresh
payload = JsonWebToken.decode(params[:refresh_token] || bearer_token) payload = JsonWebToken.decode(params[:refresh_token] || bearer_token)
user = User.find_by(id: payload&.dig(:user_id)) user = User.find_by(id: payload&.dig(:user_id))
return render json: { error: "Invalid token" }, status: :unauthorized unless user return render json: { error: I18n.t("flash.sessions.invalid_token") }, status: :unauthorized unless user
render json: token_response(user) render json: token_response(user)
end end
@@ -45,7 +45,7 @@ module Api
def forgot_password def forgot_password
Users::RequestPasswordReset.call(email: params[:email]) Users::RequestPasswordReset.call(email: params[:email])
render json: { render json: {
message: "If the email is registered, you will receive a password reset link shortly." message: I18n.t("flash.password_resets.email_sent")
} }
end end
@@ -1,17 +1,25 @@
class ApplicationController < ActionController::API class ApplicationController < ActionController::API
include ActionController::HttpAuthentication::Token::ControllerMethods include ActionController::HttpAuthentication::Token::ControllerMethods
before_action :set_api_locale
before_action :authenticate_request! before_action :authenticate_request!
attr_reader :current_user attr_reader :current_user
private private
def set_api_locale
explicit = LocaleResolver.normalize(request.headers["X-Locale"])
I18n.locale = explicit ||
LocaleResolver.from_accept_language(request.headers["Accept-Language"]) ||
I18n.default_locale
end
def authenticate_request! def authenticate_request!
token = bearer_token token = bearer_token
payload = JsonWebToken.decode(token) payload = JsonWebToken.decode(token)
@current_user = User.find_by(id: payload[:user_id]) if payload @current_user = User.find_by(id: payload[:user_id]) if payload
render json: { error: "Unauthorized" }, status: :unauthorized unless @current_user render json: { error: I18n.t("flash.sessions.unauthorized") }, status: :unauthorized unless @current_user
end end
def bearer_token def bearer_token
@@ -29,8 +29,8 @@ module Public
) )
unless result.ok? unless result.ok?
flash.now[:alert] = t("flash.accounts.password_#{result.error}") redirect_to public_account_path, alert: t("flash.accounts.password_#{result.error}")
return render :show, status: :unprocessable_entity return
end end
redirect_to public_account_path, notice: t("flash.accounts.password_updated") redirect_to public_account_path, notice: t("flash.accounts.password_updated")
@@ -11,17 +11,8 @@ module Public
load_club_billing_context_for_pricing load_club_billing_context_for_pricing
end end
private def support
@app_store_review_chrome = true
def load_club_billing_context_for_pricing
return unless logged_in?
@club = current_user.primary_club
return unless @club
@subscription = @club.subscription
@team = @club.teams.order(:name).first
@entitlements = @team&.entitlements
end end
def privacy def privacy
@@ -38,5 +29,18 @@ module Public
def pallavolo def pallavolo
end end
private
def load_club_billing_context_for_pricing
return unless logged_in?
@club = current_user.primary_club
return unless @club
@subscription = @club.subscription
@team = @club.teams.order(:name).first
@entitlements = @team&.entitlements
end
end end
end end
@@ -13,7 +13,7 @@ module Public
def edit def edit
@user = User.find_by_password_reset_token(params[:token]) @user = User.find_by_password_reset_token(params[:token])
if @user.nil? || @user.password_reset_expired? if @user.nil? || @user.password_reset_expired?
redirect_to new_public_password_reset_path, redirect_to public_password_forgot_path,
alert: t("flash.password_resets.invalid_or_expired_link") alert: t("flash.password_resets.invalid_or_expired_link")
return return
end end
@@ -23,19 +23,25 @@ module Public
def update def update
@user = User.find_by_password_reset_token(params[:token]) @user = User.find_by_password_reset_token(params[:token])
if @user.nil? || @user.password_reset_expired? if @user.nil? || @user.password_reset_expired?
redirect_to new_public_password_reset_path, redirect_to public_password_forgot_path,
alert: t("flash.password_resets.invalid_or_expired_link") alert: t("flash.password_resets.invalid_or_expired_link")
return return
end end
if params[:password].blank? || params[:password].length < 8 if params[:password] != params[:password_confirmation]
flash.now[:alert] = t("flash.password_resets.password_min_length") flash.now[:alert] = t("flash.password_resets.password_mismatch")
@token = params[:token] @token = params[:token]
return render :edit, status: :unprocessable_entity return render :edit, status: :unprocessable_entity
end end
if params[:password] != params[:password_confirmation] if (code = PasswordComplexity.violation(params[:password]))
flash.now[:alert] = t("flash.password_resets.password_mismatch") flash.now[:alert] = t("flash.password_resets.password_#{code == :blank ? :too_short : code}")
@token = params[:token]
return render :edit, status: :unprocessable_entity
end
if PasswordComplexity.same_as_current?(@user, params[:password])
flash.now[:alert] = t("flash.password_resets.password_same_as_current")
@token = params[:token] @token = params[:token]
return render :edit, status: :unprocessable_entity return render :edit, status: :unprocessable_entity
end end
@@ -11,6 +11,7 @@ module Public
{ loc: "#{base}/live", changefreq: "hourly", priority: "0.85" }, { loc: "#{base}/live", changefreq: "hourly", priority: "0.85" },
{ loc: "#{base}/squadre", changefreq: "daily", priority: "0.85" }, { loc: "#{base}/squadre", changefreq: "daily", priority: "0.85" },
{ loc: "#{base}/privacy", changefreq: "yearly", priority: "0.3" }, { loc: "#{base}/privacy", changefreq: "yearly", priority: "0.3" },
{ loc: "#{base}/support", changefreq: "yearly", priority: "0.3" },
{ loc: "#{base}/cookie", changefreq: "yearly", priority: "0.3" }, { loc: "#{base}/cookie", changefreq: "yearly", priority: "0.3" },
{ loc: "#{base}/termini", changefreq: "yearly", priority: "0.3" } { loc: "#{base}/termini", changefreq: "yearly", priority: "0.3" }
] ]
@@ -57,6 +57,10 @@ module Public
Billing::Stripe::PlanChangeMessages.button_label(plan: plan, interval: interval, kind: btn_kind) Billing::Stripe::PlanChangeMessages.button_label(plan: plan, interval: interval, kind: btn_kind)
end end
def plan_cta_name(plan)
I18n.t("pages.plans.cta_name.#{plan.slug}", default: plan.name)
end
def billing_plan_change_info_lines(subscription: nil) def billing_plan_change_info_lines(subscription: nil)
Billing::Stripe::PlanChangeMessages.billing_info_lines(subscription: subscription) Billing::Stripe::PlanChangeMessages.billing_info_lines(subscription: subscription)
end end
+2
View File
@@ -1,4 +1,6 @@
class AdminAccount < ApplicationRecord class AdminAccount < ApplicationRecord
include PasswordComplexity
has_secure_password has_secure_password
validates :username, presence: true, uniqueness: true validates :username, presence: true, uniqueness: true
@@ -0,0 +1,62 @@
# Criteri "di mercato" (stile Cognito/Auth0 bilanciato):
# - minimo 8 caratteri (max 72 per bcrypt)
# - almeno 3 classi su 4: minuscole, maiuscole, numeri, simboli
module PasswordComplexity
extend ActiveSupport::Concern
MIN_LENGTH = 8
MAX_LENGTH = 72
REQUIRED_CLASSES = 3
CLASS_CHECKS = {
lowercase: /[a-z]/,
uppercase: /[A-Z]/,
digit: /\d/,
symbol: /[^A-Za-z0-9]/
}.freeze
class << self
def violation(password)
value = password.to_s
return :blank if value.blank?
return :too_short if value.length < MIN_LENGTH
return :too_long if value.bytesize > MAX_LENGTH
return :too_weak unless strong_enough?(value)
nil
end
def strong_enough?(password)
matched = CLASS_CHECKS.count { |_, pattern| password.match?(pattern) }
matched >= REQUIRED_CLASSES
end
# Confronta con il digest già salvato (prima di assegnare la nuova password).
def same_as_current?(record, password)
return false if password.blank? || !record.respond_to?(:authenticate)
record.authenticate(password).present?
end
def requirement_summary
I18n.t("password_policy.hint")
end
end
included do
validate :password_meets_complexity_policy, if: -> { password.present? }
end
private
def password_meets_complexity_policy
case PasswordComplexity.violation(password)
when :too_short
errors.add(:password, :too_short, count: MIN_LENGTH)
when :too_long
errors.add(:password, :too_long, count: MAX_LENGTH)
when :too_weak
errors.add(:password, :complexity)
end
end
end
+2
View File
@@ -1,4 +1,6 @@
class User < ApplicationRecord class User < ApplicationRecord
include PasswordComplexity
ROLES = %w[admin coach parent volunteer].freeze ROLES = %w[admin coach parent volunteer].freeze
has_secure_password has_secure_password
@@ -5,9 +5,13 @@ module Billing
AMOUNT_HINTS = { AMOUNT_HINTS = {
500 => %w[premium_light monthly], 500 => %w[premium_light monthly],
790 => %w[premium_light monthly],
4000 => %w[premium_light yearly], 4000 => %w[premium_light yearly],
5900 => %w[premium_light yearly],
2000 => %w[premium_full monthly], 2000 => %w[premium_full monthly],
20_000 => %w[premium_full yearly] 2490 => %w[premium_full monthly],
20_000 => %w[premium_full yearly],
19_900 => %w[premium_full yearly]
}.freeze }.freeze
class << self class << self
@@ -23,10 +23,11 @@ module Billing
def button_label(plan:, interval:, kind:) def button_label(plan:, interval:, kind:)
price = PriceCatalog.label(plan_slug: plan.slug, interval: interval) price = PriceCatalog.label(plan_slug: plan.slug, interval: interval)
name = I18n.t("pages.plans.cta_name.#{plan.slug}", default: plan.name)
if kind == :checkout if kind == :checkout
I18n.t("billing.messages.activate_button", plan: plan.name, price: price) I18n.t("billing.messages.activate_button", plan: name, price: price)
else else
I18n.t("billing.messages.switch_button", plan: plan.name, price: price) I18n.t("billing.messages.switch_button", plan: name, price: price)
end end
end end
@@ -4,9 +4,16 @@ module Billing
INTERVALS = %w[monthly yearly].freeze INTERVALS = %w[monthly yearly].freeze
DEFAULT_INTERVAL = "yearly" DEFAULT_INTERVAL = "yearly"
PRICING = { # Importi in centesimi (EUR). Il yearly è il prezzo lancio addebitato.
"premium_light" => { "yearly" => "€40/anno", "monthly" => "€5/mese" }, AMOUNTS = {
"premium_full" => { "yearly" => "€200/anno", "monthly" => "€20/mese" } "premium_light" => {
"yearly" => { charge: 5900, list: 7900, equivalent_monthly: 492 },
"monthly" => { charge: 790 }
},
"premium_full" => {
"yearly" => { charge: 19_900, list: 24_900, equivalent_monthly: 1658 },
"monthly" => { charge: 2490 }
}
}.freeze }.freeze
class << self class << self
@@ -30,7 +37,23 @@ module Billing
end end
def label(plan_slug:, interval:) def label(plan_slug:, interval:)
PRICING.dig(plan_slug.to_s, normalize_interval(interval)) || normalize_interval(interval) interval = normalize_interval(interval)
format_interval_price(charge_cents(plan_slug, interval), interval)
end
def list_label(plan_slug:, interval: "yearly")
interval = normalize_interval(interval)
cents = list_cents(plan_slug, interval)
return nil if cents.blank?
format_interval_price(cents, interval)
end
def equivalent_monthly_label(plan_slug:)
cents = AMOUNTS.dig(plan_slug.to_s, "yearly", :equivalent_monthly)
return nil if cents.blank?
format_interval_price(cents, "monthly")
end end
def available_intervals(plan_slug:) def available_intervals(plan_slug:)
@@ -52,8 +75,33 @@ module Billing
nil nil
end end
def format_eur(cents)
cents = cents.to_i
whole, frac = cents.divmod(100)
if frac.zero?
"#{whole}"
else
sep = I18n.locale.to_s == "en" ? "." : ","
"#{whole}#{sep}#{frac.to_s.rjust(2, '0')}"
end
end
private private
def charge_cents(plan_slug, interval)
AMOUNTS.dig(plan_slug.to_s, interval, :charge)
end
def list_cents(plan_slug, interval)
AMOUNTS.dig(plan_slug.to_s, interval, :list)
end
def format_interval_price(cents, interval)
return nil if cents.blank?
I18n.t("billing.prices.#{interval}", amount: format_eur(cents))
end
def price_id_for(plan_slug, interval) def price_id_for(plan_slug, interval)
case [plan_slug, interval] case [plan_slug, interval]
when %w[premium_light monthly] when %w[premium_light monthly]
+13 -5
View File
@@ -23,15 +23,23 @@ module Users
return Result.new(ok?: false, error: :current_incorrect) return Result.new(ok?: false, error: :current_incorrect)
end end
if @password.blank? || @password.length < 8
return Result.new(ok?: false, error: :too_short)
end
if @password != @password_confirmation if @password != @password_confirmation
return Result.new(ok?: false, error: :mismatch) return Result.new(ok?: false, error: :mismatch)
end end
@user.update!(password: @password) if (code = PasswordComplexity.violation(@password))
return Result.new(ok?: false, error: code == :blank ? :too_short : code)
end
if PasswordComplexity.same_as_current?(@user, @password)
return Result.new(ok?: false, error: :same_as_current)
end
unless @user.update(password: @password)
complexity_error = @user.errors.details[:password]&.any? { |d| d[:error] == :complexity }
return Result.new(ok?: false, error: complexity_error ? :too_weak : :too_short)
end
@user.clear_password_reset! @user.clear_password_reset!
Result.new(ok?: true) Result.new(ok?: true)
end end
+3 -3
View File
@@ -8,17 +8,17 @@
<%= render "shared/meta_tags" %> <%= render "shared/meta_tags" %>
<%= yield :head %> <%= yield :head %>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" integrity="sha512-SnH5WK+bZxgPHs44uWIX+LLJAJ9/2PkPKZ5QiAj6Ta86w+fsb2TkcmfRyVX3pBnMFcV7oQPJkl9QevSCWr3W6A==" crossorigin="anonymous" referrerpolicy="no-referrer"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" integrity="sha512-SnH5WK+bZxgPHs44uWIX+LLJAJ9/2PkPKZ5QiAj6Ta86w+fsb2TkcmfRyVX3pBnMFcV7oQPJkl9QevSCWr3W6A==" crossorigin="anonymous" referrerpolicy="no-referrer">
<link rel="stylesheet" href="/marketing.css?v=44"> <link rel="stylesheet" href="/marketing.css?v=63">
</head> </head>
<body data-confirm-i18n='<%= raw confirm_dialog_i18n_json %>'<% if MatchLiveTv.google_analytics_configured? %> data-ga-id="<%= MatchLiveTv.google_analytics_measurement_id %>"<% end %>> <body data-confirm-i18n='<%= raw confirm_dialog_i18n_json %>'<% if MatchLiveTv.google_analytics_configured? %> data-ga-id="<%= MatchLiveTv.google_analytics_measurement_id %>"<% end %>>
<%= render "shared/cookie_banner" %> <%= render "shared/cookie_banner" %>
<%= render "shared/marketing_nav" %> <%= render(@app_store_review_chrome ? "shared/marketing_nav_app_store" : "shared/marketing_nav") %>
<main> <main>
<% if flash[:notice] %><div class="wrap"><div class="flash notice"><%= flash[:notice] %></div></div><% end %> <% if flash[:notice] %><div class="wrap"><div class="flash notice"><%= flash[:notice] %></div></div><% end %>
<% if flash[:alert] %><div class="wrap"><div class="flash alert"><%= flash[:alert] %></div></div><% end %> <% if flash[:alert] %><div class="wrap"><div class="flash alert"><%= flash[:alert] %></div></div><% end %>
<%= yield %> <%= yield %>
</main> </main>
<%= render "shared/marketing_footer" %> <%= render(@app_store_review_chrome ? "shared/marketing_footer_app_store" : "shared/marketing_footer") %>
<script src="/branding-form.js?v=1" defer></script> <script src="/branding-form.js?v=1" defer></script>
<script src="/roster-form.js?v=1" defer></script> <script src="/roster-form.js?v=1" defer></script>
<script src="/password-toggle.js?v=2" defer></script> <script src="/password-toggle.js?v=2" defer></script>
@@ -6,7 +6,7 @@
<title><%= content_for?(:title) ? yield(:title) : "Match Live TV" %></title> <title><%= content_for?(:title) ? yield(:title) : "Match Live TV" %></title>
<%= render "shared/meta_tags" %> <%= render "shared/meta_tags" %>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" integrity="sha512-SnH5WK+bZxgPHs44uWIX+LLJAJ9/2PkPKZ5QiAj6Ta86w+fsb2TkcmfRyVX3pBnMFcV7oQPJkl9QevSCWr3W6A==" crossorigin="anonymous" referrerpolicy="no-referrer"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" integrity="sha512-SnH5WK+bZxgPHs44uWIX+LLJAJ9/2PkPKZ5QiAj6Ta86w+fsb2TkcmfRyVX3pBnMFcV7oQPJkl9QevSCWr3W6A==" crossorigin="anonymous" referrerpolicy="no-referrer">
<link rel="stylesheet" href="/marketing.css?v=44"> <link rel="stylesheet" href="/marketing.css?v=63">
<link rel="stylesheet" href="/live.css?v=26"> <link rel="stylesheet" href="/live.css?v=26">
<%= yield :head %> <%= yield :head %>
</head> </head>
@@ -30,6 +30,7 @@
<div class="card"> <div class="card">
<h2 style="font-size:1.1rem;margin-top:0"><%= t("auth.account.password_heading") %></h2> <h2 style="font-size:1.1rem;margin-top:0"><%= t("auth.account.password_heading") %></h2>
<p class="muted" style="font-size:0.9rem"><%= t("password_policy.hint") %></p>
<%= form_with url: public_account_password_path, method: :patch, local: true do %> <%= form_with url: public_account_password_path, method: :patch, local: true do %>
<%= render "shared/input_toggle", <%= render "shared/input_toggle",
name: :current_password, name: :current_password,
@@ -2,7 +2,8 @@
<% content_for :meta_description, t("auth.invitation.meta_description") %> <% content_for :meta_description, t("auth.invitation.meta_description") %>
<% content_for :robots, "noindex, nofollow" %> <% content_for :robots, "noindex, nofollow" %>
<div class="card" style="max-width:480px"> <section class="auth-page">
<div class="card">
<h1><%= raw t("auth.invitation.title_html", team_name: @invitation.team.name) %></h1> <h1><%= raw t("auth.invitation.title_html", team_name: @invitation.team.name) %></h1>
<p><%= raw t("auth.invitation.role_notice_html", email: @invitation.email) %></p> <p><%= raw t("auth.invitation.role_notice_html", email: @invitation.email) %></p>
<p class="muted" style="font-size:0.9rem;margin-bottom:16px"> <p class="muted" style="font-size:0.9rem;margin-bottom:16px">
@@ -23,4 +24,5 @@
<%= t("auth.invitation.mobile_app_label") %> <%= t("auth.invitation.mobile_app_label") %>
<a href="matchlivetv://join/<%= @token %>"><%= t("auth.invitation.open_in_app") %></a> <a href="matchlivetv://join/<%= @token %>"><%= t("auth.invitation.open_in_app") %></a>
</p> </p>
</div> </div>
</section>
@@ -46,6 +46,7 @@
<h3><%= t("legal.cookies.s4_1_title") %></h3> <h3><%= t("legal.cookies.s4_1_title") %></h3>
<p><%= t("legal.cookies.s4_1_intro") %></p> <p><%= t("legal.cookies.s4_1_intro") %></p>
<div class="table-scroll">
<table class="legal-table"> <table class="legal-table">
<thead> <thead>
<tr><th><%= t("legal.cookies.table_col_name") %></th><th><%= t("legal.cookies.table_col_purpose") %></th><th><%= t("legal.cookies.table_col_duration") %></th><th><%= t("legal.cookies.table_col_provider") %></th></tr> <tr><th><%= t("legal.cookies.table_col_name") %></th><th><%= t("legal.cookies.table_col_purpose") %></th><th><%= t("legal.cookies.table_col_duration") %></th><th><%= t("legal.cookies.table_col_provider") %></th></tr>
@@ -65,6 +66,7 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div>
<h3><%= t("legal.cookies.s4_2_title") %></h3> <h3><%= t("legal.cookies.s4_2_title") %></h3>
<p> <p>
@@ -75,6 +77,7 @@
<% else %> <% else %>
<p class="muted"><%= t("legal.cookies.s4_2_inactive") %></p> <p class="muted"><%= t("legal.cookies.s4_2_inactive") %></p>
<% end %> <% end %>
<div class="table-scroll">
<table class="legal-table"> <table class="legal-table">
<thead> <thead>
<tr><th><%= t("legal.cookies.table2_col_name") %></th><th><%= t("legal.cookies.table_col_purpose") %></th><th><%= t("legal.cookies.table_col_duration") %></th><th><%= t("legal.cookies.table_col_provider") %></th></tr> <tr><th><%= t("legal.cookies.table2_col_name") %></th><th><%= t("legal.cookies.table_col_purpose") %></th><th><%= t("legal.cookies.table_col_duration") %></th><th><%= t("legal.cookies.table_col_provider") %></th></tr>
@@ -100,6 +103,7 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div>
<p> <p>
<%= raw t( <%= raw t(
"legal.cookies.s4_2_p2_html", "legal.cookies.s4_2_p2_html",
+264 -44
View File
@@ -2,63 +2,283 @@
<% content_for :meta_description, t("pages.features.meta_description") %> <% content_for :meta_description, t("pages.features.meta_description") %>
<% content_for :canonical_url, seo_absolute_url(public_features_path) %> <% content_for :canonical_url, seo_absolute_url(public_features_path) %>
<div class="wrap"> <article class="features-page">
<h1 style="margin-top:24px"><%= t("pages.features.title") %></h1> <header class="features-hero">
<p style="color:#aaa;max-width:640px"><%= t("pages.features.lead") %></p> <div class="wrap features-hero__grid">
<div class="features-hero__copy">
<p class="features-hero__eyebrow">Match Live TV</p>
<h1><%= t("pages.features.hero_title") %></h1>
<p class="features-hero__lead"><%= t("pages.features.hero_lead") %></p>
<section class="section"> <ol class="features-flow" aria-label="<%= t("pages.features.flow_aria") %>">
<h2><%= t("pages.features.staff_section_title") %></h2> <li class="features-flow__item">
<div class="features-grid"> <span class="features-flow__icon" aria-hidden="true"><i class="fa-solid fa-mobile-screen"></i></span>
<div class="feature-card"> <span class="features-flow__label"><%= t("pages.features.flow_phone") %></span>
<h3><%= t("pages.features.staff_invite_title") %></h3> </li>
<p><%= t("pages.features.staff_invite_body") %></p> <li class="features-flow__arrow" aria-hidden="true"><i class="fa-solid fa-arrow-right"></i></li>
<li class="features-flow__item">
<span class="features-flow__icon features-flow__icon--blue" aria-hidden="true"><i class="fa-solid fa-user"></i></span>
<span class="features-flow__label"><%= t("pages.features.flow_account") %></span>
</li>
<li class="features-flow__arrow" aria-hidden="true"><i class="fa-solid fa-arrow-right"></i></li>
<li class="features-flow__item features-flow__item--live">
<span class="features-flow__icon features-flow__icon--live" aria-hidden="true">
<span class="features-flow__dot"></span>
</span>
<span class="features-flow__label"><%= t("pages.features.flow_live") %></span>
</li>
</ol>
</div>
<div class="features-hero__visual">
<%= image_tag "/hero-devices.png",
alt: t("pages.features.hero_alt"),
class: "features-hero__img",
loading: "eager",
fetchpriority: "high" %>
</div>
</div>
</header>
<section class="section wrap features-how" aria-labelledby="features-how-title">
<h2 id="features-how-title"><%= t("pages.features.how_title") %></h2>
<ol class="features-how__steps">
<li class="features-how__step">
<span class="features-how__num" aria-hidden="true">1</span>
<h3><%= t("pages.features.how_1_title") %></h3>
<p><%= t("pages.features.how_1_body") %></p>
</li>
<li class="features-how__step">
<span class="features-how__num" aria-hidden="true">2</span>
<h3><%= t("pages.features.how_2_title") %></h3>
<p><%= t("pages.features.how_2_body") %></p>
</li>
<li class="features-how__step">
<span class="features-how__num" aria-hidden="true">3</span>
<h3><%= t("pages.features.how_3_title") %></h3>
<p><%= t("pages.features.how_3_body") %></p>
</li>
<li class="features-how__step">
<span class="features-how__num" aria-hidden="true">4</span>
<h3><%= t("pages.features.how_4_title") %></h3>
<p><%= t("pages.features.how_4_body") %></p>
</li>
<li class="features-how__step">
<span class="features-how__num" aria-hidden="true">5</span>
<h3><%= t("pages.features.how_5_title") %></h3>
<p><%= t("pages.features.how_5_body") %></p>
</li>
</ol>
</section>
<section class="section wrap features-accounts" aria-labelledby="features-accounts-title">
<div class="features-panel">
<h2 id="features-accounts-title"><%= t("pages.features.accounts_title") %></h2>
<p class="features-panel__lead"><%= t("pages.features.accounts_lead") %></p>
<div class="features-accounts__grid">
<div class="features-accounts__card">
<span class="feature-card__icon" aria-hidden="true"><i class="fa-solid fa-user-plus"></i></span>
<h3><%= t("pages.features.accounts_invite_title") %></h3>
<p><%= t("pages.features.accounts_invite_body") %></p>
</div>
<div class="features-accounts__card">
<span class="feature-card__icon feature-card__icon--blue" aria-hidden="true"><i class="fa-solid fa-mobile-screen"></i></span>
<h3><%= t("pages.features.accounts_transmit_title") %></h3>
<p><%= t("pages.features.accounts_transmit_body") %></p>
</div>
<div class="features-accounts__card">
<span class="feature-card__icon feature-card__icon--gold" aria-hidden="true"><i class="fa-solid fa-user-shield"></i></span>
<h3><%= t("pages.features.accounts_revoke_title") %></h3>
<p><%= t("pages.features.accounts_revoke_body") %></p>
</div>
</div>
<p class="features-highlight"><%= t("pages.features.accounts_highlight") %></p>
</div>
</section>
<section class="section wrap features-score" aria-labelledby="features-score-title">
<div class="features-split">
<div class="features-split__copy">
<h2 id="features-score-title"><%= t("pages.features.score_title") %></h2>
<p class="features-panel__lead features-panel__lead--left"><%= t("pages.features.score_lead") %></p>
<ul class="features-checklist">
<li><%= t("pages.features.score_item_app") %></li>
<li><%= t("pages.features.score_item_signup") %></li>
<li><%= t("pages.features.score_item_password") %></li>
<li><%= t("pages.features.score_item_link") %></li>
</ul>
</div>
<aside class="features-score__card" aria-label="<%= t("pages.features.score_card_aria") %>">
<div class="features-score__visual" aria-hidden="true">
<div class="features-score__phone">
<div class="features-score__phone-top">
<span class="features-score__live"><span class="features-score__live-dot"></span> REGIA</span>
<span class="features-score__set">3° set</span>
</div>
<div class="features-score__board">
<div class="features-score__team">
<span class="features-score__team-name">Casa</span>
<span class="features-score__points">21</span>
</div>
<span class="features-score__sep"></span>
<div class="features-score__team features-score__team--away">
<span class="features-score__team-name">Ospiti</span>
<span class="features-score__points">18</span>
</div>
</div>
<div class="features-score__actions">
<span class="features-score__btn">+1</span>
<span class="features-score__btn features-score__btn--muted">+1</span>
</div>
</div>
<div class="features-score__linkchip">
<i class="fa-solid fa-link" aria-hidden="true"></i>
<span>matchlivetv.it/regia/…</span>
</div>
</div>
<div class="features-score__body">
<span class="feature-card__badge feature-card__badge--link"><%= t("pages.features.access_link") %></span>
<p class="features-score__card-title"><%= t("pages.features.score_card_title") %></p>
<p><%= t("pages.features.score_card_body") %></p>
</div>
</aside>
</div>
</section>
<section class="section wrap features-distribute" aria-labelledby="features-distribute-title">
<h2 id="features-distribute-title"><%= t("pages.features.distribute_title") %></h2>
<p class="features-panel__lead"><%= t("pages.features.distribute_lead") %></p>
<ol class="features-pipeline" aria-label="<%= t("pages.features.distribute_aria") %>">
<li class="features-pipeline__node">
<span class="features-pipeline__icon" aria-hidden="true"><i class="fa-solid fa-mobile-screen"></i></span>
<span class="features-pipeline__label"><%= t("pages.features.distribute_phone") %></span>
</li>
<li class="features-pipeline__join" aria-hidden="true"></li>
<li class="features-pipeline__node features-pipeline__node--hub">
<span class="features-pipeline__icon" aria-hidden="true"><i class="fa-solid fa-tower-broadcast"></i></span>
<span class="features-pipeline__label"><%= t("pages.features.distribute_mltv") %></span>
</li>
<li class="features-pipeline__join" aria-hidden="true"></li>
<li class="features-pipeline__targets">
<div class="features-pipeline__node">
<span class="features-pipeline__icon features-pipeline__icon--blue" aria-hidden="true"><i class="fa-solid fa-users"></i></span>
<span class="features-pipeline__label"><%= t("pages.features.distribute_viewers") %></span>
</div>
<div class="features-pipeline__node">
<span class="features-pipeline__icon" aria-hidden="true"><i class="fa-brands fa-youtube"></i></span>
<span class="features-pipeline__label"><%= t("pages.features.distribute_youtube") %></span>
</div>
</li>
</ol>
</section>
<section class="section wrap features-youtube" aria-labelledby="features-youtube-title">
<div class="features-panel features-panel--accent features-youtube__panel">
<div class="features-youtube__copy">
<span class="feature-card__badge feature-card__badge--invite"><%= t("pages.features.youtube_badge") %></span>
<h2 id="features-youtube-title"><%= t("pages.features.youtube_title") %></h2>
<p class="features-panel__lead features-panel__lead--left"><%= t("pages.features.youtube_body") %></p>
</div>
<div class="features-youtube__visual" aria-hidden="true">
<div class="features-yt-mock">
<div class="features-yt-mock__player">
<span class="features-yt-mock__live"><span class="features-yt-mock__dot"></span> LIVE</span>
<span class="features-yt-mock__play"><i class="fa-brands fa-youtube"></i></span>
</div>
<div class="features-yt-mock__meta">
<span class="features-yt-mock__avatar"><i class="fa-solid fa-shield-halved"></i></span>
<div class="features-yt-mock__text">
<strong><%= t("pages.features.youtube_mock_channel") %></strong>
<span><%= t("pages.features.youtube_mock_subs") %></span>
</div>
</div> </div>
<div class="feature-card">
<h3><%= t("pages.features.staff_access_title") %></h3>
<p><%= t("pages.features.staff_access_body") %></p>
</div> </div>
<div class="feature-card">
<h3><%= t("pages.features.staff_plans_title") %></h3>
<p><%= t("pages.features.staff_plans_body") %></p>
</div> </div>
</div> </div>
</section> </section>
<section class="section"> <section class="section wrap features-replay" aria-labelledby="features-replay-title">
<h2><%= t("pages.features.live_section_title") %></h2> <div class="features-split features-split--reverse">
<div class="features-grid"> <div class="features-split__copy">
<h2 id="features-replay-title"><%= t("pages.features.replay_title") %></h2>
<p class="features-panel__lead features-panel__lead--left"><%= t("pages.features.replay_lead") %></p>
<ul class="features-checklist">
<li><%= t("pages.features.replay_item_server") %></li>
<li><%= t("pages.features.replay_item_plan") %></li>
<li><%= t("pages.features.replay_item_download") %></li>
</ul>
</div>
<aside class="features-replay__card" aria-label="<%= t("pages.features.replay_card_title") %>">
<div class="features-replay__visual" aria-hidden="true">
<div class="features-archive-mock">
<div class="features-archive-mock__head">
<i class="fa-solid fa-cloud" aria-hidden="true"></i>
<span><%= t("pages.features.replay_card_title") %></span>
</div>
<ul class="features-archive-mock__list">
<li>
<span class="features-archive-mock__thumb features-archive-mock__thumb--a"></span>
<span class="features-archive-mock__info">
<strong><%= t("pages.features.replay_mock_1_title") %></strong>
<em><%= t("pages.features.replay_mock_1_meta") %></em>
</span>
<i class="fa-solid fa-download" aria-hidden="true"></i>
</li>
<li>
<span class="features-archive-mock__thumb features-archive-mock__thumb--b"></span>
<span class="features-archive-mock__info">
<strong><%= t("pages.features.replay_mock_2_title") %></strong>
<em><%= t("pages.features.replay_mock_2_meta") %></em>
</span>
<i class="fa-solid fa-download" aria-hidden="true"></i>
</li>
<li>
<span class="features-archive-mock__thumb features-archive-mock__thumb--c"></span>
<span class="features-archive-mock__info">
<strong><%= t("pages.features.replay_mock_3_title") %></strong>
<em><%= t("pages.features.replay_mock_3_meta") %></em>
</span>
<i class="fa-solid fa-download" aria-hidden="true"></i>
</li>
</ul>
</div>
</div>
<div class="features-replay__body">
<p class="features-score__card-title"><%= t("pages.features.replay_card_title") %></p>
<p><%= t("pages.features.replay_card_body") %></p>
</div>
</aside>
</div>
</section>
<section class="section wrap features-more" aria-labelledby="features-more-title">
<h2 id="features-more-title"><%= t("pages.features.more_title") %></h2>
<div class="features-grid features-grid--compact">
<div class="feature-card"> <div class="feature-card">
<h3><%= t("pages.features.live_phone_title") %></h3> <span class="feature-card__icon" aria-hidden="true"><i class="fa-solid fa-tower-broadcast"></i></span>
<p><%= t("pages.features.live_phone_body") %></p> <h3><%= t("pages.features.more_stable_title") %></h3>
<p><%= t("pages.features.more_stable_body") %></p>
</div> </div>
<div class="feature-card"> <div class="feature-card">
<h3><%= t("pages.features.live_score_title") %></h3> <span class="feature-card__icon feature-card__icon--blue" aria-hidden="true"><i class="fa-solid fa-share-nodes"></i></span>
<p><%= t("pages.features.live_score_body") %></p> <h3><%= t("pages.features.more_link_title") %></h3>
<p><%= t("pages.features.more_link_body") %></p>
</div> </div>
<div class="feature-card"> <div class="feature-card">
<h3><%= t("pages.features.live_link_title") %></h3> <span class="feature-card__icon" aria-hidden="true"><i class="fa-brands fa-youtube"></i></span>
<p><%= t("pages.features.live_link_body") %></p> <h3><%= t("pages.features.more_yt_mltv_title") %></h3>
</div> <p><%= t("pages.features.more_yt_mltv_body") %></p>
<div class="feature-card">
<h3><%= t("pages.features.live_archive_title") %></h3>
<p><%= t("pages.features.live_archive_body") %></p>
</div>
<div class="feature-card">
<h3><%= t("pages.features.live_youtube_mltv_title") %></h3>
<p><%= t("pages.features.live_youtube_mltv_body") %></p>
</div>
<div class="feature-card">
<h3><%= t("pages.features.live_youtube_club_title") %></h3>
<p><%= t("pages.features.live_youtube_club_body") %></p>
</div>
<div class="feature-card">
<h3><%= t("pages.features.live_website_title") %></h3>
<p><%= t("pages.features.live_website_body") %></p>
</div> </div>
</div> </div>
</section> </section>
<p style="text-align:center;margin:40px 0"> <section class="section wrap features-cta" aria-labelledby="features-cta-title">
<%= link_to t("pages.features.cta_signup"), public_signup_path, class: "btn btn-primary" %> <div class="features-cta__box">
</p> <h2 id="features-cta-title"><%= t("pages.features.cta_title") %></h2>
</div> <p><%= t("pages.features.cta_body") %></p>
<div class="features-cta__actions">
<%= link_to t("pages.features.cta_primary"), public_signup_path, class: "btn btn-primary" %>
<%= link_to t("pages.features.cta_secondary"), public_prezzi_path, class: "btn btn-outline" %>
</div>
</div>
</section>
</article>
+6 -1
View File
@@ -10,7 +10,8 @@
"url" => seo_absolute_url(root_path), "url" => seo_absolute_url(root_path),
"applicationCategory" => "SportsApplication", "applicationCategory" => "SportsApplication",
"operatingSystem" => "Web, Android, iOS", "operatingSystem" => "Web, Android, iOS",
"description" => t("home.schema_description") "description" => t("home.schema_description"),
"sameAs" => [MatchLiveTv.app_store_url, MatchLiveTv.play_store_url]
}.to_json) %> }.to_json) %>
</script> </script>
<% end %> <% end %>
@@ -26,6 +27,10 @@
<%= link_to t("home.cta_signup"), public_signup_path, class: "btn btn-primary" %> <%= link_to t("home.cta_signup"), public_signup_path, class: "btn btn-primary" %>
<%= link_to t("home.cta_live"), public_live_index_path, class: "btn btn-secondary" %> <%= link_to t("home.cta_live"), public_live_index_path, class: "btn btn-secondary" %>
</div> </div>
<div class="hero-stores">
<p class="hero-stores-label"><%= t("home.stores_label") %></p>
<%= render "shared/store_badges", variant: "hero" %>
</div>
<ul class="hero-features" aria-label="<%= t('home.features_aria') %>"> <ul class="hero-features" aria-label="<%= t('home.features_aria') %>">
<li class="hero-feature-item"> <li class="hero-feature-item">
<span class="hero-feature-icon" aria-hidden="true"><i class="fa-solid fa-tower-broadcast"></i></span> <span class="hero-feature-icon" aria-hidden="true"><i class="fa-solid fa-tower-broadcast"></i></span>
@@ -22,20 +22,63 @@
<%= render "shared/plan_cards" %> <%= render "shared/plan_cards" %>
<div class="table-scroll compare-table-wrap">
<table class="compare-table"> <table class="compare-table">
<colgroup>
<col class="compare-table__feature">
<col class="compare-table__plan">
<col class="compare-table__plan">
<col class="compare-table__plan compare-table__plan--featured">
</colgroup>
<thead> <thead>
<tr><th></th><th>Free</th><th>Premium Light</th><th>Premium Full</th></tr> <tr>
<th></th>
<th>Free</th>
<th>Premium Light</th>
<th>Premium Full</th>
</tr>
</thead> </thead>
<tbody> <tbody>
<tr><td><%= t("pages.pricing.table_staff") %></td><td>1</td><td>5</td><td><%= t("pages.pricing.table_unlimited") %></td></tr> <tr>
<td><%= t("pages.pricing.table_staff") %></td>
<td>1</td>
<td>5</td>
<td><%= t("pages.pricing.table_unlimited") %></td>
</tr>
<tr><td><%= t("pages.pricing.table_matches") %></td><td>1</td><td>3</td><td>10</td></tr> <tr><td><%= t("pages.pricing.table_matches") %></td><td>1</td><td>3</td><td>10</td></tr>
<tr><td><%= t("pages.pricing.table_live_mltv") %></td><td><%= t("pages.pricing.table_yes") %></td><td><%= t("pages.pricing.table_yes") %></td><td><%= t("pages.pricing.table_yes") %></td></tr> <tr><td><%= t("pages.pricing.table_live_mltv") %></td><td><%= t("pages.pricing.table_yes") %></td><td><%= t("pages.pricing.table_yes") %></td><td><%= t("pages.pricing.table_yes") %></td></tr>
<tr><td><%= t("pages.pricing.table_youtube") %></td><td><%= t("pages.pricing.table_no") %></td><td>Match Live TV</td><td><%= t("pages.pricing.table_youtube_club") %></td></tr> <tr><td><%= t("pages.pricing.table_youtube") %></td><td><%= t("pages.pricing.table_no") %></td><td>Match Live TV</td><td><%= t("pages.pricing.table_youtube_club") %></td></tr>
<tr><td><%= t("pages.pricing.table_replay") %></td><td><%= t("pages.pricing.table_no") %></td><td><%= t("pages.plans.replay_days", count: 30) %></td><td><%= t("pages.plans.replay_days", count: 90) %></td></tr> <tr><td><%= t("pages.pricing.table_replay") %></td><td><%= t("pages.pricing.table_no") %></td><td><%= t("pages.plans.replay_days", count: 30) %></td><td><%= t("pages.plans.replay_days", count: 90) %></td></tr>
<tr><td><%= t("pages.pricing.table_download") %></td><td><%= t("pages.pricing.table_no") %></td><td><%= t("pages.pricing.table_yes") %></td><td><%= t("pages.pricing.table_yes") %></td></tr> <tr><td><%= t("pages.pricing.table_download") %></td><td><%= t("pages.pricing.table_no") %></td><td><%= t("pages.pricing.table_yes") %></td><td><%= t("pages.pricing.table_yes") %></td></tr>
<tr><td><%= t("pages.pricing.table_price") %></td><td><%= t("pages.pricing.table_price_free") %></td><td><%= raw t("pages.pricing.table_price_light_html") %></td><td><%= raw t("pages.pricing.table_price_full_html") %></td></tr> <tr>
<td><%= t("pages.pricing.table_price") %></td>
<td><%= t("pages.pricing.table_price_free") %></td>
<td>
<div class="compare-price">
<strong><%= Billing::Stripe::PriceCatalog.label(plan_slug: "premium_light", interval: "yearly") %></strong>
<span class="compare-price__launch"><%= t("pages.plans.launch_price") %></span>
<small class="compare-price__note"><%= t(
"pages.pricing.table_price_note",
list: Billing::Stripe::PriceCatalog.list_label(plan_slug: "premium_light", interval: "yearly"),
monthly: Billing::Stripe::PriceCatalog.label(plan_slug: "premium_light", interval: "monthly")
) %></small>
</div>
</td>
<td>
<div class="compare-price">
<strong><%= Billing::Stripe::PriceCatalog.label(plan_slug: "premium_full", interval: "yearly") %></strong>
<span class="compare-price__launch"><%= t("pages.plans.launch_price") %></span>
<small class="compare-price__note"><%= t(
"pages.pricing.table_price_note",
list: Billing::Stripe::PriceCatalog.list_label(plan_slug: "premium_full", interval: "yearly"),
monthly: Billing::Stripe::PriceCatalog.label(plan_slug: "premium_full", interval: "monthly")
) %></small>
</div>
</td>
</tr>
</tbody> </tbody>
</table> </table>
</div>
<div class="card" style="margin-top:32px;text-align:center"> <div class="card" style="margin-top:32px;text-align:center">
<h3 style="margin-top:0"><%= t("pages.pricing.different_title") %></h3> <h3 style="margin-top:0"><%= t("pages.pricing.different_title") %></h3>
@@ -77,6 +77,7 @@
<section> <section>
<h2><%= t("legal.privacy.s5_title") %></h2> <h2><%= t("legal.privacy.s5_title") %></h2>
<div class="table-scroll">
<table class="legal-table"> <table class="legal-table">
<thead> <thead>
<tr><th><%= t("legal.privacy.s5_col_purpose") %></th><th><%= t("legal.privacy.s5_col_basis") %></th></tr> <tr><th><%= t("legal.privacy.s5_col_purpose") %></th><th><%= t("legal.privacy.s5_col_basis") %></th></tr>
@@ -108,6 +109,7 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div>
</section> </section>
<section> <section>
@@ -0,0 +1,38 @@
<% content_for :title, t("legal.support.title") %>
<% content_for :meta_description, t("legal.support.meta_description") %>
<% content_for :canonical_url, seo_absolute_url(public_support_path) %>
<div class="wrap legal-doc">
<h1><%= t("legal.support.h1") %></h1>
<p><%= t("legal.support.intro") %></p>
<p>
<%= t("legal.support.email_label") %>
<a href="mailto:<%= MatchLiveTv.support_email %>"><%= MatchLiveTv.support_email %></a>
</p>
<section>
<h2><%= t("legal.support.access_title") %></h2>
<p><%= t("legal.support.access_body") %></p>
</section>
<section>
<h2><%= t("legal.support.live_title") %></h2>
<p><%= t("legal.support.live_body") %></p>
</section>
<section>
<h2><%= t("legal.support.team_title") %></h2>
<p><%= t("legal.support.team_body") %></p>
</section>
<section>
<h2><%= t("legal.support.privacy_title") %></h2>
<p>
<%= raw t(
"legal.support.privacy_body_html",
privacy_link: link_to(t("legal.support.privacy_link_text"), public_privacy_path)
) %>
</p>
</section>
</div>
@@ -4,6 +4,7 @@
<section class="auth-page"> <section class="auth-page">
<h1><%= t("auth.password_reset.title") %></h1> <h1><%= t("auth.password_reset.title") %></h1>
<div class="card"> <div class="card">
<p class="muted" style="font-size:0.9rem"><%= t("password_policy.hint") %></p>
<%= form_with url: public_password_reset_path, method: :patch, local: true do %> <%= form_with url: public_password_reset_path, method: :patch, local: true do %>
<%= hidden_field_tag :token, @token %> <%= hidden_field_tag :token, @token %>
<%= render "shared/input_toggle", <%= render "shared/input_toggle",
@@ -25,6 +25,7 @@
required: true, required: true,
minlength: 8, minlength: 8,
autocomplete: "new-password" %> autocomplete: "new-password" %>
<p class="muted" style="font-size:0.9rem;margin-top:-0.5rem"><%= t("password_policy.hint") %></p>
<%= render "shared/input_toggle", <%= render "shared/input_toggle",
name: "user[password_confirmation]", name: "user[password_confirmation]",
id: "user_password_confirmation", id: "user_password_confirmation",
@@ -20,6 +20,7 @@
<h3 style="margin-top:28px;font-size:1.1rem"><%= t("billing.documents.table_heading") %></h3> <h3 style="margin-top:28px;font-size:1.1rem"><%= t("billing.documents.table_heading") %></h3>
<% if payments.any? %> <% if payments.any? %>
<div class="table-scroll">
<table class="data billing-table"> <table class="data billing-table">
<thead> <thead>
<tr> <tr>
@@ -60,6 +61,7 @@
<% end %> <% end %>
</tbody> </tbody>
</table> </table>
</div>
<% else %> <% else %>
<p style="color:#888"><%= t("billing.documents.no_payments") %></p> <p style="color:#888"><%= t("billing.documents.no_payments") %></p>
<% end %> <% end %>
@@ -58,10 +58,18 @@
} }
toggle.addEventListener("click", function (e) { toggle.addEventListener("click", function (e) {
e.preventDefault();
e.stopPropagation(); e.stopPropagation();
setOpen(menu.hidden); setOpen(menu.hidden);
}); });
// Keep parent mobile nav open while interacting with the switcher UI.
root.addEventListener("click", function (e) {
if (e.target.closest(".lang-switcher__toggle")) {
e.stopPropagation();
}
});
document.addEventListener("click", function (e) { document.addEventListener("click", function (e) {
if (!root.contains(e.target)) setOpen(false); if (!root.contains(e.target)) setOpen(false);
}); });
@@ -2,8 +2,10 @@
<div class="wrap"> <div class="wrap">
<div> <div>
<strong style="color:#fff">Match Live TV</strong><%= t("footer.tagline") %> <strong style="color:#fff">Match Live TV</strong><%= t("footer.tagline") %>
<%= render "shared/store_badges", variant: "footer" %>
</div> </div>
<div> <div>
<%= link_to t("common.support"), public_support_path %> ·
<%= link_to t("common.pricing"), public_prezzi_path %> · <%= link_to t("common.pricing"), public_prezzi_path %> ·
<%= link_to t("footer.live"), public_live_index_path %> · <%= link_to t("footer.live"), public_live_index_path %> ·
<%= link_to t("common.faq"), public_faq_path %> · <%= link_to t("common.faq"), public_faq_path %> ·
@@ -0,0 +1,19 @@
<%# Footer minimale per App Store Review: solo link legali/supporto, nessun CTA commerciale. %>
<footer class="site-footer">
<div class="wrap">
<div>
<strong style="color:#fff">Match Live TV</strong><%= t("footer.tagline") %>
</div>
<div>
<%= link_to t("common.support"), public_support_path %> ·
<%= link_to t("common.privacy"), public_privacy_path %> ·
<%= link_to t("common.cookies"), public_cookies_path %> ·
<%= link_to t("common.terms"), public_termini_path %>
· <button type="button" class="footer-link-btn" data-cookie-manage><%= t("footer.manage_cookies") %></button>
</div>
<div class="site-footer__legal">
<p><%= t("footer.copyright") %></p>
<p><%= t("footer.responsibility") %></p>
</div>
</div>
</footer>
@@ -19,6 +19,15 @@
<nav id="site-nav" class="nav" aria-label="<%= t('nav.main_menu') %>" aria-hidden="true"> <nav id="site-nav" class="nav" aria-label="<%= t('nav.main_menu') %>" aria-hidden="true">
<div class="nav-panel"> <div class="nav-panel">
<div class="nav-mobile-head">
<%= link_to root_path, class: "nav-mobile-brand", aria: { label: "Match Live TV" }, title: "Match Live TV" do %>
<img class="nav-mobile-brand-logo" src="/logo.png?v=3" alt="" width="40" height="40" decoding="async">
<span class="brand">Match <span>Live TV</span></span>
<% end %>
<div class="nav-lang">
<%= render "shared/language_switcher" %>
</div>
</div>
<%= link_to t("nav.home"), root_path, class: (request.path == "/" ? "nav-active" : nil) %> <%= link_to t("nav.home"), root_path, class: (request.path == "/" ? "nav-active" : nil) %>
<%= link_to t("nav.features"), public_features_path, class: (request.path == "/funzionalita" ? "nav-active" : nil) %> <%= link_to t("nav.features"), public_features_path, class: (request.path == "/funzionalita" ? "nav-active" : nil) %>
<%= link_to t("nav.pricing"), public_prezzi_path, class: (request.path == "/prezzi" ? "nav-active" : nil) %> <%= link_to t("nav.pricing"), public_prezzi_path, class: (request.path == "/prezzi" ? "nav-active" : nil) %>
@@ -40,9 +49,6 @@
<%= link_to t("nav.login"), public_login_path, class: "nav-link-item" %> <%= link_to t("nav.login"), public_login_path, class: "nav-link-item" %>
<%= link_to t("nav.signup"), public_signup_path, class: "btn btn-primary nav-btn" %> <%= link_to t("nav.signup"), public_signup_path, class: "btn btn-primary nav-btn" %>
<% end %> <% end %>
<div class="nav-lang">
<%= render "shared/language_switcher" %>
</div>
</div> </div>
</div> </div>
</nav> </nav>
@@ -78,8 +84,16 @@
if (backdrop) backdrop.addEventListener("click", closeMenu); if (backdrop) backdrop.addEventListener("click", closeMenu);
function shouldCloseNavOnControl(el) {
// Language toggle must keep the mobile menu open; only a locale choice may close it.
if (!el.closest("[data-lang-switcher]")) return true;
return el.classList.contains("lang-switcher__option");
}
nav.querySelectorAll("a, button").forEach(function (el) { nav.querySelectorAll("a, button").forEach(function (el) {
el.addEventListener("click", closeMenu); el.addEventListener("click", function () {
if (shouldCloseNavOnControl(el)) closeMenu();
});
}); });
window.addEventListener("resize", function () { window.addEventListener("resize", function () {
@@ -0,0 +1,86 @@
<%# Chrome minimale per App Store Review: branding, lingua, Privacy e Supporto — senza CTA commerciali. %>
<div class="site-chrome">
<div class="site-masthead">
<div class="wrap mast-inner">
<%= link_to public_support_path, class: "mast-brand", aria: { label: "Match Live TV" }, title: "Match Live TV" do %>
<img class="mast-brand-logo" src="/logo.png?v=3" alt="Match Live TV" width="40" height="40" decoding="async">
<span class="brand" aria-hidden="true">Match <span>Live TV</span></span>
<% end %>
<div class="mast-tools">
<button type="button" class="nav-toggle" aria-label="<%= t('nav.open_menu') %>" aria-expanded="false" aria-controls="site-nav">
<span class="nav-toggle-bar" aria-hidden="true"></span>
<span class="nav-toggle-bar" aria-hidden="true"></span>
<span class="nav-toggle-bar" aria-hidden="true"></span>
</button>
</div>
</div>
</div>
<nav id="site-nav" class="nav" aria-label="<%= t('nav.main_menu') %>" aria-hidden="true">
<div class="nav-panel">
<div class="nav-mobile-head">
<%= link_to public_support_path, class: "nav-mobile-brand", aria: { label: "Match Live TV" }, title: "Match Live TV" do %>
<img class="nav-mobile-brand-logo" src="/logo.png?v=3" alt="" width="40" height="40" decoding="async">
<span class="brand">Match <span>Live TV</span></span>
<% end %>
<div class="nav-lang">
<%= render "shared/language_switcher" %>
</div>
</div>
<%= link_to t("common.support"), public_support_path, class: "nav-active" %>
<%= link_to t("common.privacy"), public_privacy_path, class: (request.path == "/privacy" ? "nav-active" : nil) %>
</div>
</nav>
</div>
<div class="nav-backdrop" id="nav-backdrop" aria-hidden="true"></div>
<script>
(function () {
var chrome = document.querySelector(".site-chrome");
var toggle = document.querySelector(".nav-toggle");
var backdrop = document.getElementById("nav-backdrop");
var nav = document.getElementById("site-nav");
if (!chrome || !toggle || !nav) return;
var openLabel = <%= raw t("nav.open_menu").to_json %>;
var closeLabel = <%= raw t("nav.close_menu").to_json %>;
function setOpen(open) {
chrome.classList.toggle("nav-open", open);
document.body.classList.toggle("nav-menu-open", open);
toggle.setAttribute("aria-expanded", open ? "true" : "false");
toggle.setAttribute("aria-label", open ? closeLabel : openLabel);
nav.setAttribute("aria-hidden", open ? "false" : "true");
if (backdrop) backdrop.setAttribute("aria-hidden", open ? "false" : "true");
}
function closeMenu() { setOpen(false); }
toggle.addEventListener("click", function (e) {
e.stopPropagation();
document.body.classList.contains("nav-menu-open") ? closeMenu() : setOpen(true);
});
if (backdrop) backdrop.addEventListener("click", closeMenu);
function shouldCloseNavOnControl(el) {
if (!el.closest("[data-lang-switcher]")) return true;
return el.classList.contains("lang-switcher__option");
}
nav.querySelectorAll("a, button").forEach(function (el) {
el.addEventListener("click", function () {
if (shouldCloseNavOnControl(el)) closeMenu();
});
});
window.addEventListener("resize", function () {
if (window.matchMedia("(min-width: 900px)").matches) closeMenu();
});
document.addEventListener("keydown", function (e) {
if (e.key === "Escape") closeMenu();
});
})();
</script>
+30 -14
View File
@@ -15,23 +15,33 @@
current_interval: current_interval, current_interval: current_interval,
subscription: subscription subscription: subscription
) : nil %> ) : nil %>
<div class="plan-card"> <% featured = plan.slug == "premium_full" %>
<div class="plan-card<%= " plan-card--featured" if featured %>">
<% if featured %>
<p class="plan-card__recommend"><%= t("pages.plans.recommended_badge") %></p>
<% end %>
<%= render "shared/plan_title", plan: plan %> <%= render "shared/plan_title", plan: plan %>
<% if plan.slug == "free" %> <% if plan.slug == "free" %>
<div class="plan-price">€0</div> <div class="plan-price">€0</div>
<% elsif plan.slug == "premium_light" %>
<% light_yearly = Billing::Stripe::PriceCatalog.label(plan_slug: plan.slug, interval: "yearly") %>
<% light_monthly = Billing::Stripe::PriceCatalog.label(plan_slug: plan.slug, interval: "monthly") %>
<div class="plan-price" style="color:#e53935"><%= light_yearly %></div>
<p class="plan-price-alt"><%= raw t("pages.plans.price_alt_html", price: light_monthly) %></p>
<% else %> <% else %>
<% full_yearly = Billing::Stripe::PriceCatalog.label(plan_slug: plan.slug, interval: "yearly") %> <% yearly = Billing::Stripe::PriceCatalog.label(plan_slug: plan.slug, interval: "yearly") %>
<% full_monthly = Billing::Stripe::PriceCatalog.label(plan_slug: plan.slug, interval: "monthly") %> <% monthly = Billing::Stripe::PriceCatalog.label(plan_slug: plan.slug, interval: "monthly") %>
<div class="plan-price"><%= full_yearly %></div> <% list = Billing::Stripe::PriceCatalog.list_label(plan_slug: plan.slug, interval: "yearly") %>
<p class="plan-price-alt"><%= raw t("pages.plans.price_alt_html", price: full_monthly) %></p> <% equiv = Billing::Stripe::PriceCatalog.equivalent_monthly_label(plan_slug: plan.slug) %>
<div class="plan-price-block">
<% if list.present? %>
<div class="plan-price-list"><s><%= list %></s></div>
<% end %>
<div class="plan-price<%= " plan-price--featured" if featured %>"><%= yearly %></div>
<span class="plan-price-badge"><%= t("pages.plans.launch_price") %></span>
<% if equiv.present? %>
<p class="plan-price-equiv"><%= t("pages.plans.price_equiv", price: equiv) %></p>
<% end %>
</div>
<p class="plan-price-alt"><%= raw t("pages.plans.price_alt_html", price: monthly) %></p>
<% end %> <% end %>
<ul> <ul>
<li><%= raw t("pages.plans.staff_html", count: plan.max_staff_transmission || t("pages.plans.staff_unlimited")) %></li> <li><%= raw(plan.max_staff_transmission ? t("pages.plans.staff_html", count: plan.max_staff_transmission) : t("pages.plans.staff_unlimited_html")) %></li>
<li><%= raw t("pages.plans.matches_html", count: plan.concurrent_streams_limit || t("pages.plans.matches_unlimited")) %></li> <li><%= raw t("pages.plans.matches_html", count: plan.concurrent_streams_limit || t("pages.plans.matches_unlimited")) %></li>
<li><%= raw t( <li><%= raw t(
"pages.plans.replay_html", "pages.plans.replay_html",
@@ -48,6 +58,9 @@
end end
) %></li> ) %></li>
</ul> </ul>
<% if plan.slug == "premium_full" %>
<p class="plan-staff-note"><%= t("pages.plans.staff_note_full") %></p>
<% end %>
<% if billing_mode %> <% if billing_mode %>
<% action_kind = action[:kind] %> <% action_kind = action[:kind] %>
<% if action_kind == :current %> <% if action_kind == :current %>
@@ -78,8 +91,12 @@
<%= link_to t("pages.plans.start_free"), public_signup_path, class: "btn btn-secondary" %> <%= link_to t("pages.plans.start_free"), public_signup_path, class: "btn btn-secondary" %>
<% else %> <% else %>
<div class="plan-interval-actions"> <div class="plan-interval-actions">
<% plan_intervals_for_display(Billing::Stripe::PriceCatalog.available_intervals(plan_slug: plan.slug)).each do |interval| %> <% %w[yearly monthly].each do |interval| %>
<%= link_to t("pages.plans.register_with_price", price: Billing::Stripe::PriceCatalog.label(plan_slug: plan.slug, interval: interval)), <%= link_to t(
"billing.messages.activate_button",
plan: plan_cta_name(plan),
price: Billing::Stripe::PriceCatalog.label(plan_slug: plan.slug, interval: interval)
),
public_signup_path(plan: plan.slug, interval: interval), public_signup_path(plan: plan.slug, interval: interval),
class: plan_interval_button_class(interval) %> class: plan_interval_button_class(interval) %>
<% end %> <% end %>
@@ -88,4 +105,3 @@
</div> </div>
<% end %> <% end %>
</div> </div>
@@ -0,0 +1,31 @@
<% variant = local_assigns.fetch(:variant, "hero") %>
<div class="store-badges store-badges--<%= variant %>">
<%= link_to MatchLiveTv.app_store_url,
class: "store-badge",
target: "_blank",
rel: "noopener noreferrer",
aria: { label: t("home.store_app_store_aria") } do %>
<%= image_tag "/images/store/apple.svg",
alt: "",
class: "store-badge__logo store-badge__logo--apple",
aria: { hidden: true } %>
<span>
<span class="store-badge__kicker"><%= t("home.store_app_store_kicker") %></span>
<span class="store-badge__name"><%= t("home.store_app_store_name") %></span>
</span>
<% end %>
<%= link_to MatchLiveTv.play_store_url,
class: "store-badge",
target: "_blank",
rel: "noopener noreferrer",
aria: { label: t("home.store_play_aria") } do %>
<%= image_tag "/images/store/google-play.svg",
alt: "",
class: "store-badge__logo store-badge__logo--play",
aria: { hidden: true } %>
<span>
<span class="store-badge__kicker"><%= t("home.store_play_kicker") %></span>
<span class="store-badge__name"><%= t("home.store_play_name") %></span>
</span>
<% end %>
</div>
@@ -81,6 +81,12 @@ module MatchLiveTv
ENV.fetch("PRIVACY_CONTACT_EMAIL", "privacy@matchlivetv.it") ENV.fetch("PRIVACY_CONTACT_EMAIL", "privacy@matchlivetv.it")
end end
# Email di supporto (App Store Support URL e contatti assistenza).
# Preferisce SUPPORT_CONTACT_EMAIL; default = contatto commerciale già usato nel sito.
def support_email
ENV["SUPPORT_CONTACT_EMAIL"].presence || "info@matchlivetv.it"
end
def privacy_controller_address def privacy_controller_address
ENV.fetch("PRIVACY_CONTROLLER_ADDRESS", "Via Guido De Ruggiero, 89 - 20142 - Milano (MI)") ENV.fetch("PRIVACY_CONTROLLER_ADDRESS", "Via Guido De Ruggiero, 89 - 20142 - Milano (MI)")
end end
@@ -174,5 +180,13 @@ module MatchLiveTv
def google_analytics_configured? def google_analytics_configured?
google_analytics_measurement_id.present? google_analytics_measurement_id.present?
end end
def app_store_url
"https://apps.apple.com/us/app/matchlivetv/id6799510705"
end
def play_store_url
"https://play.google.com/store/apps/details?id=com.matchlivetv.match_live_tv"
end
end end
end end
+4 -1
View File
@@ -18,6 +18,9 @@ de:
logout_success: Abgemeldet logout_success: Abgemeldet
password_current_incorrect: Das aktuelle Passwort ist falsch password_current_incorrect: Das aktuelle Passwort ist falsch
password_too_short: Das neue Passwort muss mindestens 8 Zeichen lang sein password_too_short: Das neue Passwort muss mindestens 8 Zeichen lang sein
password_too_long: Das neue Passwort darf höchstens 72 Zeichen lang sein
password_too_weak: Das neue Passwort muss mindestens 3 aus Kleinbuchstaben, Großbuchstaben, Zahlen und Symbolen enthalten
password_same_as_current: Das neue Passwort muss sich vom aktuellen unterscheiden
password_mismatch: Die Passwörter stimmen nicht überein password_mismatch: Die Passwörter stimmen nicht überein
password_updated: Passwort aktualisiert password_updated: Passwort aktualisiert
ops_acknowledged: Vorfall übernommen ops_acknowledged: Vorfall übernommen
@@ -53,7 +56,7 @@ de:
edit: edit:
title: Passwort ändern title: Passwort ändern
current_password_label: Aktuelles Passwort current_password_label: Aktuelles Passwort
new_password_label: "Neues Passwort (mind. 8 Zeichen)" new_password_label: "Neues Passwort (mind. 8, mindestens 3 Zeichenarten)"
confirm_password_label: Neues Passwort bestätigen confirm_password_label: Neues Passwort bestätigen
submit: Passwort speichern submit: Passwort speichern
cancel: Abbrechen cancel: Abbrechen
+4 -1
View File
@@ -18,6 +18,9 @@ en:
logout_success: Signed out logout_success: Signed out
password_current_incorrect: Current password is incorrect password_current_incorrect: Current password is incorrect
password_too_short: The new password must be at least 8 characters long password_too_short: The new password must be at least 8 characters long
password_too_long: New password cannot exceed 72 characters
password_too_weak: "New password must include at least 3 of: lowercase, uppercase, numbers and symbols"
password_same_as_current: New password must be different from the current password
password_mismatch: Passwords do not match password_mismatch: Passwords do not match
password_updated: Password updated password_updated: Password updated
ops_acknowledged: Incident acknowledged ops_acknowledged: Incident acknowledged
@@ -53,7 +56,7 @@ en:
edit: edit:
title: Change password title: Change password
current_password_label: Current password current_password_label: Current password
new_password_label: "New password (min. 8 characters)" new_password_label: "New password (min. 8, at least 3 character types)"
confirm_password_label: Confirm new password confirm_password_label: Confirm new password
submit: Save password submit: Save password
cancel: Cancel cancel: Cancel
+4 -1
View File
@@ -18,6 +18,9 @@ es:
logout_success: Sesión cerrada logout_success: Sesión cerrada
password_current_incorrect: La contraseña actual no es correcta password_current_incorrect: La contraseña actual no es correcta
password_too_short: La nueva contraseña debe tener al menos 8 caracteres password_too_short: La nueva contraseña debe tener al menos 8 caracteres
password_too_long: La nueva contraseña no puede superar los 72 caracteres
password_too_weak: "La nueva contraseña debe incluir al menos 3 entre: minúsculas, mayúsculas, números y símbolos"
password_same_as_current: La nueva contraseña debe ser distinta de la actual
password_mismatch: Las contraseñas no coinciden password_mismatch: Las contraseñas no coinciden
password_updated: Contraseña actualizada password_updated: Contraseña actualizada
ops_acknowledged: Incidencia asumida ops_acknowledged: Incidencia asumida
@@ -53,7 +56,7 @@ es:
edit: edit:
title: Cambiar contraseña title: Cambiar contraseña
current_password_label: Contraseña actual current_password_label: Contraseña actual
new_password_label: "Nueva contraseña (mín. 8 caracteres)" new_password_label: "Nueva contraseña (mín. 8, al menos 3 tipos de caracteres)"
confirm_password_label: Confirma la nueva contraseña confirm_password_label: Confirma la nueva contraseña
submit: Guardar contraseña submit: Guardar contraseña
cancel: Cancelar cancel: Cancelar
+4 -1
View File
@@ -18,6 +18,9 @@ fr:
logout_success: Déconnecté logout_success: Déconnecté
password_current_incorrect: Le mot de passe actuel est incorrect password_current_incorrect: Le mot de passe actuel est incorrect
password_too_short: Le nouveau mot de passe doit contenir au moins 8 caractères password_too_short: Le nouveau mot de passe doit contenir au moins 8 caractères
password_too_long: Le nouveau mot de passe ne peut pas dépasser 72 caractères
password_too_weak: "Le nouveau mot de passe doit inclure au moins 3 parmi : minuscules, majuscules, chiffres et symboles"
password_same_as_current: "Le nouveau mot de passe doit être différent de l'actuel"
password_mismatch: Les mots de passe ne correspondent pas password_mismatch: Les mots de passe ne correspondent pas
password_updated: Mot de passe mis à jour password_updated: Mot de passe mis à jour
ops_acknowledged: Incident pris en charge ops_acknowledged: Incident pris en charge
@@ -53,7 +56,7 @@ fr:
edit: edit:
title: Changer le mot de passe title: Changer le mot de passe
current_password_label: Mot de passe actuel current_password_label: Mot de passe actuel
new_password_label: "Nouveau mot de passe (min. 8 caractères)" new_password_label: "Nouveau mot de passe (min. 8, au moins 3 types de caractères)"
confirm_password_label: Confirmer le nouveau mot de passe confirm_password_label: Confirmer le nouveau mot de passe
submit: Enregistrer le mot de passe submit: Enregistrer le mot de passe
cancel: Annuler cancel: Annuler
+4 -1
View File
@@ -18,6 +18,9 @@ it:
logout_success: Disconnesso logout_success: Disconnesso
password_current_incorrect: Password attuale non corretta password_current_incorrect: Password attuale non corretta
password_too_short: La nuova password deve avere almeno 8 caratteri password_too_short: La nuova password deve avere almeno 8 caratteri
password_too_long: La nuova password non può superare i 72 caratteri
password_too_weak: "La nuova password deve includere almeno 3 tra: minuscole, maiuscole, numeri e simboli"
password_same_as_current: La nuova password deve essere diversa da quella attuale
password_mismatch: Le password non coincidono password_mismatch: Le password non coincidono
password_updated: Password aggiornata password_updated: Password aggiornata
ops_acknowledged: Incidente preso in carico ops_acknowledged: Incidente preso in carico
@@ -53,7 +56,7 @@ it:
edit: edit:
title: Cambia password title: Cambia password
current_password_label: Password attuale current_password_label: Password attuale
new_password_label: "Nuova password (min. 8 caratteri)" new_password_label: "Nuova password (min. 8, almeno 3 tipi di caratteri)"
confirm_password_label: Conferma nuova password confirm_password_label: Conferma nuova password
submit: Salva password submit: Salva password
cancel: Annulla cancel: Annulla
+38 -3
View File
@@ -1,4 +1,26 @@
de: de:
password_policy:
hint: "Mindestens 8 Zeichen, mit mindestens 3 aus: Kleinbuchstaben, Großbuchstaben, Zahlen und Symbolen."
activerecord:
attributes:
user:
password: Passwort
admin_account:
password: Passwort
errors:
models:
user:
attributes:
password:
too_short: "ist zu kurz (mindestens %{count} Zeichen)"
too_long: "ist zu lang (höchstens %{count} Zeichen)"
complexity: "muss mindestens 3 aus Kleinbuchstaben, Großbuchstaben, Zahlen und Symbolen enthalten"
admin_account:
attributes:
password:
too_short: "ist zu kurz (mindestens %{count} Zeichen)"
too_long: "ist zu lang (höchstens %{count} Zeichen)"
complexity: "muss mindestens 3 aus Kleinbuchstaben, Großbuchstaben, Zahlen und Symbolen enthalten"
club: club:
back_to_club: "← Verein" back_to_club: "← Verein"
sport_label: Hauptsportart sport_label: Hauptsportart
@@ -60,12 +82,12 @@ de:
first_team_name_label: Teamname first_team_name_label: Teamname
first_team_name_placeholder: "z. B. Under 15" first_team_name_placeholder: "z. B. Under 15"
plan_label: "Starttarif (für den gesamten Verein)" plan_label: "Starttarif (für den gesamten Verein)"
plan_free: "Free — 1 Übertragungsverantwortlicher pro Team, 1 Livestream" plan_free: "Free — 1 Streaming-Konto pro Team, 1 Livestream"
plan_light: Premium Light plan_light: Premium Light
plan_full: Premium Full plan_full: Premium Full
interval_label: Premium-Abrechnung interval_label: Premium-Abrechnung
interval_yearly: "Jährlich — 40 €/Jahr (Light) oder 200 €/Jahr (Full)" interval_yearly: "Jährlich — 59 €/Jahr (Light) oder 199 €/Jahr (Full)"
interval_monthly: "Monatlich — 5 €/Monat (Light) oder 20 €/Monat (Full)" interval_monthly: "Monatlich — 7,90 €/Monat (Light) oder 24,90 €/Monat (Full)"
submit: Verein erstellen submit: Verein erstellen
billing: billing:
title: "Abonnement — %{name}" title: "Abonnement — %{name}"
@@ -401,6 +423,9 @@ de:
upgrade_info_line: "Upgrade (höherer Tarif oder Wechsel zur jährlichen Abrechnung): Die Differenz wird sofort von der gespeicherten Stripe-Karte abgebucht." upgrade_info_line: "Upgrade (höherer Tarif oder Wechsel zur jährlichen Abrechnung): Die Differenz wird sofort von der gespeicherten Stripe-Karte abgebucht."
downgrade_info_line: "Downgrade zwischen kostenpflichtigen Tarifen (z. B. Full → Light): Der neue Tarif gilt ab der nächsten Verlängerung; bis dahin bleiben die Funktionen des aktuellen Tarifs ohne zusätzliche Kosten erhalten." downgrade_info_line: "Downgrade zwischen kostenpflichtigen Tarifen (z. B. Full → Light): Der neue Tarif gilt ab der nächsten Verlängerung; bis dahin bleiben die Funktionen des aktuellen Tarifs ohne zusätzliche Kosten erhalten."
already_requested_line: "Du hast %{plan} bereits ab dem %{when} angefordert: Bis dahin bleibt %{current_plan} aktiv." already_requested_line: "Du hast %{plan} bereits ab dem %{when} angefordert: Bis dahin bleibt %{current_plan} aktiv."
prices:
yearly: "%{amount}/Jahr"
monthly: "%{amount}/Monat"
regia: regia:
meta_title_fallback: "Regie — Match Live TV" meta_title_fallback: "Regie — Match Live TV"
title: "Regie — %{home} vs %{away}" title: "Regie — %{home} vs %{away}"
@@ -600,10 +625,16 @@ de:
welcome_back: Willkommen zurück! welcome_back: Willkommen zurück!
invalid_credentials: E-Mail oder Passwort ungültig invalid_credentials: E-Mail oder Passwort ungültig
logged_out: Abgemeldet logged_out: Abgemeldet
unauthorized: Nicht autorisiert
invalid_token: Ungültiges Token
password_resets: password_resets:
email_sent: Wenn die E-Mail registriert ist, erhältst du in Kürze einen Link zum Zurücksetzen des Passworts. email_sent: Wenn die E-Mail registriert ist, erhältst du in Kürze einen Link zum Zurücksetzen des Passworts.
invalid_or_expired_link: Link ungültig oder abgelaufen. Fordere ein neues Zurücksetzen des Passworts an. invalid_or_expired_link: Link ungültig oder abgelaufen. Fordere ein neues Zurücksetzen des Passworts an.
password_min_length: Das Passwort muss mindestens 8 Zeichen lang sein password_min_length: Das Passwort muss mindestens 8 Zeichen lang sein
password_too_short: Das Passwort muss mindestens 8 Zeichen lang sein
password_too_long: Das Passwort darf höchstens 72 Zeichen lang sein
password_too_weak: Das Passwort muss mindestens 3 aus Kleinbuchstaben, Großbuchstaben, Zahlen und Symbolen enthalten
password_same_as_current: Das neue Passwort muss sich vom aktuellen unterscheiden
password_mismatch: Die Passwörter stimmen nicht überein password_mismatch: Die Passwörter stimmen nicht überein
password_updated: Passwort aktualisiert. Du kannst dich jetzt anmelden. password_updated: Passwort aktualisiert. Du kannst dich jetzt anmelden.
accounts: accounts:
@@ -611,8 +642,12 @@ de:
profile_updated: Profil aktualisiert. profile_updated: Profil aktualisiert.
password_current_incorrect: Das aktuelle Passwort ist falsch password_current_incorrect: Das aktuelle Passwort ist falsch
password_too_short: Das Passwort muss mindestens 8 Zeichen haben password_too_short: Das Passwort muss mindestens 8 Zeichen haben
password_too_long: Das Passwort darf höchstens 72 Zeichen lang sein
password_too_weak: Das Passwort muss mindestens 3 aus Kleinbuchstaben, Großbuchstaben, Zahlen und Symbolen enthalten
password_same_as_current: Das neue Passwort muss sich vom aktuellen unterscheiden
password_mismatch: Die Passwörter stimmen nicht überein password_mismatch: Die Passwörter stimmen nicht überein
password_updated: Passwort aktualisiert. password_updated: Passwort aktualisiert.
password_update_failed: Passwort konnte nicht aktualisiert werden
replay: replay:
download_unavailable: Download nicht verfügbar download_unavailable: Download nicht verfügbar
not_available: Replay nicht verfügbar not_available: Replay nicht verfügbar
+33 -3
View File
@@ -1,4 +1,21 @@
en: en:
password_policy:
hint: "At least 8 characters, including 3 of: lowercase, uppercase, numbers and symbols."
activerecord:
errors:
models:
user:
attributes:
password:
too_short: "is too short (minimum is %{count} characters)"
too_long: "is too long (maximum is %{count} characters)"
complexity: "must include at least 3 of: lowercase letters, uppercase letters, numbers and symbols"
admin_account:
attributes:
password:
too_short: "is too short (minimum is %{count} characters)"
too_long: "is too long (maximum is %{count} characters)"
complexity: "must include at least 3 of: lowercase letters, uppercase letters, numbers and symbols"
club: club:
back_to_club: "← Club" back_to_club: "← Club"
sport_label: Main sport sport_label: Main sport
@@ -60,12 +77,12 @@ en:
first_team_name_label: Team name first_team_name_label: Team name
first_team_name_placeholder: "e.g. Under 15" first_team_name_placeholder: "e.g. Under 15"
plan_label: "Starting plan (for the whole club)" plan_label: "Starting plan (for the whole club)"
plan_free: "Free — 1 broadcast manager per team, 1 live stream" plan_free: "Free — 1 transmitter account per team, 1 live stream"
plan_light: Premium Light plan_light: Premium Light
plan_full: Premium Full plan_full: Premium Full
interval_label: Premium billing interval_label: Premium billing
interval_yearly: "Yearly — €40/year (Light) or €200/year (Full)" interval_yearly: "Yearly — €59/year (Light) or €199/year (Full)"
interval_monthly: "Monthly — €5/month (Light) or €20/month (Full)" interval_monthly: "Monthly — €7.90/month (Light) or €24.90/month (Full)"
submit: Create club submit: Create club
billing: billing:
title: "Subscription — %{name}" title: "Subscription — %{name}"
@@ -401,6 +418,9 @@ en:
upgrade_info_line: "Upgrade (higher plan or switch to annual billing): the difference is charged immediately to the saved Stripe card." upgrade_info_line: "Upgrade (higher plan or switch to annual billing): the difference is charged immediately to the saved Stripe card."
downgrade_info_line: "Downgrade between paid plans (e.g. Full → Light): the new plan takes effect from the next renewal; until then the current plan's features remain, with no additional charge now." downgrade_info_line: "Downgrade between paid plans (e.g. Full → Light): the new plan takes effect from the next renewal; until then the current plan's features remain, with no additional charge now."
already_requested_line: "You already requested %{plan} from %{when}: until then %{current_plan} stays active." already_requested_line: "You already requested %{plan} from %{when}: until then %{current_plan} stays active."
prices:
yearly: "%{amount}/year"
monthly: "%{amount}/month"
regia: regia:
meta_title_fallback: "Control room — Match Live TV" meta_title_fallback: "Control room — Match Live TV"
title: "Control room — %{home} vs %{away}" title: "Control room — %{home} vs %{away}"
@@ -600,10 +620,16 @@ en:
welcome_back: Welcome back! welcome_back: Welcome back!
invalid_credentials: Invalid email or password invalid_credentials: Invalid email or password
logged_out: Logged out logged_out: Logged out
unauthorized: Unauthorized
invalid_token: Invalid token
password_resets: password_resets:
email_sent: If the email is registered, you'll receive a password reset link shortly. email_sent: If the email is registered, you'll receive a password reset link shortly.
invalid_or_expired_link: Invalid or expired link. Request a new password reset. invalid_or_expired_link: Invalid or expired link. Request a new password reset.
password_min_length: Password must be at least 8 characters password_min_length: Password must be at least 8 characters
password_too_short: Password must be at least 8 characters
password_too_long: Password cannot exceed 72 characters
password_too_weak: "Password must include at least 3 of: lowercase, uppercase, numbers and symbols"
password_same_as_current: New password must be different from the current password
password_mismatch: Passwords don't match password_mismatch: Passwords don't match
password_updated: Password updated. You can now log in. password_updated: Password updated. You can now log in.
accounts: accounts:
@@ -611,8 +637,12 @@ en:
profile_updated: Profile updated. profile_updated: Profile updated.
password_current_incorrect: Current password is incorrect password_current_incorrect: Current password is incorrect
password_too_short: Password must be at least 8 characters password_too_short: Password must be at least 8 characters
password_too_long: Password cannot exceed 72 characters
password_too_weak: "Password must include at least 3 of: lowercase, uppercase, numbers and symbols"
password_same_as_current: New password must be different from the current password
password_mismatch: Passwords do not match password_mismatch: Passwords do not match
password_updated: Password updated. password_updated: Password updated.
password_update_failed: Unable to update password
replay: replay:
download_unavailable: Download not available download_unavailable: Download not available
not_available: Replay not available not_available: Replay not available
+38 -3
View File
@@ -1,4 +1,26 @@
es: es:
password_policy:
hint: "Mínimo 8 caracteres, con al menos 3 entre: minúsculas, mayúsculas, números y símbolos."
activerecord:
attributes:
user:
password: Contraseña
admin_account:
password: Contraseña
errors:
models:
user:
attributes:
password:
too_short: "es demasiado corta (mínimo %{count} caracteres)"
too_long: "es demasiado larga (máximo %{count} caracteres)"
complexity: "debe incluir al menos 3 entre: minúsculas, mayúsculas, números y símbolos"
admin_account:
attributes:
password:
too_short: "es demasiado corta (mínimo %{count} caracteres)"
too_long: "es demasiado larga (máximo %{count} caracteres)"
complexity: "debe incluir al menos 3 entre: minúsculas, mayúsculas, números y símbolos"
club: club:
back_to_club: "← Club" back_to_club: "← Club"
sport_label: Deporte principal sport_label: Deporte principal
@@ -60,12 +82,12 @@ es:
first_team_name_label: Nombre del equipo first_team_name_label: Nombre del equipo
first_team_name_placeholder: "ej. Sub-15" first_team_name_placeholder: "ej. Sub-15"
plan_label: "Plan inicial (para todo el club)" plan_label: "Plan inicial (para todo el club)"
plan_free: "Free — 1 responsable de transmisión por equipo, 1 directo" plan_free: "Free — 1 cuenta de emisión por equipo, 1 directo"
plan_light: Premium Light plan_light: Premium Light
plan_full: Premium Full plan_full: Premium Full
interval_label: Facturación premium interval_label: Facturación premium
interval_yearly: "Anual — 40 €/año (Light) o 200 €/año (Full)" interval_yearly: "Anual — 59 €/año (Light) o 199 €/año (Full)"
interval_monthly: "Mensual — 5 €/mes (Light) o 20 €/mes (Full)" interval_monthly: "Mensual — 7,90 €/mes (Light) o 24,90 €/mes (Full)"
submit: Crear club submit: Crear club
billing: billing:
title: "Suscripción — %{name}" title: "Suscripción — %{name}"
@@ -401,6 +423,9 @@ es:
upgrade_info_line: "Mejora (plan superior o cambio a facturación anual): la diferencia se cobra de inmediato en la tarjeta guardada en Stripe." upgrade_info_line: "Mejora (plan superior o cambio a facturación anual): la diferencia se cobra de inmediato en la tarjeta guardada en Stripe."
downgrade_info_line: "Downgrade entre planes de pago (p. ej. Full → Light): el nuevo plan se activa desde la próxima renovación; hasta entonces se mantienen las funciones del plan actual, sin cargo adicional ahora." downgrade_info_line: "Downgrade entre planes de pago (p. ej. Full → Light): el nuevo plan se activa desde la próxima renovación; hasta entonces se mantienen las funciones del plan actual, sin cargo adicional ahora."
already_requested_line: "Ya has solicitado %{plan} desde el %{when}: hasta entonces sigue activo %{current_plan}." already_requested_line: "Ya has solicitado %{plan} desde el %{when}: hasta entonces sigue activo %{current_plan}."
prices:
yearly: "%{amount}/año"
monthly: "%{amount}/mes"
regia: regia:
meta_title_fallback: "Control — Match Live TV" meta_title_fallback: "Control — Match Live TV"
title: "Control — %{home} vs %{away}" title: "Control — %{home} vs %{away}"
@@ -600,10 +625,16 @@ es:
welcome_back: "¡Bienvenido de nuevo!" welcome_back: "¡Bienvenido de nuevo!"
invalid_credentials: Correo o contraseña no válidos invalid_credentials: Correo o contraseña no válidos
logged_out: Sesión cerrada logged_out: Sesión cerrada
unauthorized: No autorizado
invalid_token: Token no válido
password_resets: password_resets:
email_sent: Si el correo está registrado, recibirás en breve un enlace para restablecer la contraseña. email_sent: Si el correo está registrado, recibirás en breve un enlace para restablecer la contraseña.
invalid_or_expired_link: Enlace no válido o caducado. Solicita un nuevo restablecimiento de contraseña. invalid_or_expired_link: Enlace no válido o caducado. Solicita un nuevo restablecimiento de contraseña.
password_min_length: La contraseña debe tener al menos 8 caracteres password_min_length: La contraseña debe tener al menos 8 caracteres
password_too_short: La contraseña debe tener al menos 8 caracteres
password_too_long: La contraseña no puede superar los 72 caracteres
password_too_weak: "La contraseña debe incluir al menos 3 entre: minúsculas, mayúsculas, números y símbolos"
password_same_as_current: La nueva contraseña debe ser distinta de la actual
password_mismatch: Las contraseñas no coinciden password_mismatch: Las contraseñas no coinciden
password_updated: Contraseña actualizada. Ya puedes iniciar sesión. password_updated: Contraseña actualizada. Ya puedes iniciar sesión.
accounts: accounts:
@@ -611,8 +642,12 @@ es:
profile_updated: Perfil actualizado. profile_updated: Perfil actualizado.
password_current_incorrect: La contraseña actual no es correcta password_current_incorrect: La contraseña actual no es correcta
password_too_short: La contraseña debe tener al menos 8 caracteres password_too_short: La contraseña debe tener al menos 8 caracteres
password_too_long: La contraseña no puede superar los 72 caracteres
password_too_weak: "La contraseña debe incluir al menos 3 entre: minúsculas, mayúsculas, números y símbolos"
password_same_as_current: La nueva contraseña debe ser distinta de la actual
password_mismatch: Las contraseñas no coinciden password_mismatch: Las contraseñas no coinciden
password_updated: Contraseña actualizada. password_updated: Contraseña actualizada.
password_update_failed: No se pudo actualizar la contraseña
replay: replay:
download_unavailable: Descarga no disponible download_unavailable: Descarga no disponible
not_available: Repetición no disponible not_available: Repetición no disponible
+38 -3
View File
@@ -1,4 +1,26 @@
fr: fr:
password_policy:
hint: "Au moins 8 caractères, avec au moins 3 parmi : minuscules, majuscules, chiffres et symboles."
activerecord:
attributes:
user:
password: Mot de passe
admin_account:
password: Mot de passe
errors:
models:
user:
attributes:
password:
too_short: "est trop court (minimum %{count} caractères)"
too_long: "est trop long (maximum %{count} caractères)"
complexity: "doit inclure au moins 3 parmi : minuscules, majuscules, chiffres et symboles"
admin_account:
attributes:
password:
too_short: "est trop court (minimum %{count} caractères)"
too_long: "est trop long (maximum %{count} caractères)"
complexity: "doit inclure au moins 3 parmi : minuscules, majuscules, chiffres et symboles"
club: club:
back_to_club: "← Club" back_to_club: "← Club"
sport_label: Sport principal sport_label: Sport principal
@@ -60,12 +82,12 @@ fr:
first_team_name_label: Nom de l'équipe first_team_name_label: Nom de l'équipe
first_team_name_placeholder: "ex. Under 15" first_team_name_placeholder: "ex. Under 15"
plan_label: "Forfait de départ (pour tout le club)" plan_label: "Forfait de départ (pour tout le club)"
plan_free: "Gratuit — 1 responsable diffusion par équipe, 1 direct" plan_free: "Gratuit — 1 compte diffuseur par équipe, 1 direct"
plan_light: Premium Light plan_light: Premium Light
plan_full: Premium Full plan_full: Premium Full
interval_label: Facturation premium interval_label: Facturation premium
interval_yearly: "Annuel — 40 €/an (Light) ou 200 €/an (Full)" interval_yearly: "Annuel — 59 €/an (Light) ou 199 €/an (Full)"
interval_monthly: "Mensuel — 5 €/mois (Light) ou 20 €/mois (Full)" interval_monthly: "Mensuel — 7,90 €/mois (Light) ou 24,90 €/mois (Full)"
submit: Créer le club submit: Créer le club
billing: billing:
title: "Abonnement — %{name}" title: "Abonnement — %{name}"
@@ -401,6 +423,9 @@ fr:
upgrade_info_line: "Upgrade (forfait supérieur ou passage à la facturation annuelle) : la différence est débitée immédiatement sur la carte Stripe enregistrée." upgrade_info_line: "Upgrade (forfait supérieur ou passage à la facturation annuelle) : la différence est débitée immédiatement sur la carte Stripe enregistrée."
downgrade_info_line: "Downgrade entre forfaits payants (ex. Full → Light) : le nouveau forfait est actif dès le prochain renouvellement ; jusque-là, les fonctionnalités du forfait actuel restent actives, sans débit supplémentaire pour l'instant." downgrade_info_line: "Downgrade entre forfaits payants (ex. Full → Light) : le nouveau forfait est actif dès le prochain renouvellement ; jusque-là, les fonctionnalités du forfait actuel restent actives, sans débit supplémentaire pour l'instant."
already_requested_line: "Vous avez déjà demandé %{plan} à partir du %{when} : jusque-là, %{current_plan} reste actif." already_requested_line: "Vous avez déjà demandé %{plan} à partir du %{when} : jusque-là, %{current_plan} reste actif."
prices:
yearly: "%{amount}/an"
monthly: "%{amount}/mois"
regia: regia:
meta_title_fallback: "Régie — Match Live TV" meta_title_fallback: "Régie — Match Live TV"
title: "Régie — %{home} vs %{away}" title: "Régie — %{home} vs %{away}"
@@ -600,10 +625,16 @@ fr:
welcome_back: Bon retour ! welcome_back: Bon retour !
invalid_credentials: E-mail ou mot de passe invalide invalid_credentials: E-mail ou mot de passe invalide
logged_out: Déconnecté logged_out: Déconnecté
unauthorized: Non autorisé
invalid_token: Jeton invalide
password_resets: password_resets:
email_sent: Si l'e-mail est enregistré, tu recevras bientôt un lien pour réinitialiser le mot de passe. email_sent: Si l'e-mail est enregistré, tu recevras bientôt un lien pour réinitialiser le mot de passe.
invalid_or_expired_link: Lien invalide ou expiré. Demande une nouvelle réinitialisation du mot de passe. invalid_or_expired_link: Lien invalide ou expiré. Demande une nouvelle réinitialisation du mot de passe.
password_min_length: Le mot de passe doit comporter au moins 8 caractères password_min_length: Le mot de passe doit comporter au moins 8 caractères
password_too_short: Le mot de passe doit comporter au moins 8 caractères
password_too_long: Le mot de passe ne peut pas dépasser 72 caractères
password_too_weak: "Le mot de passe doit inclure au moins 3 parmi : minuscules, majuscules, chiffres et symboles"
password_same_as_current: "Le nouveau mot de passe doit être différent de l'actuel"
password_mismatch: Les mots de passe ne correspondent pas password_mismatch: Les mots de passe ne correspondent pas
password_updated: Mot de passe mis à jour. Tu peux maintenant te connecter. password_updated: Mot de passe mis à jour. Tu peux maintenant te connecter.
accounts: accounts:
@@ -611,8 +642,12 @@ fr:
profile_updated: Profil mis à jour. profile_updated: Profil mis à jour.
password_current_incorrect: Le mot de passe actuel est incorrect password_current_incorrect: Le mot de passe actuel est incorrect
password_too_short: Le mot de passe doit contenir au moins 8 caractères password_too_short: Le mot de passe doit contenir au moins 8 caractères
password_too_long: Le mot de passe ne peut pas dépasser 72 caractères
password_too_weak: "Le mot de passe doit inclure au moins 3 parmi : minuscules, majuscules, chiffres et symboles"
password_same_as_current: "Le nouveau mot de passe doit être différent de l'actuel"
password_mismatch: Les mots de passe ne correspondent pas password_mismatch: Les mots de passe ne correspondent pas
password_updated: Mot de passe mis à jour. password_updated: Mot de passe mis à jour.
password_update_failed: Impossible de mettre à jour le mot de passe
replay: replay:
download_unavailable: Téléchargement non disponible download_unavailable: Téléchargement non disponible
not_available: Replay non disponible not_available: Replay non disponible
+38 -3
View File
@@ -1,4 +1,21 @@
it: it:
password_policy:
hint: "Minimo 8 caratteri, con almeno 3 tra: minuscole, maiuscole, numeri e simboli."
activerecord:
errors:
models:
user:
attributes:
password:
too_short: "è troppo corta (minimo %{count} caratteri)"
too_long: "è troppo lunga (massimo %{count} caratteri)"
complexity: "deve includere almeno 3 tra: lettere minuscole, maiuscole, numeri e simboli"
admin_account:
attributes:
password:
too_short: "è troppo corta (minimo %{count} caratteri)"
too_long: "è troppo lunga (massimo %{count} caratteri)"
complexity: "deve includere almeno 3 tra: lettere minuscole, maiuscole, numeri e simboli"
club: club:
back_to_club: "← Società" back_to_club: "← Società"
sport_label: Sport principale sport_label: Sport principale
@@ -60,12 +77,12 @@ it:
first_team_name_label: Nome squadra first_team_name_label: Nome squadra
first_team_name_placeholder: "es. Under 15" first_team_name_placeholder: "es. Under 15"
plan_label: "Piano iniziale (per tutta la società)" plan_label: "Piano iniziale (per tutta la società)"
plan_free: "Free — 1 responsabile trasmissione per squadra, 1 live" plan_free: "Free — 1 account trasmettitore per squadra, 1 live"
plan_light: Premium Light plan_light: Premium Light
plan_full: Premium Full plan_full: Premium Full
interval_label: Fatturazione premium interval_label: Fatturazione premium
interval_yearly: "Annuale — €40/anno (Light) o €200/anno (Full)" interval_yearly: "Annuale — €59/anno (Light) o €199/anno (Full)"
interval_monthly: "Mensile — €5/mese (Light) o €20/mese (Full)" interval_monthly: "Mensile — €7,90/mese (Light) o €24,90/mese (Full)"
submit: Crea società submit: Crea società
billing: billing:
title: "Abbonamento — %{name}" title: "Abbonamento — %{name}"
@@ -401,6 +418,9 @@ it:
upgrade_info_line: "Upgrade (piano superiore o passaggio alla fatturazione annuale): la differenza viene addebitata subito sulla carta salvata in Stripe." upgrade_info_line: "Upgrade (piano superiore o passaggio alla fatturazione annuale): la differenza viene addebitata subito sulla carta salvata in Stripe."
downgrade_info_line: "Downgrade tra piani a pagamento (es. Full → Light): il nuovo piano è attivo dal prossimo rinnovo; fino ad allora restano le funzioni del piano attuale, senza addebito aggiuntivo ora." downgrade_info_line: "Downgrade tra piani a pagamento (es. Full → Light): il nuovo piano è attivo dal prossimo rinnovo; fino ad allora restano le funzioni del piano attuale, senza addebito aggiuntivo ora."
already_requested_line: "Hai già richiesto %{plan} dal %{when}: fino ad allora resta attivo %{current_plan}." already_requested_line: "Hai già richiesto %{plan} dal %{when}: fino ad allora resta attivo %{current_plan}."
prices:
yearly: "%{amount}/anno"
monthly: "%{amount}/mese"
regia: regia:
meta_title_fallback: "Regia — Match Live TV" meta_title_fallback: "Regia — Match Live TV"
title: "Regia — %{home} vs %{away}" title: "Regia — %{home} vs %{away}"
@@ -600,10 +620,19 @@ it:
welcome_back: Bentornato! welcome_back: Bentornato!
invalid_credentials: Email o password non validi invalid_credentials: Email o password non validi
logged_out: Disconnesso logged_out: Disconnesso
unauthorized: Non autorizzato
invalid_token: Token non valido
password_resets: password_resets:
email_sent: Se l'email è registrata, riceverai a breve un link per reimpostare la password. email_sent: Se l'email è registrata, riceverai a breve un link per reimpostare la password.
invalid_or_expired_link: Link non valido o scaduto. Richiedi un nuovo reset password. invalid_or_expired_link: Link non valido o scaduto. Richiedi un nuovo reset password.
password_min_length: La password deve avere almeno 8 caratteri password_min_length: La password deve avere almeno 8 caratteri
password_too_short: La password deve avere almeno 8 caratteri
password_too_long: La password non può superare i 72 caratteri
password_too_weak: "La password deve includere almeno 3 tra: minuscole, maiuscole, numeri e simboli"
password_same_as_current: La nuova password deve essere diversa da quella attuale
password_too_short: La password deve avere almeno 8 caratteri
password_too_long: La password non può superare i 72 caratteri
password_too_weak: "La password deve includere almeno 3 tra: minuscole, maiuscole, numeri e simboli"
password_mismatch: Le password non coincidono password_mismatch: Le password non coincidono
password_updated: Password aggiornata. Ora puoi accedere. password_updated: Password aggiornata. Ora puoi accedere.
accounts: accounts:
@@ -611,8 +640,14 @@ it:
profile_updated: Profilo aggiornato. profile_updated: Profilo aggiornato.
password_current_incorrect: La password attuale non è corretta password_current_incorrect: La password attuale non è corretta
password_too_short: La password deve avere almeno 8 caratteri password_too_short: La password deve avere almeno 8 caratteri
password_too_long: La password non può superare i 72 caratteri
password_too_weak: "La password deve includere almeno 3 tra: minuscole, maiuscole, numeri e simboli"
password_same_as_current: La nuova password deve essere diversa da quella attuale
password_too_long: La password non può superare i 72 caratteri
password_too_weak: "La password deve includere almeno 3 tra: minuscole, maiuscole, numeri e simboli"
password_mismatch: Le password non coincidono password_mismatch: Le password non coincidono
password_updated: Password aggiornata. password_updated: Password aggiornata.
password_update_failed: Impossibile aggiornare la password
replay: replay:
download_unavailable: Download non disponibile download_unavailable: Download non disponibile
not_available: Replay non disponibile not_available: Replay non disponibile
+15
View File
@@ -195,3 +195,18 @@ de:
s6_body_html: "Um die von der DSGVO vorgesehenen Rechte auszuüben (Zugang, Löschung, Widerspruch, Widerruf der Einwilligung), schreiben Sie an %{email_link}. Details finden Sie im %{privacy_doc_link}." s6_body_html: "Um die von der DSGVO vorgesehenen Rechte auszuüben (Zugang, Löschung, Widerspruch, Widerruf der Einwilligung), schreiben Sie an %{email_link}. Details finden Sie im %{privacy_doc_link}."
s6_privacy_doc_link_text: Datenschutzdokument s6_privacy_doc_link_text: Datenschutzdokument
manage_button: Cookie-Einstellungen verwalten manage_button: Cookie-Einstellungen verwalten
support:
title: Match Live TV Support
meta_description: "Match Live TV Hilfe: Zugangsprobleme, Livestreams, Teamverwaltung und Support-Kontaktdaten."
h1: Match Live TV Support
intro: "Brauchen Sie Hilfe mit Match Live TV? Bei Problemen mit dem Zugang, der Einrichtung von Livestreams, der Spielverwaltung oder der Nutzung der App können Sie unseren Support kontaktieren."
email_label: "Support-E-Mail:"
access_title: Zugangsprobleme
access_body: "Wenn Sie sich nicht anmelden können, prüfen Sie die Zugangsdaten, die Sie von Ihrem Sportverein erhalten haben. Wenn das Problem weiterhin besteht, kontaktieren Sie den Support."
live_title: Probleme während eines Livestreams
live_body: "Prüfen Sie Ihre Internetverbindung und versuchen Sie erneut, die Übertragung in der App zu starten. Wenn das Problem weiterhin besteht, kontaktieren Sie den Support und geben Sie Gerät, App-Version und eine kurze Beschreibung des Problems an."
team_title: Teamverwaltung
team_body: "Konten und Übertragungsberechtigungen werden vom Sportverein verwaltet."
privacy_title: Datenschutz
privacy_body_html: "Informationen zur Verarbeitung personenbezogener Daten finden Sie auf der Seite %{privacy_link}."
privacy_link_text: Datenschutz
+15
View File
@@ -195,3 +195,18 @@ en:
s6_body_html: "To exercise the rights provided by the GDPR (access, erasure, objection, withdrawal of consent) write to %{email_link}. Details are in the %{privacy_doc_link}." s6_body_html: "To exercise the rights provided by the GDPR (access, erasure, objection, withdrawal of consent) write to %{email_link}. Details are in the %{privacy_doc_link}."
s6_privacy_doc_link_text: privacy document s6_privacy_doc_link_text: privacy document
manage_button: Manage cookie preferences manage_button: Manage cookie preferences
support:
title: Match Live TV Support
meta_description: "Match Live TV help: access issues, live streaming, team management and support contact details."
h1: Match Live TV Support
intro: "Need help with Match Live TV? For access issues, live stream setup, match management or app usage, you can contact our support team."
email_label: "Support email:"
access_title: Access issues
access_body: "If you cannot sign in, check the credentials provided by your sports club. If the problem persists, contact support."
live_title: Issues during a live stream
live_body: "Check your Internet connection and try starting the broadcast from the app again. If the problem persists, contact support and include your device, app version and a short description of the issue."
team_title: Team management
team_body: "Accounts and broadcasting permissions are managed by the sports club."
privacy_title: Privacy
privacy_body_html: "For information on personal data processing, see the %{privacy_link} page."
privacy_link_text: Privacy
+15
View File
@@ -195,3 +195,18 @@ es:
s6_body_html: "Para ejercer los derechos previstos por el RGPD (acceso, supresión, oposición, revocación del consentimiento) escribe a %{email_link}. Más detalles en el %{privacy_doc_link}." s6_body_html: "Para ejercer los derechos previstos por el RGPD (acceso, supresión, oposición, revocación del consentimiento) escribe a %{email_link}. Más detalles en el %{privacy_doc_link}."
s6_privacy_doc_link_text: documento de privacidad s6_privacy_doc_link_text: documento de privacidad
manage_button: Gestionar preferencias de cookies manage_button: Gestionar preferencias de cookies
support:
title: Soporte Match Live TV
meta_description: "Ayuda de Match Live TV: problemas de acceso, directos, gestión del equipo y datos de contacto del soporte."
h1: Soporte Match Live TV
intro: "¿Necesitas ayuda con Match Live TV? Para problemas de acceso, configuración de directos, gestión de partidos o uso de la app puedes contactar con nuestro soporte."
email_label: "Correo de soporte:"
access_title: Problemas de acceso
access_body: "Si no puedes acceder, verifica las credenciales recibidas de tu club deportivo. Si el problema continúa, contacta con el soporte."
live_title: Problemas durante un directo
live_body: "Verifica la conexión a Internet e intenta de nuevo iniciar la transmisión desde la app. Si el problema continúa, contacta con el soporte indicando el dispositivo, la versión de la app y una breve descripción del problema."
team_title: Gestión del equipo
team_body: "Las cuentas y los permisos de transmisión los gestiona el club deportivo."
privacy_title: Privacidad
privacy_body_html: "Para información sobre el tratamiento de datos personales, consulta la página %{privacy_link}."
privacy_link_text: Privacidad
+15
View File
@@ -195,3 +195,18 @@ fr:
s6_body_html: "Pour exercer les droits prévus par le RGPD (accès, effacement, opposition, retrait du consentement), écrivez à %{email_link}. Détails dans le %{privacy_doc_link}." s6_body_html: "Pour exercer les droits prévus par le RGPD (accès, effacement, opposition, retrait du consentement), écrivez à %{email_link}. Détails dans le %{privacy_doc_link}."
s6_privacy_doc_link_text: document de confidentialité s6_privacy_doc_link_text: document de confidentialité
manage_button: Gérer les préférences de cookies manage_button: Gérer les préférences de cookies
support:
title: Support Match Live TV
meta_description: "Assistance Match Live TV : problèmes d'accès, directs, gestion d'équipe et coordonnées du support."
h1: Support Match Live TV
intro: "Besoin d'aide avec Match Live TV ? Pour les problèmes d'accès, la configuration des directs, la gestion des matchs ou l'utilisation de l'application, vous pouvez contacter notre support."
email_label: "E-mail du support :"
access_title: Problèmes d'accès
access_body: "Si vous ne parvenez pas à vous connecter, vérifiez les identifiants fournis par votre club sportif. Si le problème persiste, contactez le support."
live_title: Problèmes pendant un direct
live_body: "Vérifiez votre connexion Internet et réessayez de démarrer la diffusion depuis l'application. Si le problème persiste, contactez le support en indiquant l'appareil, la version de l'application et une brève description du problème."
team_title: Gestion de l'équipe
team_body: "Les comptes et les autorisations de diffusion sont gérés par le club sportif."
privacy_title: Confidentialité
privacy_body_html: "Pour les informations sur le traitement des données personnelles, consultez la page %{privacy_link}."
privacy_link_text: Confidentialité
+15
View File
@@ -195,3 +195,18 @@ it:
s6_body_html: "Per esercitare i diritti previsti dal GDPR (accesso, cancellazione, opposizione, revoca consenso) scrivi a %{email_link}. Dettagli nel %{privacy_doc_link}." s6_body_html: "Per esercitare i diritti previsti dal GDPR (accesso, cancellazione, opposizione, revoca consenso) scrivi a %{email_link}. Dettagli nel %{privacy_doc_link}."
s6_privacy_doc_link_text: documento privacy s6_privacy_doc_link_text: documento privacy
manage_button: Gestisci preferenze cookie manage_button: Gestisci preferenze cookie
support:
title: Supporto Match Live TV
meta_description: "Assistenza Match Live TV: problemi di accesso, dirette live, gestione squadra e contatti del supporto."
h1: Supporto Match Live TV
intro: "Hai bisogno di assistenza con Match Live TV? Per problemi di accesso, configurazione delle dirette, gestione delle partite o utilizzo dellapp puoi contattare il nostro supporto."
email_label: "Email di supporto:"
access_title: Problemi di accesso
access_body: "Se non riesci ad accedere, verifica le credenziali ricevute dalla tua società sportiva. Se il problema persiste, contatta il supporto."
live_title: Problemi durante una diretta
live_body: "Verifica la connessione Internet e riprova ad avviare la trasmissione dallapp. Se il problema persiste, contatta il supporto indicando dispositivo, versione dellapp e una breve descrizione del problema."
team_title: Gestione della squadra
team_body: "Gli account e le autorizzazioni per la trasmissione sono gestiti dalla società sportiva."
privacy_title: Privacy
privacy_body_html: "Per informazioni sul trattamento dei dati personali consulta la pagina %{privacy_link}."
privacy_link_text: Privacy
+86 -31
View File
@@ -2,32 +2,80 @@ de:
pages: pages:
features: features:
meta_title: "Streaming-Funktionen für Jugendspiele — Match Live TV" meta_title: "Streaming-Funktionen für Jugendspiele — Match Live TV"
meta_description: "App für Livestreams vom Handy: Spiel filmen, Spielstand aktualisieren, Link mit Eltern und Großeltern teilen. Spielarchiv und Pläne für Jugend-Sportvereine." meta_description: "Übertragen Sie Vereinsspiele vom Smartphone mit persönlichen Konten, Spielstand per Link, Live auf Match Live TV und YouTube sowie Replay und Archiv. Ohne komplizierte Technik."
title: Funktionen hero_title: "Die Livestreams Ihres Vereins. Ohne geteilte Passwörter, ohne komplizierte Technik."
lead: "Alles, was nötig ist, damit auch die, die nicht in der Halle sein können, das Spiel verfolgen: einfach für Trainer und Helfer, praktisch für Eltern, Großeltern und Verwandte." hero_lead: "Laden Sie Trainer, Verantwortliche und Eltern ein, mit dem eigenen Konto zu streamen. Ein Smartphone reicht: Match Live TV übernimmt Zugänge, Spielstand, Live, Replay und Veröffentlichung."
staff_section_title: Für Verein und Team hero_alt: Smartphone auf Stativ für den Livestream und zweites Handy für den Spielstand in der Halle
staff_invite_title: Einladungen mit eigener E-Mail flow_aria: Ablauf der Übertragung
staff_invite_body: "Der Vorsitzende oder Inhaber lädt ein, wer filmt und wer den Spielstand aktualisiert: jeder meldet sich mit seiner eigenen E-Mail an, ohne Passwörter zu teilen." flow_phone: Ein Smartphone
staff_access_title: Zugriffsverwaltung flow_account: Ein persönliches Konto
staff_access_body: Sie können einem Helfer den Zugriff entziehen oder eine noch offene Einladung vom Team-Dashboard aus stornieren. flow_live: Sie sind live
staff_plans_title: Maßgeschneiderte Pläne how_title: Von der Halle zu den Zuschauern in wenigen Minuten
staff_plans_body: "Mit Free, Premium Light oder Premium Full erhöhen Sie, wie viele Personen live helfen können und wie viele Spiele parallel laufen können." how_1_title: Spiel anlegen
live_section_title: Auf dem Platz und live how_1_body: Der Verein bereitet das Spiel mit Team, Gegner, Datum und Uhrzeit vor.
live_phone_title: Vom Handy filmen how_2_title: App öffnen
live_phone_body: "Handy aufstellen und den Livestream starten: auch bei schwachem 4G bleibt die Übertragung so stabil wie möglich, ohne Kabelsalat oder Mischpult." how_2_body: Die beauftragte Person meldet sich in der Match-Live-TV-App mit dem eigenen persönlichen Konto an.
live_score_title: Spielstand vom zweiten Handy how_3_title: Livestream starten
live_score_body: "Ein weiterer Helfer aktualisiert Sätze und Punkte von einem zweiten Handy aus: einfach den Code scannen, der auf dem Bildschirm des Filmenden erscheint." how_3_body: Ein Smartphone reicht. Keine spezielle Ausrüstung und keine komplizierte Einrichtung.
live_link_title: Link für Familien und Fans how_4_title: Spielstand führen
live_link_body: "Teilen Sie einen Link: Eltern, Großeltern und Freunde schauen im Browser von Handy oder Computer, ohne App-Installation." how_4_body: Eine andere Person kann Ergebnis und Spielstand über einen einfachen Link aktualisieren ohne App und ohne Konto.
live_archive_title: Spielarchiv how_5_title: Replay
live_archive_body: "Mit Premium Light oder Full bleiben die Spiele auf dem Server gespeichert (30 oder 90 Tage) und Sie können sie bei Bedarf aufs Handy herunterladen." how_5_body: Nach dem Livestream kann das Spiel gespeichert und als Replay verfügbar gemacht werden je nach Plan.
live_youtube_mltv_title: Auch auf YouTube (Match Live TV) accounts_title: Jeder mit dem eigenen Konto. Das Vereins-Passwort bleibt beim Verein.
live_youtube_mltv_body: "Mit Premium Light ist der Livestream zusätzlich zum Link auf der Website auch auf dem YouTube-Kanal von Match Live TV zu sehen." accounts_lead: Username und Passwort des Hauptkontos müssen nicht mehr geteilt werden. Verantwortliche, Trainer und beauftragte Eltern erhalten einen persönlichen, widerrufbaren Zugang.
live_youtube_club_title: YouTube-Kanal des Vereins accounts_invite_title: Einladen
live_youtube_club_body: "Mit Premium Full können Sie denselben Livestream auch auf den offiziellen YouTube-Kanal des Vereins senden." accounts_invite_body: Der Verein lädt Trainer, Verantwortliche oder Eltern ein, die die Übertragung übernehmen.
live_website_title: Auf der Website des Vereins accounts_transmit_title: Übertragen
live_website_body: "Integration zum Einbetten des Players auf der Vereinswebsite — demnächst mit Premium Full." accounts_transmit_body: Jeder meldet sich in der App mit den eigenen Zugangsdaten an.
cta_signup: Team registrieren accounts_revoke_title: Widerrufen
accounts_revoke_body: Wenn jemand nicht mehr mit Team oder Verein zusammenarbeitet, kann der Zugang widerrufen werden, ohne das Passwort der anderen zu ändern.
accounts_highlight: Keine geteilten Passwörter. Jede beauftragte Person hat den eigenen persönlichen Zugang.
score_title: Wer filmt, filmt. Wer den Spielstand führt, führt den Spielstand.
score_lead: Wer überträgt, kann sich auf den Bildausschnitt konzentrieren. Eine andere Person auch von der Tribüne öffnet einen einfachen Link und aktualisiert den Spielstand in Echtzeit.
score_item_app: Keine App-Installation
score_item_signup: Keine Registrierung
score_item_password: Kein Passwort
score_item_link: Einfach den geteilten Link öffnen
score_card_aria: Zugang zur Spielstand-Regie
score_card_title: Regie per Link
score_card_body: Wer Sätze und Punkte aktualisiert, öffnet einen geteilten Link. Kein Konto, keine Einladung.
access_link: Zugang per Link
distribute_title: Ein Spiel, mehrere Wege es zu verfolgen
distribute_lead: Vom Smartphone in der Halle erreicht der Livestream die Zuschauer auf Match Live TV und je nach Plan auch YouTube.
distribute_aria: Wohin der Livestream geht
distribute_phone: Smartphone
distribute_mltv: Match Live TV
distribute_viewers: Zuschauer
distribute_youtube: YouTube
youtube_badge: Premium Full
youtube_title: Der Livestream bleibt bei Ihrem Verein
youtube_body: Mit Premium Full können Sie auch auf den offiziellen YouTube-Kanal des Vereins streamen. So behält der Verein Publikum, Abonnenten, Aufrufe, Spielarchiv und Sponsorensichtbarkeit.
youtube_mock_channel: Offizieller Vereinskanal
youtube_mock_subs: Abonnenten und Aufrufe des Vereins
replay_title: Das Spiel endet. Die Erinnerung bleibt.
replay_lead: Bei Plänen mit Replay bleiben Spiele auf den Match-Live-TV-Servern gespeichert und können später angesehen werden. Die Aufbewahrungsdauer hängt vom Plan ab.
replay_item_server: Speicherung auf Match-Live-TV-Servern
replay_item_plan: Archivdauer je nach Plan
replay_item_download: Video bei Bedarf aufs Handy herunterladen
replay_card_title: Spielarchiv
replay_card_body: Spiele nach dem Schlusspfiff noch einmal ansehen und bei Bedarf herunterladen.
replay_mock_1_title: Rossi vs Neri
replay_mock_1_meta: "12/05 · 01:25:34"
replay_mock_2_title: Rossi vs Blu
replay_mock_2_meta: "05/05 · 01:18:22"
replay_mock_3_title: Rossi vs Bianchi
replay_mock_3_meta: "28/04 · 01:07:15"
more_title: Auch das gehört zum Produkt
more_stable_title: Livestream vom Smartphone
more_stable_body: Handy aufstellen und Übertragung starten. Auch bei schwachem 4G bleibt der Livestream so stabil wie möglich ohne Kabel oder Mischpult.
more_link_title: Link für Familien und Fans
more_link_body: "Einen Link teilen: Eltern, Großeltern und Freunde schauen im Browser von Handy oder Computer ohne App."
more_yt_mltv_title: Auch auf YouTube Match Live TV
more_yt_mltv_body: Der Livestream kann zusätzlich zum Link auf der Website auch auf dem YouTube-Kanal von Match Live TV sichtbar sein.
cta_title: Bereit für das nächste Spiel?
cta_body: Probieren Sie Match Live TV kostenlos. Keine spezielle Ausrüstung nötig.
cta_primary: Kostenlos starten
cta_secondary: Pläne vergleichen
pricing: pricing:
meta_title: "Preise für Jugendspiel-Streaming — Match Live TV" meta_title: "Preise für Jugendspiel-Streaming — Match Live TV"
meta_description: "Free-, Premium-Light- und Premium-Full-Pläne für Livestreams und Spielarchiv. Jahresabo für Vereine: mehr Team-Mitglieder, mehr parallele Spiele, Replay und YouTube." meta_description: "Free-, Premium-Light- und Premium-Full-Pläne für Livestreams und Spielarchiv. Jahresabo für Vereine: mehr Team-Mitglieder, mehr parallele Spiele, Replay und YouTube."
@@ -35,8 +83,8 @@ de:
club_info_html: "Verein: <strong>%{name}</strong> · %{billing_link}" club_info_html: "Verein: <strong>%{name}</strong> · %{billing_link}"
manage_subscription: Abo verwalten manage_subscription: Abo verwalten
team_title: Pläne für Ihr Team team_title: Pläne für Ihr Team
team_lead: "Jahresabo für Vereine: die Streaming-Verantwortlichen starten den Livestream aus der App; der Spielstand wird über einen teilbaren Link verwaltet." team_lead: "Jahresabo für Vereine: die Streaming-Konten starten den Livestream aus der App; der Spielstand wird über einen teilbaren Link verwaltet."
table_staff: Streaming-Verantwortliche / Jahr table_staff: Streaming-Konten
table_matches: Gleichzeitige Spiele table_matches: Gleichzeitige Spiele
table_live_mltv: Live auf Match Live TV table_live_mltv: Live auf Match Live TV
table_youtube: YouTube table_youtube: YouTube
@@ -44,8 +92,7 @@ de:
table_download: Handy-Download table_download: Handy-Download
table_price: Preis table_price: Preis
table_price_free: "€0" table_price_free: "€0"
table_price_light_html: "€40/Jahr oder €5/Monat" table_price_note: "Listenpreis %{list} — %{monthly}"
table_price_full_html: "€200/Jahr oder €20/Monat"
table_yes: Ja table_yes: Ja
table_no: Nein table_no: Nein
table_unlimited: Unbegrenzt table_unlimited: Unbegrenzt
@@ -55,8 +102,16 @@ de:
contact_button: Kontaktieren Sie uns contact_button: Kontaktieren Sie uns
plans: plans:
price_alt_html: "oder <strong>%{price}</strong>" price_alt_html: "oder <strong>%{price}</strong>"
staff_html: "Streaming-Verantwortliche: <strong>%{count}</strong> / Jahr pro Team" price_equiv: "entspricht %{price}"
launch_price: Einführungspreis
recommended_badge: Empfohlen für Vereine
cta_name:
premium_light: Light
premium_full: Full
staff_html: "Streaming-Konten: maximal <strong>%{count}</strong> pro Abonnement"
staff_unlimited_html: "Streaming-Konten: <strong>unbegrenzt</strong> pro Abonnement"
staff_unlimited: unbegrenzt staff_unlimited: unbegrenzt
staff_note_full: "Keine geteilten Passwörter: jede Person nutzt den eigenen Zugang, jederzeit widerrufbar."
matches_html: "Spiele: <strong>%{count}</strong> gleichzeitig (ganzer Verein)" matches_html: "Spiele: <strong>%{count}</strong> gleichzeitig (ganzer Verein)"
matches_unlimited: unbegrenzt matches_unlimited: unbegrenzt
replay_html: "Replay: %{value}" replay_html: "Replay: %{value}"
+86 -31
View File
@@ -2,32 +2,80 @@ en:
pages: pages:
features: features:
meta_title: "Youth match streaming features — Match Live TV" meta_title: "Youth match streaming features — Match Live TV"
meta_description: "App for live streaming from your phone: film the match, update the score, share the link with parents and grandparents. Match archive and plans for youth sports clubs." meta_description: "Stream your club's matches from a smartphone with personal accounts, score via link, live on Match Live TV and YouTube, plus replay and archive. No complicated equipment."
title: Features hero_title: "Your club's live streams. No shared passwords, no complicated gear."
lead: "Everything you need so people who can't be at the gym can follow the match: simple for coaches and volunteers, convenient for parents, grandparents and relatives." hero_lead: "Invite coaches, managers and parents to stream with their own account. All you need is a smartphone: Match Live TV handles access, scoring, the live stream, replay and publishing."
staff_section_title: For the club and staff hero_alt: Smartphone on a tripod for the live stream and a second phone for the gym scoreboard
staff_invite_title: Invites with their own email flow_aria: Streaming flow
staff_invite_body: "The president or owner invites whoever films and whoever updates the score: each person signs in with their own email, no shared passwords." flow_phone: One smartphone
staff_access_title: Access management flow_account: One personal account
staff_access_body: You can remove a volunteer's access or cancel a pending invite from the team dashboard. flow_live: You're live
staff_plans_title: Plans to fit your needs how_title: From the gym to viewers in a few minutes
staff_plans_body: With Free, Premium Light or Premium Full you increase how many people can help stream live and how many matches can run in parallel. how_1_title: Create the match
live_section_title: On the court and live how_1_body: The club sets up the match with team, opponent, date and time.
live_phone_title: Film from your phone how_2_title: Open the app
live_phone_body: "Prop up the phone and start the stream: even with weak 4G the broadcast stays as stable as possible, with no cables or mixers to worry about." how_2_body: The assigned person signs into the Match Live TV app with their own personal account.
live_score_title: Score from a second phone how_3_title: Start the stream
live_score_body: "Another volunteer updates sets and points from a second phone: just scan the code shown on the streamer's screen." how_3_body: A smartphone is enough. No dedicated equipment or complicated setup.
live_link_title: Link for families and fans how_4_title: Manage the score
live_link_body: "Share a link: parents, grandparents and friends watch from their phone or computer browser, no app to install." how_4_body: Someone else can update the result and score through a simple link, without installing an app or creating an account.
live_archive_title: Match archive how_5_title: Replay
live_archive_body: "With Premium Light or Full, matches stay saved on the server (30 or 90 days) and you can download them to your phone whenever you need." how_5_body: After the stream ends, the match can be kept and made available as a replay, depending on the plan.
live_youtube_mltv_title: Also on YouTube (Match Live TV) accounts_title: Everyone with their own account. The club password stays with the club.
live_youtube_mltv_body: With Premium Light the stream is also visible on the Match Live TV YouTube channel, in addition to the link on the site. accounts_lead: You no longer need to share the main club username and password. Managers, coaches and assigned parents get a personal, revocable login.
live_youtube_club_title: Your club's YouTube channel accounts_invite_title: Invite
live_youtube_club_body: With Premium Full you can send the same stream to your club's official YouTube channel. accounts_invite_body: The club invites coaches, managers or parents assigned to stream.
live_website_title: On the club's website accounts_transmit_title: Stream
live_website_body: Integration to embed the player on the club's website — coming with Premium Full. accounts_transmit_body: Each person signs into the app with their own credentials.
cta_signup: Register your team accounts_revoke_title: Revoke
accounts_revoke_body: When someone no longer works with the team or club, their access can be revoked without changing everyone else's password.
accounts_highlight: No shared passwords. Every assigned person has their own personal access.
score_title: Who films, films. Who keeps score, keeps score.
score_lead: The person streaming can focus on the framing. Someone else, even from the stands, can open a simple link and update the score in real time.
score_item_app: No app to install
score_item_signup: No sign-up
score_item_password: No password
score_item_link: Just open the shared link
score_card_aria: Score control access
score_card_title: Score control via a link
score_card_body: Whoever updates sets and points opens a shared link. No account and no invite.
access_link: Open with a link
distribute_title: One match, more ways to follow it
distribute_lead: From the smartphone in the gym, the stream reaches viewers on Match Live TV and, when included in the plan, also on YouTube.
distribute_aria: Where the stream goes
distribute_phone: Smartphone
distribute_mltv: Match Live TV
distribute_viewers: Viewers
distribute_youtube: YouTube
youtube_badge: Premium Full
youtube_title: The stream stays with your club
youtube_body: With Premium Full you can also stream to the club's official YouTube channel. The club keeps its audience, subscribers, views, match archive and sponsor visibility.
youtube_mock_channel: Official club channel
youtube_mock_subs: Club subscribers and views
replay_title: The match ends. The memory stays.
replay_lead: On plans that include replay, matches are stored on Match Live TV servers and can be watched later. How long they are kept depends on the plan.
replay_item_server: Stored on Match Live TV servers
replay_item_plan: Archive length based on the plan
replay_item_download: Download the video to your phone when you need it
replay_card_title: Match archive
replay_card_body: Rewatch matches after the final whistle and download them when you need them.
replay_mock_1_title: Rossi vs Neri
replay_mock_1_meta: "12/05 · 01:25:34"
replay_mock_2_title: Rossi vs Blu
replay_mock_2_meta: "05/05 · 01:18:22"
replay_mock_3_title: Rossi vs Bianchi
replay_mock_3_meta: "28/04 · 01:07:15"
more_title: Also part of the product
more_stable_title: Stream from your smartphone
more_stable_body: Prop up the phone and start streaming. Even with weak 4G the broadcast stays as stable as possible, with no cables or mixers.
more_link_title: Link for families and fans
more_link_body: "Share a link: parents, grandparents and friends watch from their phone or computer browser, no app to install."
more_yt_mltv_title: Also on Match Live TV YouTube
more_yt_mltv_body: You can also make the stream visible on the Match Live TV YouTube channel, in addition to the link on the site.
cta_title: Ready for the next match?
cta_body: Try Match Live TV for free. No dedicated equipment needed.
cta_primary: Start for free
cta_secondary: Compare plans
pricing: pricing:
meta_title: "Youth match streaming pricing — Match Live TV" meta_title: "Youth match streaming pricing — Match Live TV"
meta_description: "Free, Premium Light and Premium Full plans for live streaming and match archive. Annual subscription for clubs: more staff, more concurrent matches, replay and YouTube." meta_description: "Free, Premium Light and Premium Full plans for live streaming and match archive. Annual subscription for clubs: more staff, more concurrent matches, replay and YouTube."
@@ -35,8 +83,8 @@ en:
club_info_html: "Club: <strong>%{name}</strong> · %{billing_link}" club_info_html: "Club: <strong>%{name}</strong> · %{billing_link}"
manage_subscription: Manage subscription manage_subscription: Manage subscription
team_title: Plans for your team team_title: Plans for your team
team_lead: "Annual subscription for clubs: streaming staff start the broadcast from the app; the score is managed with a shareable link." team_lead: "Annual subscription for clubs: transmitter accounts start the broadcast from the app; the score is managed with a shareable link."
table_staff: Streaming staff / year table_staff: Transmitter accounts
table_matches: Concurrent matches table_matches: Concurrent matches
table_live_mltv: Live on Match Live TV table_live_mltv: Live on Match Live TV
table_youtube: YouTube table_youtube: YouTube
@@ -44,8 +92,7 @@ en:
table_download: Phone download table_download: Phone download
table_price: Price table_price: Price
table_price_free: "€0" table_price_free: "€0"
table_price_light_html: "€40/year or €5/month" table_price_note: "list %{list} — %{monthly}"
table_price_full_html: "€200/year or €20/month"
table_yes: "Yes" table_yes: "Yes"
table_no: "No" table_no: "No"
table_unlimited: Unlimited table_unlimited: Unlimited
@@ -55,8 +102,16 @@ en:
contact_button: Contact us contact_button: Contact us
plans: plans:
price_alt_html: "or <strong>%{price}</strong>" price_alt_html: "or <strong>%{price}</strong>"
staff_html: "Streaming staff: <strong>%{count}</strong> / year per team" price_equiv: "equivalent to %{price}"
launch_price: Launch price
recommended_badge: Recommended for clubs
cta_name:
premium_light: Light
premium_full: Full
staff_html: "Transmitter accounts: max <strong>%{count}</strong> per subscription"
staff_unlimited_html: "Transmitter accounts: <strong>unlimited</strong> per subscription"
staff_unlimited: unlimited staff_unlimited: unlimited
staff_note_full: "No shared passwords: each person uses their own login, which can be revoked at any time."
matches_html: "Matches: <strong>%{count}</strong> concurrent (whole club)" matches_html: "Matches: <strong>%{count}</strong> concurrent (whole club)"
matches_unlimited: unlimited matches_unlimited: unlimited
replay_html: "Replay: %{value}" replay_html: "Replay: %{value}"
+87 -32
View File
@@ -2,32 +2,80 @@ es:
pages: pages:
features: features:
meta_title: "Funciones de streaming de partidos juveniles — Match Live TV" meta_title: "Funciones de streaming de partidos juveniles — Match Live TV"
meta_description: "App para el directo desde el móvil: graba el partido, actualiza el marcador, comparte el enlace con padres y abuelos. Archivo de partidos y planes para clubes deportivos juveniles." meta_description: "Emite los partidos del club desde el móvil con cuentas personales, marcador por enlace, directo en Match Live TV y YouTube, más repetición y archivo. Sin equipo complicado."
title: Funciones hero_title: "Los directos de tu club. Sin contraseñas compartidas, sin equipo complicado."
lead: "Todo lo necesario para que quienes no pueden estar en el pabellón sigan el partido: sencillo para entrenadores y voluntarios, cómodo para padres, abuelos y familiares." hero_lead: "Invita a entrenadores, directivos y padres a emitir con su propia cuenta. Basta un smartphone: Match Live TV gestiona accesos, marcador, directo, repetición y publicación."
staff_section_title: Para el club y el staff hero_alt: Smartphone en trípode para el directo y segundo móvil para el marcador en el pabellón
staff_invite_title: Invitaciones con su propio email flow_aria: Flujo de la emisión
staff_invite_body: "El presidente o propietario invita a quien graba y a quien actualiza el marcador: cada uno accede con su propio email, sin compartir contraseñas." flow_phone: Un smartphone
staff_access_title: Gestión de accesos flow_account: Una cuenta personal
staff_access_body: Puedes retirar el acceso a un voluntario o cancelar una invitación pendiente desde el panel del equipo. flow_live: Estás en directo
staff_plans_title: Planes a medida how_title: Del pabellón a los espectadores en pocos minutos
staff_plans_body: Con Free, Premium Light o Premium Full aumentas cuántas personas pueden ayudar en directo y cuántos partidos pueden ir en paralelo. how_1_title: Crea el partido
live_section_title: En la cancha y en directo how_1_body: El club prepara el partido indicando equipo, rival, fecha y hora.
live_phone_title: Graba desde el móvil how_2_title: Abre la app
live_phone_body: "Apoyas el móvil e inicias el directo: incluso con 4G débil, la transmisión se mantiene lo más estable posible, sin líos de cables ni mezcladores." how_2_body: La persona encargada accede a la app Match Live TV con su propia cuenta personal.
live_score_title: Marcador desde un segundo móvil how_3_title: Empieza el directo
live_score_body: "Otro voluntario actualiza sets y puntos desde un segundo móvil: solo hay que escanear el código que aparece en la pantalla de quien graba." how_3_body: Basta el smartphone. Sin equipo dedicado ni configuración complicada.
live_link_title: Enlace para familias y aficionados how_4_title: Gestiona el marcador
live_link_body: "Comparte un enlace: padres, abuelos y amigos miran desde el navegador del móvil o del ordenador, sin instalar apps." how_4_body: Otra persona puede actualizar resultado y marcador con un enlace sencillo, sin instalar apps ni crear una cuenta.
live_archive_title: Archivo de partidos how_5_title: Repetición
live_archive_body: "Con Premium Light o Full los partidos quedan guardados en el servidor (30 o 90 días) y puedes descargarlos al móvil cuando quieras." how_5_body: Al terminar el directo, el partido puede conservarse y quedar disponible como repetición, según el plan elegido.
live_youtube_mltv_title: También en YouTube (Match Live TV) accounts_title: Cada uno con su propia cuenta. La contraseña del club se queda en el club.
live_youtube_mltv_body: "Con Premium Light el directo también se ve en el canal de YouTube de Match Live TV, además del enlace en el sitio." accounts_lead: Ya no hace falta compartir el usuario y la contraseña de la cuenta principal. Directivos, entrenadores y padres encargados reciben un acceso personal y revocable.
live_youtube_club_title: Canal de YouTube del club accounts_invite_title: Invita
live_youtube_club_body: "Con Premium Full puedes enviar el mismo directo también al canal oficial de YouTube del club." accounts_invite_body: El club invita a entrenadores, directivos o padres encargados de la emisión.
live_website_title: En el sitio web del club accounts_transmit_title: Emite
live_website_body: "Integración para incrustar el reproductor en el sitio web del club — próximamente con Premium Full." accounts_transmit_body: Cada uno accede a la app con sus propias credenciales.
cta_signup: Registra tu equipo accounts_revoke_title: Revoca
accounts_revoke_body: Cuando una persona deja de colaborar con el equipo o el club, su acceso puede revocarse sin cambiar la contraseña a los demás.
accounts_highlight: Sin contraseñas compartidas. Cada encargado tiene su propio acceso personal.
score_title: Quien graba, graba. Quien lleva el marcador, lleva el marcador.
score_lead: Quien emite puede centrarse en el encuadre. Otra persona, incluso desde la grada, puede abrir un enlace sencillo y actualizar el marcador en tiempo real.
score_item_app: Sin app que instalar
score_item_signup: Sin registro
score_item_password: Sin contraseña
score_item_link: Basta abrir el enlace compartido
score_card_aria: Acceso a la regia del marcador
score_card_title: Regia con un enlace
score_card_body: Quien actualiza sets y puntos abre un enlace compartido. Sin cuenta y sin invitación.
access_link: Acceso con enlace
distribute_title: Un partido, más formas de seguirlo
distribute_lead: Desde el smartphone en el pabellón, el directo llega a los espectadores en Match Live TV y, según el plan, también a YouTube.
distribute_aria: Dónde llega el directo
distribute_phone: Smartphone
distribute_mltv: Match Live TV
distribute_viewers: Espectadores
distribute_youtube: YouTube
youtube_badge: Premium Full
youtube_title: El directo se queda en tu club
youtube_body: Con Premium Full también puedes emitir en el canal oficial de YouTube del club. Así el club conserva su público, suscriptores, visualizaciones, archivo de partidos y visibilidad de patrocinadores.
youtube_mock_channel: Canal oficial del club
youtube_mock_subs: Suscriptores y visualizaciones del club
replay_title: El partido termina. El recuerdo permanece.
replay_lead: En los planes con repetición, los partidos se guardan en los servidores de Match Live TV y pueden verse después. La duración del archivo depende del plan.
replay_item_server: Conservación en servidores Match Live TV
replay_item_plan: Duración del archivo según el plan
replay_item_download: Descarga del vídeo al móvil cuando haga falta
replay_card_title: Archivo de partidos
replay_card_body: Vuelve a ver los partidos tras el pitido final y descárgalos cuando los necesites.
replay_mock_1_title: Rossi vs Neri
replay_mock_1_meta: "12/05 · 01:25:34"
replay_mock_2_title: Rossi vs Blu
replay_mock_2_meta: "05/05 · 01:18:22"
replay_mock_3_title: Rossi vs Bianchi
replay_mock_3_meta: "28/04 · 01:07:15"
more_title: También forma parte del producto
more_stable_title: Directo desde el smartphone
more_stable_body: Apoyas el móvil e inicias la emisión. Incluso con 4G débil, el directo se mantiene lo más estable posible, sin cables ni mezcladores.
more_link_title: Enlace para familias y aficionados
more_link_body: "Comparte un enlace: padres, abuelos y amigos miran desde el navegador del móvil o del ordenador, sin instalar apps."
more_yt_mltv_title: También en YouTube Match Live TV
more_yt_mltv_body: También puedes hacer visible el directo en el canal de YouTube de Match Live TV, además del enlace en el sitio.
cta_title: ¿Listos para el próximo partido?
cta_body: Prueba Match Live TV gratis. No hace falta equipo dedicado.
cta_primary: Empieza gratis
cta_secondary: Compara los planes
pricing: pricing:
meta_title: "Precios de streaming de partidos juveniles — Match Live TV" meta_title: "Precios de streaming de partidos juveniles — Match Live TV"
meta_description: "Planes Free, Premium Light y Premium Full para directos y archivo de partidos. Suscripción anual para clubes: más staff, más partidos en paralelo, repetición y YouTube." meta_description: "Planes Free, Premium Light y Premium Full para directos y archivo de partidos. Suscripción anual para clubes: más staff, más partidos en paralelo, repetición y YouTube."
@@ -35,8 +83,8 @@ es:
club_info_html: "Club: <strong>%{name}</strong> · %{billing_link}" club_info_html: "Club: <strong>%{name}</strong> · %{billing_link}"
manage_subscription: Gestionar suscripción manage_subscription: Gestionar suscripción
team_title: Planes para tu equipo team_title: Planes para tu equipo
team_lead: "Suscripción anual para clubes: los responsables de emisión inician el directo desde la app; el marcador se gestiona con un enlace para compartir." team_lead: "Suscripción anual para clubes: las cuentas de emisión inician el directo desde la app; el marcador se gestiona con un enlace para compartir."
table_staff: Responsables de emisión / año table_staff: Cuentas de emisión
table_matches: Partidos simultáneos table_matches: Partidos simultáneos
table_live_mltv: Directo en Match Live TV table_live_mltv: Directo en Match Live TV
table_youtube: YouTube table_youtube: YouTube
@@ -44,8 +92,7 @@ es:
table_download: Descarga al móvil table_download: Descarga al móvil
table_price: Precio table_price: Precio
table_price_free: "€0" table_price_free: "€0"
table_price_light_html: "€40/año o €5/mes" table_price_note: "tarifa %{list} — %{monthly}"
table_price_full_html: "€200/año o €20/mes"
table_yes: "Sí" table_yes: "Sí"
table_no: "No" table_no: "No"
table_unlimited: Ilimitado table_unlimited: Ilimitado
@@ -55,8 +102,16 @@ es:
contact_button: Contáctanos contact_button: Contáctanos
plans: plans:
price_alt_html: "o <strong>%{price}</strong>" price_alt_html: "o <strong>%{price}</strong>"
staff_html: "Responsables de emisión: <strong>%{count}</strong> / año por equipo" price_equiv: "equivale a %{price}"
staff_unlimited: ilimitado launch_price: Precio de lanzamiento
recommended_badge: Recomendado para clubes
cta_name:
premium_light: Light
premium_full: Full
staff_html: "Cuentas de emisión: máximo <strong>%{count}</strong> por suscripción"
staff_unlimited_html: "Cuentas de emisión: <strong>ilimitadas</strong> por suscripción"
staff_unlimited: ilimitadas
staff_note_full: "Sin contraseñas compartidas: cada persona usa su propio acceso, revocable en cualquier momento."
matches_html: "Partidos: <strong>%{count}</strong> simultáneos (todo el club)" matches_html: "Partidos: <strong>%{count}</strong> simultáneos (todo el club)"
matches_unlimited: ilimitados matches_unlimited: ilimitados
replay_html: "Repetición: %{value}" replay_html: "Repetición: %{value}"
+87 -32
View File
@@ -2,32 +2,80 @@ fr:
pages: pages:
features: features:
meta_title: "Fonctionnalités de streaming des matchs jeunes — Match Live TV" meta_title: "Fonctionnalités de streaming des matchs jeunes — Match Live TV"
meta_description: "Application pour le direct depuis le téléphone : filmez le match, mettez à jour le score, partagez le lien avec parents et grands-parents. Archive des matchs et offres pour les clubs sportifs jeunes." meta_description: "Diffusez les matchs du club depuis un smartphone avec des comptes personnels, le score via un lien, le direct sur Match Live TV et YouTube, plus le replay et l'archive. Sans matériel compliqué."
title: Fonctionnalités hero_title: "Les directs de votre club. Sans mots de passe partagés, sans matériel compliqué."
lead: "Tout ce qu'il faut pour faire suivre les matchs à ceux qui ne peuvent pas être en salle : simple pour les coachs et bénévoles, pratique pour parents, grands-parents et proches." hero_lead: "Invitez entraîneurs, dirigeants et parents à diffuser avec leur propre compte. Un smartphone suffit : Match Live TV gère les accès, le score, le direct, le replay et la publication."
staff_section_title: Pour le club et le staff hero_alt: Smartphone sur trépied pour le direct et second téléphone pour le score en salle
staff_invite_title: Invitations avec leur propre email flow_aria: Déroulement de la diffusion
staff_invite_body: "Le président ou le propriétaire invite qui filme et qui met à jour le score : chacun se connecte avec son propre email, sans partager de mot de passe." flow_phone: Un smartphone
staff_access_title: Gestion des accès flow_account: Un compte personnel
staff_access_body: Vous pouvez retirer l'accès à un bénévole ou annuler une invitation encore en attente depuis le tableau de bord de l'équipe. flow_live: Vous êtes en direct
staff_plans_title: Des offres sur mesure how_title: De la salle aux spectateurs en quelques minutes
staff_plans_body: "Avec Free, Premium Light ou Premium Full, augmentez le nombre de personnes qui peuvent aider en direct et le nombre de matchs en parallèle." how_1_title: Créez le match
live_section_title: Sur le terrain et en direct how_1_body: Le club prépare le match en indiquant l'équipe, l'adversaire, la date et l'heure.
live_phone_title: Filmez depuis votre téléphone how_2_title: Ouvrez l'application
live_phone_body: "Posez le téléphone et lancez le direct : même avec un 4G faible, la diffusion reste aussi stable que possible, sans câbles ni mixeur." how_2_body: La personne désignée se connecte à l'application Match Live TV avec son propre compte personnel.
live_score_title: Score depuis un second téléphone how_3_title: Lancez le direct
live_score_body: "Un autre bénévole met à jour les sets et les points depuis un second téléphone : il suffit de scanner le code affiché sur l'écran de celui qui filme." how_3_body: Un smartphone suffit. Pas de matériel dédié ni de configuration compliquée.
live_link_title: Lien pour les familles et supporters how_4_title: Gérez le score
live_link_body: "Partagez un lien : parents, grands-parents et amis regardent depuis le navigateur du téléphone ou de l'ordinateur, sans installer d'application." how_4_body: Une autre personne peut mettre à jour le résultat et le score via un simple lien, sans installer d'application ni créer de compte.
live_archive_title: Archive des matchs how_5_title: Replay
live_archive_body: "Avec Premium Light ou Full, les matchs restent enregistrés sur le serveur (30 ou 90 jours) et vous pouvez les télécharger sur le téléphone quand vous voulez." how_5_body: À la fin du direct, le match peut être conservé et rendu disponible en replay, selon l'offre choisie.
live_youtube_mltv_title: Aussi sur YouTube (Match Live TV) accounts_title: Chacun avec son propre compte. Le mot de passe du club reste au club.
live_youtube_mltv_body: "Avec Premium Light, le direct est aussi visible sur la chaîne YouTube Match Live TV, en plus du lien sur le site." accounts_lead: Plus besoin de partager l'identifiant et le mot de passe du compte principal. Dirigeants, entraîneurs et parents désignés reçoivent un accès personnel et révocable.
live_youtube_club_title: Chaîne YouTube du club accounts_invite_title: Invitez
live_youtube_club_body: "Avec Premium Full, vous pouvez diffuser le même direct sur la chaîne YouTube officielle du club." accounts_invite_body: Le club invite les entraîneurs, dirigeants ou parents chargés de la diffusion.
live_website_title: Sur le site du club accounts_transmit_title: Diffusez
live_website_body: "Intégration pour intégrer le lecteur sur le site du club — bientôt disponible avec Premium Full." accounts_transmit_body: Chacun se connecte à l'application avec ses propres identifiants.
cta_signup: Inscrire l'équipe accounts_revoke_title: Révoquez
accounts_revoke_body: Quand une personne ne collabore plus avec l'équipe ou le club, son accès peut être révoqué sans changer le mot de passe des autres.
accounts_highlight: Pas de mots de passe partagés. Chaque personne désignée a son propre accès personnel.
score_title: Qui filme, filme. Qui tient le score, tient le score.
score_lead: La personne qui diffuse peut se concentrer sur le cadrage. Une autre, même depuis les tribunes, peut ouvrir un simple lien et mettre à jour le score en temps réel.
score_item_app: Aucune application à installer
score_item_signup: Aucune inscription
score_item_password: Aucun mot de passe
score_item_link: Il suffit d'ouvrir le lien partagé
score_card_aria: Accès à la régie du score
score_card_title: Régie via un lien
score_card_body: Celui qui met à jour sets et points ouvre un lien partagé. Aucun compte, aucune invitation.
access_link: Accès par lien
distribute_title: Un match, plusieurs façons de le suivre
distribute_lead: Depuis le smartphone en salle, le direct arrive aux spectateurs sur Match Live TV et, selon l'offre, aussi sur YouTube.
distribute_aria: Où va le direct
distribute_phone: Smartphone
distribute_mltv: Match Live TV
distribute_viewers: Spectateurs
distribute_youtube: YouTube
youtube_badge: Premium Full
youtube_title: Le direct reste celui de votre club
youtube_body: Avec Premium Full, vous pouvez aussi diffuser sur la chaîne YouTube officielle du club. Le club conserve ainsi son public, ses abonnés, ses vues, l'archive des matchs et la visibilité des sponsors.
youtube_mock_channel: Chaîne officielle du club
youtube_mock_subs: Abonnés et vues du club
replay_title: Le match se termine. Le souvenir reste.
replay_lead: Sur les offres qui incluent le replay, les matchs restent enregistrés sur les serveurs Match Live TV et peuvent être revus ensuite. La durée de conservation dépend de l'offre.
replay_item_server: Conservation sur les serveurs Match Live TV
replay_item_plan: Durée d'archive selon l'offre
replay_item_download: Téléchargement de la vidéo sur le téléphone si besoin
replay_card_title: Archive des matchs
replay_card_body: Revoyez les matchs après le coup de sifflet final et téléchargez-les quand vous en avez besoin.
replay_mock_1_title: Rossi vs Neri
replay_mock_1_meta: "12/05 · 01:25:34"
replay_mock_2_title: Rossi vs Blu
replay_mock_2_meta: "05/05 · 01:18:22"
replay_mock_3_title: Rossi vs Bianchi
replay_mock_3_meta: "28/04 · 01:07:15"
more_title: Cela fait aussi partie du produit
more_stable_title: Direct depuis le smartphone
more_stable_body: Posez le téléphone et lancez la diffusion. Même avec un 4G faible, le direct reste aussi stable que possible, sans câbles ni mixeur.
more_link_title: Lien pour familles et supporters
more_link_body: "Partagez un lien : parents, grands-parents et amis regardent depuis le navigateur du téléphone ou de l'ordinateur, sans installer d'application."
more_yt_mltv_title: Aussi sur YouTube Match Live TV
more_yt_mltv_body: Vous pouvez aussi rendre le direct visible sur la chaîne YouTube Match Live TV, en plus du lien sur le site.
cta_title: Prêts pour le prochain match ?
cta_body: Essayez Match Live TV gratuitement. Aucun matériel dédié nécessaire.
cta_primary: Commencer gratuitement
cta_secondary: Comparer les offres
pricing: pricing:
meta_title: "Tarifs streaming des matchs jeunes — Match Live TV" meta_title: "Tarifs streaming des matchs jeunes — Match Live TV"
meta_description: "Offres Free, Premium Light et Premium Full pour le direct et l'archive des matchs. Abonnement annuel pour les clubs : plus de staff, plus de matchs en parallèle, replay et YouTube." meta_description: "Offres Free, Premium Light et Premium Full pour le direct et l'archive des matchs. Abonnement annuel pour les clubs : plus de staff, plus de matchs en parallèle, replay et YouTube."
@@ -35,8 +83,8 @@ fr:
club_info_html: "Club : <strong>%{name}</strong> · %{billing_link}" club_info_html: "Club : <strong>%{name}</strong> · %{billing_link}"
manage_subscription: Gérer l'abonnement manage_subscription: Gérer l'abonnement
team_title: Offres pour votre équipe team_title: Offres pour votre équipe
team_lead: "Abonnement annuel pour les clubs : les responsables de diffusion démarrent le direct depuis l'application ; le score se gère avec un lien à partager." team_lead: "Abonnement annuel pour les clubs : les comptes diffuseurs démarrent le direct depuis l'application ; le score se gère avec un lien à partager."
table_staff: Responsables de diffusion / an table_staff: Comptes diffuseurs
table_matches: Matchs simultanés table_matches: Matchs simultanés
table_live_mltv: Direct sur Match Live TV table_live_mltv: Direct sur Match Live TV
table_youtube: YouTube table_youtube: YouTube
@@ -44,8 +92,7 @@ fr:
table_download: Téléchargement téléphone table_download: Téléchargement téléphone
table_price: Prix table_price: Prix
table_price_free: "€0" table_price_free: "€0"
table_price_light_html: "€40/an ou €5/mois" table_price_note: "tarif %{list} — %{monthly}"
table_price_full_html: "€200/an ou €20/mois"
table_yes: Oui table_yes: Oui
table_no: Non table_no: Non
table_unlimited: Illimité table_unlimited: Illimité
@@ -55,8 +102,16 @@ fr:
contact_button: Contactez-nous contact_button: Contactez-nous
plans: plans:
price_alt_html: "ou <strong>%{price}</strong>" price_alt_html: "ou <strong>%{price}</strong>"
staff_html: "Responsables de diffusion : <strong>%{count}</strong> / an par équipe" price_equiv: "soit %{price}"
staff_unlimited: illimité launch_price: Prix de lancement
recommended_badge: Conseillé pour les clubs
cta_name:
premium_light: Light
premium_full: Full
staff_html: "Comptes diffuseurs : maximum <strong>%{count}</strong> par abonnement"
staff_unlimited_html: "Comptes diffuseurs : <strong>illimités</strong> par abonnement"
staff_unlimited: illimités
staff_note_full: "Pas de mot de passe partagé : chaque personne utilise son propre accès, révocable à tout moment."
matches_html: "Matchs : <strong>%{count}</strong> simultanés (tout le club)" matches_html: "Matchs : <strong>%{count}</strong> simultanés (tout le club)"
matches_unlimited: illimités matches_unlimited: illimités
replay_html: "Replay : %{value}" replay_html: "Replay : %{value}"
+87 -32
View File
@@ -2,32 +2,80 @@ it:
pages: pages:
features: features:
meta_title: "Funzionalità streaming partite giovanili — Match Live TV" meta_title: "Funzionalità streaming partite giovanili — Match Live TV"
meta_description: "App per diretta live da telefono: filma la partita, aggiorna il punteggio, condividi il link con genitori e nonni. Archivio gare e piani per società sportive giovanili." meta_description: "Trasmetti le partite della società dallo smartphone con account personali, punteggio via link, diretta su Match Live TV e YouTube, replay e archivio. Senza attrezzatura complicata."
title: Funzionalità hero_title: "Le dirette della tua società. Senza password condivise, senza attrezzatura complicata."
lead: "Tutto ciò che serve per far seguire le partite a chi non può essere in palestra: semplice per coach e volontari, comodo per genitori, nonni e parenti." hero_lead: "Invita allenatori, dirigenti e genitori a trasmettere con il proprio account. Basta uno smartphone: Match Live TV gestisce accessi, punteggio, diretta, replay e pubblicazione."
staff_section_title: Per la società e lo staff hero_alt: Smartphone su treppiede per la diretta e secondo telefono per il punteggio in palestra
staff_invite_title: Inviti con la propria email flow_aria: Flusso della trasmissione
staff_invite_body: "Il presidente o l'owner invita chi filma e chi aggiorna il punteggio: ognuno accede con la sua email, senza condividere password." flow_phone: Uno smartphone
staff_access_title: Gestione accessi flow_account: Un account personale
staff_access_body: Puoi togliere l'accesso a un volontario o annullare un invito ancora in sospeso dalla dashboard della squadra. flow_live: Sei in diretta
staff_plans_title: Piani su misura how_title: Dalla palestra agli spettatori in pochi minuti
staff_plans_body: Con Free, Premium Light o Premium Full aumenti quante persone possono aiutare in diretta e quante partite possono andare in parallelo. how_1_title: Crea la partita
live_section_title: In campo e in diretta how_1_body: La società prepara la partita indicando squadra, avversario, data e ora.
live_phone_title: Filma dal telefono how_2_title: Apri l'app
live_phone_body: "Appoggi il telefono e avvii la diretta: anche con il 4G debole la trasmissione resta stabile il più possibile, senza impazzire con cavi o mixer." how_2_body: L'incaricato accede dall'app Match Live TV con il proprio account personale.
live_score_title: Punteggio da un secondo telefono how_3_title: Inizia la diretta
live_score_body: "Un altro volontario aggiorna set e punti da un secondo cellulare: basta inquadrare il codice che compare sullo schermo di chi filma." how_3_body: Basta lo smartphone. Nessuna attrezzatura dedicata o configurazione complicata.
live_link_title: Link per famiglie e tifosi how_4_title: Gestisci il punteggio
live_link_body: "Condividi un link: genitori, nonni e amici guardano dal browser del telefono o del computer, senza installare app." how_4_body: Un'altra persona può aggiornare risultato e punteggio tramite un semplice link, senza installare app e senza creare un account.
live_archive_title: Archivio partite how_5_title: Replay
live_archive_body: "Con Premium Light o Full le gare restano salvate sul server (30 o 90 giorni) e puoi scaricarle sul telefono quando serve." how_5_body: Al termine della diretta la partita può essere conservata e resa disponibile come replay, in base al piano scelto.
live_youtube_mltv_title: Anche su YouTube (Match Live TV) accounts_title: Ognuno con il proprio account. La password della società resta alla società.
live_youtube_mltv_body: Con Premium Light la diretta è visibile anche sul canale YouTube Match Live TV, oltre al link sul sito. accounts_lead: Non serve più condividere username e password dell'account principale. Dirigenti, allenatori e genitori incaricati ricevono un accesso personale e revocabile.
live_youtube_club_title: Canale YouTube della società accounts_invite_title: Invita
live_youtube_club_body: Con Premium Full puoi mandare la stessa diretta anche sul canale YouTube ufficiale del club. accounts_invite_body: La società invita allenatori, dirigenti o genitori incaricati della trasmissione.
live_website_title: Sul sito della società accounts_transmit_title: Trasmetti
live_website_body: Integrazione per incorporare il player sul sito del club — in arrivo con Premium Full. accounts_transmit_body: Ognuno accede all'app con le proprie credenziali personali.
cta_signup: Registra la squadra accounts_revoke_title: Revoca
accounts_revoke_body: Quando una persona non collabora più con la squadra o con la società, il suo accesso può essere revocato senza cambiare la password agli altri.
accounts_highlight: Niente password condivise. Ogni incaricato ha il proprio accesso personale.
score_title: Chi filma, filma. Chi tiene il punteggio, tiene il punteggio.
score_lead: La persona che trasmette può concentrarsi sull'inquadratura. Un'altra persona, anche dagli spalti, può aprire un semplice link e aggiornare il punteggio in tempo reale.
score_item_app: Nessuna app da installare
score_item_signup: Nessuna registrazione
score_item_password: Nessuna password
score_item_link: Basta aprire il link condiviso
score_card_aria: Accesso alla regia
score_card_title: Regia con un link
score_card_body: Chi aggiorna set e punti apre un link condiviso. Nessun account e nessun invito.
access_link: Accesso con link
distribute_title: Una partita, più modi di seguirla
distribute_lead: Dallo smartphone in palestra la diretta arriva agli spettatori su Match Live TV e, quando previsto dal piano, anche su YouTube.
distribute_aria: Dove arriva la diretta
distribute_phone: Smartphone
distribute_mltv: Match Live TV
distribute_viewers: Spettatori
distribute_youtube: YouTube
youtube_badge: Premium Full
youtube_title: La diretta resta della tua società
youtube_body: Con Premium Full puoi trasmettere anche sul canale YouTube ufficiale del club. La società mantiene così il proprio pubblico, gli iscritti, le visualizzazioni, l'archivio delle partite e la visibilità degli sponsor.
youtube_mock_channel: Canale ufficiale società
youtube_mock_subs: Iscritti e visualizzazioni del club
replay_title: La partita finisce. Il ricordo resta.
replay_lead: Nei piani che prevedono il replay, le gare restano salvate sui server Match Live TV e possono essere riviste in seguito. La durata della conservazione dipende dal piano.
replay_item_server: Conservazione sui server Match Live TV
replay_item_plan: Durata dell'archivio in base al piano
replay_item_download: Download del video sul telefono quando serve
replay_card_title: Archivio partite
replay_card_body: Rivedi le gare dopo il fischio finale e scaricale quando ti servono.
replay_mock_1_title: Rossi vs Neri
replay_mock_1_meta: "12/05 · 01:25:34"
replay_mock_2_title: Rossi vs Blu
replay_mock_2_meta: "05/05 · 01:18:22"
replay_mock_3_title: Rossi vs Bianchi
replay_mock_3_meta: "28/04 · 01:07:15"
more_title: Anche questo fa parte del prodotto
more_stable_title: Diretta dallo smartphone
more_stable_body: Appoggi il telefono e avvii la trasmissione. Anche con il 4G debole la diretta resta stabile il più possibile, senza cavi o mixer.
more_link_title: Link per famiglie e tifosi
more_link_body: "Condividi un link: genitori, nonni e amici guardano dal browser del telefono o del computer, senza installare app."
more_yt_mltv_title: Anche su YouTube Match Live TV
more_yt_mltv_body: Puoi rendere la diretta visibile anche sul canale YouTube Match Live TV, oltre al link sul sito.
cta_title: Pronti per la prossima partita?
cta_body: Prova Match Live TV gratuitamente. Non serve attrezzatura dedicata.
cta_primary: Inizia gratis
cta_secondary: Confronta i piani
pricing: pricing:
meta_title: "Prezzi streaming partite giovanili — Match Live TV" meta_title: "Prezzi streaming partite giovanili — Match Live TV"
meta_description: "Piani Free, Premium Light e Premium Full per dirette live e archivio partite. Abbonamento annuale per società: più staff, più partite in parallelo, replay e YouTube." meta_description: "Piani Free, Premium Light e Premium Full per dirette live e archivio partite. Abbonamento annuale per società: più staff, più partite in parallelo, replay e YouTube."
@@ -35,8 +83,8 @@ it:
club_info_html: "Società: <strong>%{name}</strong> · %{billing_link}" club_info_html: "Società: <strong>%{name}</strong> · %{billing_link}"
manage_subscription: Gestisci abbonamento manage_subscription: Gestisci abbonamento
team_title: Piani per la tua squadra team_title: Piani per la tua squadra
team_lead: "Abbonamento annuale per società: i responsabili trasmissione avviano la diretta dall'app; il punteggio si gestisce con un link condivisibile." team_lead: "Abbonamento annuale per società: gli account trasmettitori avviano la diretta dall'app; il punteggio si gestisce con un link condivisibile."
table_staff: Responsabili trasmissione / anno table_staff: Account trasmettitori
table_matches: Partite in contemporanea table_matches: Partite in contemporanea
table_live_mltv: Live su Match Live TV table_live_mltv: Live su Match Live TV
table_youtube: YouTube table_youtube: YouTube
@@ -44,8 +92,7 @@ it:
table_download: Download telefono table_download: Download telefono
table_price: Prezzo table_price: Prezzo
table_price_free: "€0" table_price_free: "€0"
table_price_light_html: "€40/anno o €5/mese" table_price_note: "listino %{list} — %{monthly}"
table_price_full_html: "€200/anno o €20/mese"
table_yes: "Sì" table_yes: "Sì"
table_no: "No" table_no: "No"
table_unlimited: Illimitato table_unlimited: Illimitato
@@ -55,8 +102,16 @@ it:
contact_button: Contattaci contact_button: Contattaci
plans: plans:
price_alt_html: "oppure <strong>%{price}</strong>" price_alt_html: "oppure <strong>%{price}</strong>"
staff_html: "Responsabili trasmissione: <strong>%{count}</strong> / anno per squadra" price_equiv: "equivale a %{price}"
staff_unlimited: illimitato launch_price: Prezzo lancio
recommended_badge: Consigliato per le società
cta_name:
premium_light: Light
premium_full: Full
staff_html: "Account trasmettitori: massimo <strong>%{count}</strong> per abbonamento"
staff_unlimited_html: "Account trasmettitori: <strong>illimitati</strong> per abbonamento"
staff_unlimited: illimitati
staff_note_full: "Niente password condivise: ogni incaricato utilizza il proprio accesso personale, revocabile in qualsiasi momento."
matches_html: "Partite: <strong>%{count}</strong> in contemporanea (tutta la società)" matches_html: "Partite: <strong>%{count}</strong> in contemporanea (tutta la società)"
matches_unlimited: illimitate matches_unlimited: illimitate
replay_html: "Replay: %{value}" replay_html: "Replay: %{value}"
+10 -2
View File
@@ -43,6 +43,13 @@ de:
tagline: "Streamen Sie live vom Smartphone. Großeltern, entfernte Verwandte, Freunde — schauen im Browser, ohne Installation. Und wenn Sie es verpasst haben: es ist im Archiv." tagline: "Streamen Sie live vom Smartphone. Großeltern, entfernte Verwandte, Freunde — schauen im Browser, ohne Installation. Und wenn Sie es verpasst haben: es ist im Archiv."
cta_signup: Team registrieren cta_signup: Team registrieren
cta_live: Live-Spiele ansehen cta_live: Live-Spiele ansehen
stores_label: Live gehen vom Smartphone
store_app_store_kicker: Laden im
store_app_store_name: App Store
store_app_store_aria: Match Live TV im App Store laden
store_play_kicker: Jetzt bei
store_play_name: Google Play
store_play_aria: Match Live TV bei Google Play laden
features_aria: Wichtige Vorteile features_aria: Wichtige Vorteile
feature_stable: Stabile Streams feature_stable: Stabile Streams
feature_archive: Sicheres Archiv feature_archive: Sicheres Archiv
@@ -103,7 +110,7 @@ de:
password_reset: password_reset:
meta_title: "Neues Passwort — Match Live TV" meta_title: "Neues Passwort — Match Live TV"
title: Neues Passwort wählen title: Neues Passwort wählen
new_password_label: "Neues Passwort (min. 8 Zeichen)" new_password_label: "Neues Passwort (mind. 8, mindestens 3 Zeichenarten)"
submit: Passwort speichern submit: Passwort speichern
back_to_login: Zurück zur Anmeldung back_to_login: Zurück zur Anmeldung
invitation: invitation:
@@ -128,11 +135,12 @@ de:
name_label: Name name_label: Name
role_label: "Rolle: %{role}" role_label: "Rolle: %{role}"
current_password_label: Aktuelles Passwort current_password_label: Aktuelles Passwort
new_password_label: "Neues Passwort (mind. 8 Zeichen)" new_password_label: "Neues Passwort (mind. 8, mindestens 3 Zeichenarten)"
save_profile: Profil speichern save_profile: Profil speichern
save_password: Passwort aktualisieren save_password: Passwort aktualisieren
common: common:
privacy: Datenschutz privacy: Datenschutz
support: Support
cookies: Cookies cookies: Cookies
terms: AGB terms: AGB
pricing: Preise pricing: Preise
+10 -2
View File
@@ -43,6 +43,13 @@ en:
tagline: Stream live from your smartphone. Grandparents, distant relatives, friends — they watch in the browser, with nothing to install. And if you missed it, it's in the archive. tagline: Stream live from your smartphone. Grandparents, distant relatives, friends — they watch in the browser, with nothing to install. And if you missed it, it's in the archive.
cta_signup: Register your team cta_signup: Register your team
cta_live: Watch live matches cta_live: Watch live matches
stores_label: To go live from your smartphone
store_app_store_kicker: Download on the
store_app_store_name: App Store
store_app_store_aria: Download Match Live TV on the App Store
store_play_kicker: Get it on
store_play_name: Google Play
store_play_aria: Get Match Live TV on Google Play
features_aria: Key benefits features_aria: Key benefits
feature_stable: Reliable streams feature_stable: Reliable streams
feature_archive: Secure archive feature_archive: Secure archive
@@ -103,7 +110,7 @@ en:
password_reset: password_reset:
meta_title: "New password — Match Live TV" meta_title: "New password — Match Live TV"
title: Choose a new password title: Choose a new password
new_password_label: "New password (min. 8 characters)" new_password_label: "New password (min. 8, at least 3 character types)"
submit: Save password submit: Save password
back_to_login: Back to login back_to_login: Back to login
invitation: invitation:
@@ -128,11 +135,12 @@ en:
name_label: Name name_label: Name
role_label: "Role: %{role}" role_label: "Role: %{role}"
current_password_label: Current password current_password_label: Current password
new_password_label: "New password (min. 8 characters)" new_password_label: "New password (min. 8, at least 3 character types)"
save_profile: Save profile save_profile: Save profile
save_password: Update password save_password: Update password
common: common:
privacy: Privacy privacy: Privacy
support: Support
cookies: Cookies cookies: Cookies
terms: Terms terms: Terms
pricing: Pricing pricing: Pricing
+10 -2
View File
@@ -43,6 +43,13 @@ es:
tagline: Emite en directo desde el smartphone. Abuelos, familiares lejanos, amigos — miran en el navegador, sin instalar nada. Y si te lo perdiste, está en el archivo. tagline: Emite en directo desde el smartphone. Abuelos, familiares lejanos, amigos — miran en el navegador, sin instalar nada. Y si te lo perdiste, está en el archivo.
cta_signup: Registra tu equipo cta_signup: Registra tu equipo
cta_live: Ver directos cta_live: Ver directos
stores_label: Para emitir en directo desde el smartphone
store_app_store_kicker: Consíguelo en
store_app_store_name: App Store
store_app_store_aria: Descarga Match Live TV en App Store
store_play_kicker: Disponible en
store_play_name: Google Play
store_play_aria: Descarga Match Live TV en Google Play
features_aria: Ventajas principales features_aria: Ventajas principales
feature_stable: Directos estables feature_stable: Directos estables
feature_archive: Archivo seguro feature_archive: Archivo seguro
@@ -103,7 +110,7 @@ es:
password_reset: password_reset:
meta_title: "Nueva contraseña — Match Live TV" meta_title: "Nueva contraseña — Match Live TV"
title: Elige una nueva contraseña title: Elige una nueva contraseña
new_password_label: "Nueva contraseña (mín. 8 caracteres)" new_password_label: "Nueva contraseña (mín. 8, al menos 3 tipos de caracteres)"
submit: Guardar contraseña submit: Guardar contraseña
back_to_login: Volver al inicio de sesión back_to_login: Volver al inicio de sesión
invitation: invitation:
@@ -128,11 +135,12 @@ es:
name_label: Nombre name_label: Nombre
role_label: "Rol: %{role}" role_label: "Rol: %{role}"
current_password_label: Contraseña actual current_password_label: Contraseña actual
new_password_label: "Nueva contraseña (mín. 8 caracteres)" new_password_label: "Nueva contraseña (mín. 8, al menos 3 tipos de caracteres)"
save_profile: Guardar perfil save_profile: Guardar perfil
save_password: Actualizar contraseña save_password: Actualizar contraseña
common: common:
privacy: Privacidad privacy: Privacidad
support: Soporte
cookies: Cookies cookies: Cookies
terms: Términos terms: Términos
pricing: Precios pricing: Precios
+10 -2
View File
@@ -43,6 +43,13 @@ fr:
tagline: Diffusez en direct depuis votre smartphone. Les grands-parents, la famille éloignée, les amis — regardent dans le navigateur, sans rien installer. Et si vous l'avez manqué, c'est dans l'archive. tagline: Diffusez en direct depuis votre smartphone. Les grands-parents, la famille éloignée, les amis — regardent dans le navigateur, sans rien installer. Et si vous l'avez manqué, c'est dans l'archive.
cta_signup: Inscrire votre équipe cta_signup: Inscrire votre équipe
cta_live: Voir les directs cta_live: Voir les directs
stores_label: Pour passer en direct depuis le smartphone
store_app_store_kicker: Télécharger dans
store_app_store_name: App Store
store_app_store_aria: Télécharger Match Live TV dans l'App Store
store_play_kicker: Disponible sur
store_play_name: Google Play
store_play_aria: Télécharger Match Live TV sur Google Play
features_aria: Avantages principaux features_aria: Avantages principaux
feature_stable: Directs stables feature_stable: Directs stables
feature_archive: Archive sécurisée feature_archive: Archive sécurisée
@@ -103,7 +110,7 @@ fr:
password_reset: password_reset:
meta_title: "Nouveau mot de passe — Match Live TV" meta_title: "Nouveau mot de passe — Match Live TV"
title: Choisissez un nouveau mot de passe title: Choisissez un nouveau mot de passe
new_password_label: "Nouveau mot de passe (8 caractères min.)" new_password_label: "Nouveau mot de passe (min. 8, au moins 3 types de caractères)"
submit: Enregistrer le mot de passe submit: Enregistrer le mot de passe
back_to_login: Retour à la connexion back_to_login: Retour à la connexion
invitation: invitation:
@@ -128,11 +135,12 @@ fr:
name_label: Nom name_label: Nom
role_label: "Rôle : %{role}" role_label: "Rôle : %{role}"
current_password_label: Mot de passe actuel current_password_label: Mot de passe actuel
new_password_label: "Nouveau mot de passe (min. 8 caractères)" new_password_label: "Nouveau mot de passe (min. 8, au moins 3 types de caractères)"
save_profile: Enregistrer le profil save_profile: Enregistrer le profil
save_password: Mettre à jour le mot de passe save_password: Mettre à jour le mot de passe
common: common:
privacy: Confidentialité privacy: Confidentialité
support: Support
cookies: Cookies cookies: Cookies
terms: Conditions terms: Conditions
pricing: Tarifs pricing: Tarifs
+10 -2
View File
@@ -43,6 +43,13 @@ it:
tagline: Trasmetti in diretta dallo smartphone. I nonni, i parenti lontani, gli amici — guardano dal browser, senza installare nulla. E se te la sei persa, la ritrovi nell'archivio. tagline: Trasmetti in diretta dallo smartphone. I nonni, i parenti lontani, gli amici — guardano dal browser, senza installare nulla. E se te la sei persa, la ritrovi nell'archivio.
cta_signup: Registra la tua squadra cta_signup: Registra la tua squadra
cta_live: Guarda le dirette cta_live: Guarda le dirette
stores_label: Per andare in diretta dallo smartphone
store_app_store_kicker: Scarica su
store_app_store_name: App Store
store_app_store_aria: Scarica Match Live TV su App Store
store_play_kicker: Disponibile su
store_play_name: Google Play
store_play_aria: Scarica Match Live TV su Google Play
features_aria: Vantaggi principali features_aria: Vantaggi principali
feature_stable: Dirette stabili feature_stable: Dirette stabili
feature_archive: Archivio sicuro feature_archive: Archivio sicuro
@@ -103,7 +110,7 @@ it:
password_reset: password_reset:
meta_title: "Nuova password — Match Live TV" meta_title: "Nuova password — Match Live TV"
title: Scegli una nuova password title: Scegli una nuova password
new_password_label: "Nuova password (min. 8 caratteri)" new_password_label: "Nuova password (min. 8, almeno 3 tipi di caratteri)"
submit: Salva password submit: Salva password
back_to_login: Torna al login back_to_login: Torna al login
invitation: invitation:
@@ -128,11 +135,12 @@ it:
name_label: Nome name_label: Nome
role_label: "Ruolo: %{role}" role_label: "Ruolo: %{role}"
current_password_label: Password attuale current_password_label: Password attuale
new_password_label: "Nuova password (min. 8 caratteri)" new_password_label: "Nuova password (min. 8, almeno 3 tipi di caratteri)"
save_profile: Salva profilo save_profile: Salva profilo
save_password: Aggiorna password save_password: Aggiorna password
common: common:
privacy: Privacy privacy: Privacy
support: Supporto
cookies: Cookie cookies: Cookie
terms: Termini terms: Termini
pricing: Prezzi pricing: Prezzi
+2
View File
@@ -154,6 +154,7 @@ Rails.application.routes.draw do
get "prezzi", to: "pages#pricing", as: :prezzi get "prezzi", to: "pages#pricing", as: :prezzi
get "pricing", to: redirect("/prezzi") get "pricing", to: redirect("/prezzi")
get "privacy", to: "pages#privacy", as: :privacy get "privacy", to: "pages#privacy", as: :privacy
get "support", to: "pages#support", as: :support
get "cookie", to: "pages#cookies", as: :cookies get "cookie", to: "pages#cookies", as: :cookies
get "cookies", to: redirect("/cookie") get "cookies", to: redirect("/cookie")
get "termini", to: "pages#terms", as: :termini get "termini", to: "pages#terms", as: :termini
@@ -169,6 +170,7 @@ Rails.application.routes.draw do
patch "password/reset", to: "password_resets#update" patch "password/reset", to: "password_resets#update"
get "account", to: "accounts#show", as: :account get "account", to: "accounts#show", as: :account
patch "account", to: "accounts#update" patch "account", to: "accounts#update"
get "account/password", to: redirect("/account"), as: nil
patch "account/password", to: "accounts#update_password", as: :account_password patch "account/password", to: "accounts#update_password", as: :account_password
get "clubs/new", to: "clubs#new", as: :new_club get "clubs/new", to: "clubs#new", as: :new_club
post "clubs", to: "clubs#create" post "clubs", to: "clubs#create"
+4 -4
View File
@@ -1,18 +1,18 @@
load Rails.root.join("db/seeds/plans.rb") load Rails.root.join("db/seeds/plans.rb")
AdminAccount.find_or_create_by!(username: "admin") do |a| AdminAccount.find_or_create_by!(username: "admin") do |a|
a.password = "admin" a.password = "AdminPass123"
end end
coach = User.find_or_create_by!(email: "coach@matchlivetv.test") do |u| coach = User.find_or_create_by!(email: "coach@matchlivetv.test") do |u|
u.name = "Coach Demo" u.name = "Coach Demo"
u.password = "password123" u.password = "Password123"
u.role = "coach" u.role = "coach"
end end
admin = User.find_or_create_by!(email: "admin@matchlivetv.test") do |u| admin = User.find_or_create_by!(email: "admin@matchlivetv.test") do |u|
u.name = "Admin" u.name = "Admin"
u.password = "password123" u.password = "Password123"
u.role = "admin" u.role = "admin"
end end
@@ -41,5 +41,5 @@ match = team.matches.find_or_create_by!(opponent_name: "ASD Eagles Pavia") do |m
m.phase = "Semifinale" m.phase = "Semifinale"
end end
puts "Seed OK: coach@matchlivetv.test / password123" puts "Seed OK: coach@matchlivetv.test / Password123"
puts "Club: #{club.name}, Team: #{team.name}, Match: #{match.opponent_name}" puts "Club: #{club.name}, Team: #{team.name}, Match: #{match.opponent_name}"
+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" role="img" aria-hidden="true">
<path fill="#ffffff" d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.81-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z"/>
</svg>

After

Width:  |  Height:  |  Size: 569 B

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 283" role="img" aria-hidden="true">
<path fill="#EA4335" d="M119.55 134.92 1.06 259.06c2.7 9.56 9.66 17.33 18.86 21.06 9.2 3.73 19.61 2.99 28.2-1.99l133.33-75.93-61.9-67.28z"/>
<path fill="#FBBC04" d="M239.37 113.81 181.71 80.79l-64.9 56.95 65.16 64.28 57.22-32.67c10.33-5.41 16.81-16.11 16.81-27.77s-6.48-22.36-16.81-27.77z"/>
<path fill="#4285F4" d="M1.06 23.49C.34 26.13-.01 28.87 0 31.61v219.33c.01 2.74.36 5.47 1.06 8.12L123.61 138.1 1.06 23.49z"/>
<path fill="#34A853" d="m120.44 141.27 61.28-60.48L48.56 4.5C43.55 1.57 37.86.02 32.05 0 17.64-.03 4.98 9.53 1.06 23.4l119.38 117.87z"/>
</svg>

After

Width:  |  Height:  |  Size: 663 B

File diff suppressed because it is too large Load Diff
@@ -32,7 +32,7 @@ RSpec.describe Public::BillingHelper, type: :helper do
current_interval: "yearly" current_interval: "yearly"
) )
expect(action[:kind]).to eq(:current) expect(action[:kind]).to eq(:current)
expect(action[:label]).to include("40/anno") expect(action[:label]).to include("59/anno")
end end
it "propone cambio intervallo sullo stesso piano" do it "propone cambio intervallo sullo stesso piano" do
@@ -0,0 +1,45 @@
require "rails_helper"
RSpec.describe PasswordComplexity do
describe ".violation" do
it "accepts a password with 3 character classes" do
expect(described_class.violation("NewPass123")).to be_nil
end
it "accepts symbols instead of one letter class" do
expect(described_class.violation("newpass1!")).to be_nil
end
it "rejects passwords that are too short" do
expect(described_class.violation("Ab1!")).to eq(:too_short)
end
it "rejects passwords with fewer than 3 classes" do
expect(described_class.violation("password123")).to eq(:too_weak)
expect(described_class.violation("PASSWORD123")).to eq(:too_weak)
expect(described_class.violation("Password")).to eq(:too_weak)
end
end
describe ".same_as_current?" do
let!(:user) { User.create!(email: "same@example.com", name: "Same", password: "Password123", role: "coach") }
it "detects when the new password matches the current one" do
expect(described_class.same_as_current?(user, "Password123")).to eq(true)
expect(described_class.same_as_current?(user, "OtherPass123")).to eq(false)
end
end
describe "User validation" do
it "blocks weak passwords on create" do
user = User.new(email: "weak@example.com", name: "Weak", password: "password123", role: "coach")
expect(user).not_to be_valid
expect(user.errors[:password]).to be_present
end
it "allows strong passwords on create" do
user = User.new(email: "strong@example.com", name: "Strong", password: "NewPass123", role: "coach")
expect(user).to be_valid
end
end
end
+55 -9
View File
@@ -1,9 +1,9 @@
require "rails_helper" require "rails_helper"
RSpec.describe "Account API", type: :request do RSpec.describe "Account API", type: :request do
let!(:user) { User.create!(email: "account@example.com", name: "Account User", password: "password123", role: "coach") } let!(:user) { User.create!(email: "account@example.com", name: "Account User", password: "Password123", role: "coach") }
let(:auth_headers) do let(:auth_headers) do
post "/api/v1/auth/login", params: { email: user.email, password: "password123" } post "/api/v1/auth/login", params: { email: user.email, password: "Password123" }
token = JSON.parse(response.body).fetch("access_token") token = JSON.parse(response.body).fetch("access_token")
{ "Authorization" => "Bearer #{token}" } { "Authorization" => "Bearer #{token}" }
end end
@@ -40,25 +40,63 @@ RSpec.describe "Account API", type: :request do
it "changes the password with the current password" do it "changes the password with the current password" do
patch "/api/v1/account/password", patch "/api/v1/account/password",
params: { params: {
current_password: "password123", current_password: "Password123",
password: "newpass123", password: "NewPass123",
password_confirmation: "newpass123" password_confirmation: "NewPass123"
}, },
headers: auth_headers headers: auth_headers
expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)
expect(user.reload.authenticate("newpass123")).to be_truthy expect(user.reload.authenticate("NewPass123")).to be_truthy
end end
it "rejects an incorrect current password" do it "rejects an incorrect current password" do
patch "/api/v1/account/password", patch "/api/v1/account/password",
params: { params: {
current_password: "wrong", current_password: "wrong",
password: "newpass123", password: "NewPass123",
password_confirmation: "newpass123" password_confirmation: "NewPass123"
}, },
headers: auth_headers headers: auth_headers
expect(response).to have_http_status(:unprocessable_entity) expect(response).to have_http_status(:unprocessable_entity)
expect(user.reload.authenticate("password123")).to be_truthy expect(user.reload.authenticate("Password123")).to be_truthy
end
it "rejects a password that fails complexity rules" do
patch "/api/v1/account/password",
params: {
current_password: "Password123",
password: "newpass123",
password_confirmation: "newpass123"
},
headers: auth_headers.merge("Accept-Language" => "en")
expect(response).to have_http_status(:unprocessable_entity)
expect(JSON.parse(response.body)["error"]).to match(/3 of/i)
expect(user.reload.authenticate("Password123")).to be_truthy
end
it "rejects reusing the current password" do
patch "/api/v1/account/password",
params: {
current_password: "Password123",
password: "Password123",
password_confirmation: "Password123"
},
headers: auth_headers.merge("Accept-Language" => "en")
expect(response).to have_http_status(:unprocessable_entity)
expect(JSON.parse(response.body)["error"]).to match(/different/i)
expect(user.reload.authenticate("Password123")).to be_truthy
end
it "localizes password errors from Accept-Language" do
patch "/api/v1/account/password",
params: {
current_password: "Password123",
password: "Password123",
password_confirmation: "Password123"
},
headers: auth_headers.merge("Accept-Language" => "it")
expect(response).to have_http_status(:unprocessable_entity)
expect(JSON.parse(response.body)["error"]).to eq("La nuova password deve essere diversa da quella attuale")
end end
end end
@@ -77,5 +115,13 @@ RSpec.describe "Account API", type: :request do
}.not_to change { ActionMailer::Base.deliveries.size } }.not_to change { ActionMailer::Base.deliveries.size }
expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)
end end
it "localizes the response message from Accept-Language" do
post "/api/v1/auth/password/forgot",
params: { email: user.email },
headers: { "Accept-Language" => "fr" }
expect(response).to have_http_status(:ok)
expect(JSON.parse(response.body)["message"]).to include("réinitialiser")
end
end end
end end
+2 -2
View File
@@ -1,10 +1,10 @@
require "rails_helper" require "rails_helper"
RSpec.describe "Auth API", type: :request do RSpec.describe "Auth API", type: :request do
let!(:user) { User.create!(email: "test@example.com", name: "Test", password: "password123", role: "coach") } let!(:user) { User.create!(email: "test@example.com", name: "Test", password: "Password123", role: "coach") }
it "logs in with valid credentials" do it "logs in with valid credentials" do
post "/api/v1/auth/login", params: { email: user.email, password: "password123" } post "/api/v1/auth/login", params: { email: user.email, password: "Password123" }
expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)
expect(JSON.parse(response.body)).to have_key("access_token") expect(JSON.parse(response.body)).to have_key("access_token")
end end
@@ -72,7 +72,7 @@ RSpec.describe "Public club billing", type: :request do
get public_club_billing_path(club) get public_club_billing_path(club)
expect(response.body).to include("Completa dati di fatturazione") expect(response.body).to include("Completa dati di fatturazione")
expect(response.body).not_to include("Attiva Premium Light — €40/anno") expect(response.body).not_to include("Attiva Light — €59/anno")
end end
it "dopo profilo completo reindirizza al checkout" do it "dopo profilo completo reindirizza al checkout" do
@@ -111,8 +111,8 @@ RSpec.describe "Public club billing", type: :request do
login! login!
get public_club_billing_path(club) get public_club_billing_path(club)
expect(response.body).to include("Premium Light — €40/anno") expect(response.body).to include("Attiva Light — €59/anno")
expect(response.body).to include("Premium Light — €5/mese") expect(response.body).to include("Attiva Light — €7,90/mese")
allow(Billing::Stripe::CheckoutSession).to receive(:new).and_return( allow(Billing::Stripe::CheckoutSession).to receive(:new).and_return(
instance_double(Billing::Stripe::CheckoutSession, url: "https://checkout.stripe.com/test") instance_double(Billing::Stripe::CheckoutSession, url: "https://checkout.stripe.com/test")
@@ -0,0 +1,26 @@
require "rails_helper"
RSpec.describe "Public features page", type: :request do
it "racconta il prodotto con sezioni narrative e CTA" do
get public_features_path
expect(response).to have_http_status(:ok)
expect(response.body).to include("Le dirette della tua società")
expect(response.body).to include("Senza password condivise")
expect(response.body).to include("Uno smartphone")
expect(response.body).to include("Un account personale")
expect(response.body).to include("Sei in diretta")
expect(response.body).to include("Dalla palestra agli spettatori")
expect(response.body).to include("Ognuno con il proprio account")
expect(response.body).to include("Niente password condivise")
expect(response.body).to include("Chi filma, filma")
expect(response.body).to include("Accesso con link")
expect(response.body).to include("La diretta resta della tua società")
expect(response.body).to include("La partita finisce. Il ricordo resta.")
expect(response.body).to include("Inizia gratis")
expect(response.body).to include("Confronta i piani")
expect(response.body).to include(public_signup_path)
expect(response.body).to include(public_prezzi_path)
expect(response.body).not_to include("translation missing")
end
end
@@ -0,0 +1,41 @@
require "rails_helper"
RSpec.describe "Public home page", type: :request do
it "mostra i link agli store nell'hero e nel footer" do
get root_path
expect(response).to have_http_status(:ok)
expect(response.body).to include(MatchLiveTv.app_store_url)
expect(response.body).to include(MatchLiveTv.play_store_url)
expect(response.body).to include(I18n.t("home.stores_label", locale: :it))
expect(response.body).to include(I18n.t("home.store_app_store_name", locale: :it))
expect(response.body).to include(I18n.t("home.store_play_name", locale: :it))
expect(response.body.scan(MatchLiveTv.app_store_url).size).to eq(3)
expect(response.body.scan(MatchLiveTv.play_store_url).size).to eq(3)
end
{
"it" => "Per andare in diretta dallo smartphone",
"en" => "To go live from your smartphone",
"fr" => "Pour passer en direct depuis le smartphone",
"de" => "Live gehen vom Smartphone",
"es" => "Para emitir en directo desde el smartphone"
}.each do |locale, label|
it "traduce i badge store in #{locale} senza translation missing" do
cookies[:mltv_locale] = locale
get root_path
expect(response).to have_http_status(:ok)
expect(response.body).to include(label)
expect(response.body).not_to include("translation missing")
end
end
it "non mostra i badge store nel chrome App Store Review" do
get public_support_path
expect(response).to have_http_status(:ok)
expect(response.body).not_to include(MatchLiveTv.app_store_url)
expect(response.body).not_to include(MatchLiveTv.play_store_url)
end
end
@@ -0,0 +1,32 @@
require "rails_helper"
RSpec.describe "Public pricing page", type: :request do
before { load Rails.root.join("db/seeds/plans.rb") }
it "mostra prezzi lancio, listino barrato e Full in evidenza" do
get public_prezzi_path
expect(response).to have_http_status(:ok)
expect(response.body).to include("€59/anno")
expect(response.body).to include("€79/anno")
expect(response.body).to include("€199/anno")
expect(response.body).to include("€249/anno")
expect(response.body).to include("€7,90/mese")
expect(response.body).to include("€24,90/mese")
expect(response.body).to include("equivale a €4,92/mese")
expect(response.body).to include("equivale a €16,58/mese")
expect(response.body).to include("Prezzo lancio")
expect(response.body).to include("Consigliato per le società")
expect(response.body).to include("Attiva Light — €59/anno")
expect(response.body).to include("Attiva Light — €7,90/mese")
expect(response.body).to include("Attiva Full — €199/anno")
expect(response.body).to include("Attiva Full — €24,90/mese")
expect(response.body).to include("Account trasmettitori: massimo")
expect(response.body).to include("per abbonamento")
expect(response.body).to include("Account trasmettitori")
expect(response.body).to include("Niente password condivise")
expect(response.body).to include("plan-card--featured")
expect(response.body).not_to include("a vita")
expect(response.body).not_to include("Più popolare")
end
end
@@ -0,0 +1,57 @@
require "rails_helper"
RSpec.describe "Public support page", type: :request do
it "è pubblica, indicizzabile e mostra l'email di supporto configurata" do
get public_support_path
expect(response).to have_http_status(:ok)
expect(response.body).to include(MatchLiveTv.support_email)
expect(response.body).to include("mailto:#{MatchLiveTv.support_email}")
expect(response.body).to include(I18n.t("legal.support.h1", locale: :it))
expect(response.body).to include('rel="canonical"')
expect(response.body).to include(public_support_path)
expect(response.body).to include(public_privacy_path)
end
it "non espone CTA o link commerciali (App Store Review)" do
get public_support_path
body = response.body
expect(body).not_to include(public_prezzi_path)
expect(body).not_to include(public_signup_path)
expect(body).not_to include('href="/prezzi"')
expect(body).not_to include('href="/signup"')
expect(body).not_to include(I18n.t("nav.signup", locale: :it))
expect(body).not_to include(I18n.t("nav.pricing", locale: :it))
expect(body).not_to include(I18n.t("common.pricing", locale: :it))
expect(body).not_to match(/\bPremium Light\b/i)
expect(body).not_to match(/\bPremium Full\b/i)
expect(body).not_to match(/\bpiano Free\b/i)
expect(body).not_to match(/\bAbbonati\b/i)
expect(body).not_to match(/\bAcquista\b/i)
end
{
"it" => "Supporto Match Live TV",
"en" => "Match Live TV Support",
"fr" => "Support Match Live TV",
"de" => "Match Live TV Support",
"es" => "Soporte Match Live TV"
}.each do |locale, heading|
it "renderizza correttamente in #{locale} senza translation missing" do
cookies[:mltv_locale] = locale
get public_support_path
expect(response).to have_http_status(:ok)
expect(response.body).to include(heading)
expect(response.body).not_to include("translation missing")
end
end
it "include /support nella sitemap" do
get "/sitemap.xml"
expect(response).to have_http_status(:ok)
expect(response.body).to include("#{MatchLiveTv.app_public_url.chomp('/')}/support")
end
end
@@ -10,7 +10,7 @@ RSpec.describe Billing::PaymentDescription do
lines: double(data: []) lines: double(data: [])
) )
expect(described_class.for_stripe_invoice(invoice)).to eq("Premium Light — €5/mese") expect(described_class.for_stripe_invoice(invoice)).to eq("Premium Light — €7,90/mese")
end end
it "ignora descrizioni di test nel pagamento salvato" do it "ignora descrizioni di test nel pagamento salvato" do
@@ -23,6 +23,6 @@ RSpec.describe Billing::PaymentDescription do
club: Club.create!(name: "C", sport: "volleyball", primary_color: "#e53935", secondary_color: "#ffffff") club: Club.create!(name: "C", sport: "volleyball", primary_color: "#e53935", secondary_color: "#ffffff")
) )
expect(payment.display_description).to eq("Premium Light — €40/anno") expect(payment.display_description).to eq("Premium Light — €59/anno")
end end
end end
@@ -23,6 +23,15 @@ RSpec.describe Billing::Stripe::PriceCatalog do
expect { described_class.normalize_interval("weekly") }.to raise_error(ArgumentError) expect { described_class.normalize_interval("weekly") }.to raise_error(ArgumentError)
end end
it "etichetta il prezzo lancio annuale e il mensile" do
expect(described_class.label(plan_slug: "premium_light", interval: "yearly")).to eq("€59/anno")
expect(described_class.label(plan_slug: "premium_light", interval: "monthly")).to eq("€7,90/mese")
expect(described_class.list_label(plan_slug: "premium_light", interval: "yearly")).to eq("€79/anno")
expect(described_class.equivalent_monthly_label(plan_slug: "premium_light")).to eq("€4,92/mese")
expect(described_class.label(plan_slug: "premium_full", interval: "yearly")).to eq("€199/anno")
expect(described_class.label(plan_slug: "premium_full", interval: "monthly")).to eq("€24,90/mese")
end
it "elenca intervalli configurati" do it "elenca intervalli configurati" do
expect(described_class.available_intervals(plan_slug: "premium_light")).to eq(%w[monthly yearly]) expect(described_class.available_intervals(plan_slug: "premium_light")).to eq(%w[monthly yearly])
end end
@@ -38,6 +38,6 @@ RSpec.describe Billing::Stripe::RecordPayment do
expect(payment.club).to eq(club) expect(payment.club).to eq(club)
expect(payment.amount_cents).to eq(4000) expect(payment.amount_cents).to eq(4000)
expect(payment.stripe_invoice_id).to eq("in_test_1") expect(payment.stripe_invoice_id).to eq("in_test_1")
expect(payment.description).to eq("Premium Light — €40/anno") expect(payment.description).to eq("Premium Light — €59/anno")
end end
end end
+2 -2
View File
@@ -38,10 +38,10 @@ Solo se la società vuole il **proprio** canale invece di Match Live TV:
Nella home app (**Partite**): Nella home app (**Partite**):
- **Partita programmata** — scegli dal calendario una gara già inserita (sito o app)
- **Nuova partita** — programma data/ora oppure **Avvia subito** senza orario - **Nuova partita** — programma data/ora oppure **Avvia subito** senza orario
- Oppure tocca una gara già in calendario (sito o app)
Poi tocca la card o conferma dal foglio: si apre il wizard (Partita → Trasmissione → …). Poi conferma dal foglio o tocca la card: si apre il wizard (Partita → Trasmissione → …).
## 4. Avvia diretta ## 4. Avvia diretta
+7 -7
View File
@@ -1,12 +1,12 @@
# Gap Android → iOS: allineamento app nativa # Gap Android → iOS: allineamento app nativa
Documento operativo per continuare su **Mac** lo sviluppo iOS e rilasciare unapp **allineata ad Android `2.0.5-native`**. Documento operativo per continuare su **Mac** lo sviluppo iOS e rilasciare unapp **allineata ad Android**.
**Aggiornato:** 24 luglio 2026 **Aggiornato:** 8 agosto 2026
**Riferimento Android (produzione / telefono / Play):** `2.0.5-native` (`versionCode` **26**), API `https://www.matchlivetv.it` **Riferimento Android (produzione / telefono / Play):** `2.0.10-native` (`versionCode` **31**), API `https://www.matchlivetv.it`
**Stato iOS attuale:** marketing `2.0.5`, build `26` — i18n **allineato** (Login, hub, sheet/dialog, wizard, broadcast) **Stato iOS attuale:** marketing `2.0.10`, build `31` — i18n **allineato** (Login, hub, sheet/dialog, wizard, broadcast, account/forgot)
Obiettivo iOS: **stessa copertura lingua** di Android (Login, hub, sheet/dialog, wizard, broadcast) + bump versione, **senza** i bug di sessione/crash già risolti su Android 2.0.5. Obiettivo iOS: **stessa copertura lingua** di Android (Login, hub, sheet/dialog, wizard, broadcast, account) + bump versione, **senza** i bug di sessione/crash già risolti su Android.
--- ---
@@ -59,7 +59,7 @@ Queste sono regressioni/bug già visti su Android; **non ripeterli** su iOS.
| Broadcast + score dialog | Sì | `broadcast.*` / `score.*` | **Fatto** | | Broadcast + score dialog | Sì | `broadcast.*` / `score.*` | **Fatto** |
| Catalogo stringhe | `values*` (~230) | `AppLanguage.swift` L10n (~225, no FGS) | **Fatto** | | Catalogo stringhe | `values*` (~230) | `AppLanguage.swift` L10n (~225, no FGS) | **Fatto** |
| Logout UI | Icona | Icona SF Symbol | **Fatto** | | Logout UI | Icona | Icona SF Symbol | **Fatto** |
| Versione | `2.0.5-native` / **26** | `2.0.5` / **26** | **Fatto** | | Versione | `2.0.10-native` / **31** | `2.0.10` / **31** | **Fatto** |
| RTMP ingest | `RtmpIngestUrl.kt` | `MediaUrl.swift` | OK | | RTMP ingest | `RtmpIngestUrl.kt` | `MediaUrl.swift` | OK |
### Residui fuori scope Android (opzionali) ### Residui fuori scope Android (opzionali)
@@ -141,7 +141,7 @@ xcodebuild -project MatchLiveTv.xcodeproj -scheme MatchLiveTv \
Nessuna modifica server richiesta. Nessuna modifica server richiesta.
Versione store target: **`2.0.5` / build `26`** (parità con Android `2.0.5-native` / versionCode `26`). Versione store target: **`2.0.10` / build `31`** (parità con Android `2.0.10-native` / versionCode `31`).
--- ---
+88
View File
@@ -0,0 +1,88 @@
# iOS — allineamento password policy + errori API localizzati
Documento operativo per **Cursor su Mac**: allineare lapp iOS a backend/Android dopo il ramo `feature/password-policy` (merge su `main`).
**Aggiornato:** 2026-08-08
**Android di riferimento:** `2.0.10-native` (`versionCode` **31**)
**iOS (allineato):** marketing `2.0.10` / build `31`
**API produzione:** `https://www.matchlivetv.it`
---
## Contesto (già in produzione backend dopo questo rilascio)
### Policy password (server)
Definita in `backend/app/models/concerns/password_complexity.rb`:
- minimo **8** caratteri, massimo **72** byte (bcrypt)
- almeno **3 classi su 4**: minuscole, maiuscole, numeri, simboli
- in **cambio password** e **reset**: la nuova password **non** può coincidere con quella attuale
Validazione ActiveModel su `User` / `AdminAccount`. Endpoint che la applicano:
| Endpoint | Note |
|----------|------|
| `PATCH /api/v1/account/password` | App native + stessi codici errore |
| `PATCH /account/password` (web) | Flash I18n |
| reset password pubblico | Stesse regole |
| registrazione (`auth/register`) | Validazione modello |
### Errori API localizzati
`ApplicationController#set_api_locale` legge, in ordine:
1. header `X-Locale`
2. `Accept-Language`
3. `I18n.default_locale` (fallback; **retrocompatibile** se lapp vecchia non manda nulla)
Risposta invariata: `{ "error": "<messaggio già tradotto>" }` (o `{ "message": "..." }` su alcuni successi).
Chiavi rilevanti (`backend/config/locales/app.{it,en,fr,de,es}.yml`):
- `flash.accounts.password_too_short`
- `flash.accounts.password_too_long`
- `flash.accounts.password_too_weak`
- `flash.accounts.password_same_as_current`
- `flash.accounts.password_current_incorrect`
- `flash.accounts.password_mismatch`
- `flash.accounts.password_updated` / `name_required`
- analoghi in `flash.password_resets.*` e `flash.sessions.*`
---
## Stato iOS vs Android
| Area | Android 2.0.10 | iOS | Stato |
|------|----------------|-----|-------|
| Header `Accept-Language` su tutte le request API | OkHttp interceptor | `MatchLiveAPI.request` + `multipartPatch` | **OK** |
| Hint UI nuova password | `account_new_password` | `account.new.password` | **OK** |
| Schermata Account | AccountScreen | AccountScreen | **OK** |
| Forgot password | ForgotPasswordScreen | ForgotPasswordScreen | **OK** |
| Parsing errori API | body `error` | `APIError.friendlyHttpMessage` | **OK** |
| Client-side pre-check complessità | No | No | N/A (come Android) |
| Versione store | `2.0.10-native` / **31** | `2.0.10` / **31** | **OK** |
| Signup in-app | Non principale | Nessuna UI register | N/A (signup web) |
### File iOS
```
native/ios/MatchLiveTv/Data/API/MatchLiveAPI.swift
native/ios/MatchLiveTv/Core/AppLanguage.swift
native/ios/MatchLiveTv/UI/Account/AccountScreen.swift
native/ios/MatchLiveTv/UI/Login/ForgotPasswordScreen.swift
native/ios/MatchLiveTv/Resources/Info.plist
native/ios/generate_xcodeproj.py
```
---
## Criterio “fatto”
- [x] `Accept-Language` su login, me, account, change password, forgot (`MatchLiveAPI`)
- [x] Hint campo nuova password parla di “min. 8 / 3 tipi”
- [x] Catalogo stringhe account/forgot in it/en/fr/de/es
- [x] Versione bumpata a `2.0.10` / `31`
- [ ] QA manuale errori password in lingua UI (weak / same / mismatch / success)
Quando rilasci su TestFlight/App Store, conferma la build `31`.
+3 -2
View File
@@ -4,13 +4,14 @@
| | Free | Premium Light | Premium Full | | | Free | Premium Light | Premium Full |
|---|------|---------------|----------------| |---|------|---------------|----------------|
| Responsabili trasmissione / anno | 1 | 5 | Illimitato | | Account trasmettitori | 1 | 5 | Illimitati |
| Partite in contemporanea | 1 | 3 | 10 | | Partite in contemporanea | 1 | 3 | 10 |
| Live Match Live TV | Sì | Sì | Sì | | Live Match Live TV | Sì | Sì | Sì |
| YouTube | No | Match Live TV | Canale società | | YouTube | No | Match Live TV | Canale società |
| Replay server | No | 30 giorni | 90 giorni | | Replay server | No | 30 giorni | 90 giorni |
| Download telefono | No | Sì | Sì | | Download telefono | No | Sì | Sì |
| Prezzo | €0 | €40/anno o €5/mese | €200/anno o €20/mese | | Prezzo lancio | €0 | €59/anno o €7,90/mese | €199/anno o €24,90/mese |
| Listino | — | €79/anno | €249/anno |
| Embed sito società | No | No | In arrivo | | Embed sito società | No | No | In arrivo |
Il punteggio in diretta si gestisce tramite **link regia** condivisibile (WhatsApp, email, SMS…): non serve un account per chi fa il tabellone. Il punteggio in diretta si gestisce tramite **link regia** condivisibile (WhatsApp, email, SMS…): non serve un account per chi fa il tabellone.
+4 -4
View File
@@ -5,12 +5,12 @@
### Prodotti e prezzi ### Prodotti e prezzi
Crea **2 prodotti** in Stripe, ciascuno con **2 prezzi ricorrenti**: Crea **2 prodotti** in Stripe, ciascuno con **2 prezzi ricorrenti**:
| Piano app | Prodotto Stripe (Dashboard) | Mensile | Annuale | Variabile `.env` | | Piano app | Prodotto Stripe (Dashboard) | Mensile | Annuale (lancio) | Variabile `.env` |
|-----------|------------------------------|---------|---------|------------------| |-----------|------------------------------|---------|---------|------------------|
| Premium Light | **Match Live TV** | €5 | €40 | `STRIPE_PREMIUM_LIGHT_MONTHLY_PRICE_ID` / `..._YEARLY_...` | | Premium Light | **Match Live TV** | €7,90 | €59 | `STRIPE_PREMIUM_LIGHT_MONTHLY_PRICE_ID` / `..._YEARLY_...` |
| Premium Full | **Match Live TV PRO** | €20 | €200 | `STRIPE_PREMIUM_FULL_MONTHLY_PRICE_ID` / `..._YEARLY_...` | | Premium Full | **Match Live TV PRO** | €24,90 | €199 | `STRIPE_PREMIUM_FULL_MONTHLY_PRICE_ID` / `..._YEARLY_...` |
Gli importi in Stripe Dashboard devono coincidere con il sito (€5/€40 e €20/€200). Gli importi in Stripe Dashboard devono coincidere con i **prezzi lancio** addebitati sul sito (€7,90/€59 e €24,90/€199). I listini barrati (€79 e €249) sono solo display, non vanno creati come Price Stripe.
In Stripe apri ogni prezzo e copia l**ID prezzo** (`price_...`), non lID prodotto. In Stripe apri ogni prezzo e copia l**ID prezzo** (`price_...`), non lID prodotto.
+4
View File
@@ -15,6 +15,8 @@ YOUTUBE_MOCK_STREAM_KEY=mock-stream-key
PRIVACY_CONTROLLER_NAME=Emiliano Frascaro PRIVACY_CONTROLLER_NAME=Emiliano Frascaro
PRIVACY_CONTROLLER_ADDRESS=Via Guido De Ruggiero, 89 - 20142 - Milano (MI) PRIVACY_CONTROLLER_ADDRESS=Via Guido De Ruggiero, 89 - 20142 - Milano (MI)
PRIVACY_CONTACT_EMAIL=privacy@matchlivetv.it PRIVACY_CONTACT_EMAIL=privacy@matchlivetv.it
# Opzionale: email dedicata di supporto (default = info@matchlivetv.it)
# SUPPORT_CONTACT_EMAIL=info@matchlivetv.it
MAILER_FROM=Match Live TV <noreply@matchlivetv.it> MAILER_FROM=Match Live TV <noreply@matchlivetv.it>
PASSWORD_RESET_EXPIRY_HOURS=2 PASSWORD_RESET_EXPIRY_HOURS=2
@@ -24,6 +26,8 @@ PASSWORD_RESET_EXPIRY_HOURS=2
# Stripe (test: sk_test_... / price_... da Dashboard modalità Test) # Stripe (test: sk_test_... / price_... da Dashboard modalità Test)
STRIPE_SECRET_KEY= STRIPE_SECRET_KEY=
STRIPE_WEBHOOK_SECRET= STRIPE_WEBHOOK_SECRET=
# Price ID dei prezzi LANCIO da addebitare:
# Light yearly €59 · Light monthly €7,90 · Full yearly €199 · Full monthly €24,90
# Un price per prodotto e intervallo (Dashboard → Prodotto → Prezzi) # Un price per prodotto e intervallo (Dashboard → Prodotto → Prezzi)
STRIPE_PREMIUM_LIGHT_MONTHLY_PRICE_ID= STRIPE_PREMIUM_LIGHT_MONTHLY_PRICE_ID=
STRIPE_PREMIUM_LIGHT_YEARLY_PRICE_ID= STRIPE_PREMIUM_LIGHT_YEARLY_PRICE_ID=
+2
View File
@@ -39,6 +39,8 @@ RAILS_LOG_LEVEL=info
PRIVACY_CONTROLLER_NAME=Emiliano Frascaro PRIVACY_CONTROLLER_NAME=Emiliano Frascaro
PRIVACY_CONTROLLER_ADDRESS=Via Guido De Ruggiero, 89 - 20142 - Milano (MI) PRIVACY_CONTROLLER_ADDRESS=Via Guido De Ruggiero, 89 - 20142 - Milano (MI)
PRIVACY_CONTACT_EMAIL=privacy@matchlivetv.it PRIVACY_CONTACT_EMAIL=privacy@matchlivetv.it
# Opzionale: email di supporto App Store (default = info@matchlivetv.it)
# SUPPORT_CONTACT_EMAIL=info@matchlivetv.it
PRIVACY_CONTROLLER_VAT= PRIVACY_CONTROLLER_VAT=
# Email transazionali (reset password, inviti) # Email transazionali (reset password, inviti)
+1
View File
@@ -73,6 +73,7 @@ services:
HLS_PUBLIC_URL: ${HLS_PUBLIC_URL:-http://localhost:8888} HLS_PUBLIC_URL: ${HLS_PUBLIC_URL:-http://localhost:8888}
APP_PUBLIC_URL: ${APP_PUBLIC_URL:-http://localhost:3000} APP_PUBLIC_URL: ${APP_PUBLIC_URL:-http://localhost:3000}
PRIVACY_CONTACT_EMAIL: ${PRIVACY_CONTACT_EMAIL:-privacy@matchlivetv.it} PRIVACY_CONTACT_EMAIL: ${PRIVACY_CONTACT_EMAIL:-privacy@matchlivetv.it}
SUPPORT_CONTACT_EMAIL: ${SUPPORT_CONTACT_EMAIL:-}
PRIVACY_CONTROLLER_NAME: ${PRIVACY_CONTROLLER_NAME:-Emiliano Frascaro} PRIVACY_CONTROLLER_NAME: ${PRIVACY_CONTROLLER_NAME:-Emiliano Frascaro}
PRIVACY_CONTROLLER_ADDRESS: ${PRIVACY_CONTROLLER_ADDRESS:-Via Guido De Ruggiero, 89 - 20142 - Milano (MI)} PRIVACY_CONTROLLER_ADDRESS: ${PRIVACY_CONTROLLER_ADDRESS:-Via Guido De Ruggiero, 89 - 20142 - Milano (MI)}
PRIVACY_CONTROLLER_VAT: ${PRIVACY_CONTROLLER_VAT:-} PRIVACY_CONTROLLER_VAT: ${PRIVACY_CONTROLLER_VAT:-}
+1
View File
@@ -71,6 +71,7 @@ services:
PRIVACY_CONTROLLER_NAME: ${PRIVACY_CONTROLLER_NAME:-Emiliano Frascaro} PRIVACY_CONTROLLER_NAME: ${PRIVACY_CONTROLLER_NAME:-Emiliano Frascaro}
PRIVACY_CONTROLLER_ADDRESS: ${PRIVACY_CONTROLLER_ADDRESS:-Via Guido De Ruggiero, 89 - 20142 - Milano (MI)} PRIVACY_CONTROLLER_ADDRESS: ${PRIVACY_CONTROLLER_ADDRESS:-Via Guido De Ruggiero, 89 - 20142 - Milano (MI)}
PRIVACY_CONTACT_EMAIL: ${PRIVACY_CONTACT_EMAIL:-privacy@matchlivetv.it} PRIVACY_CONTACT_EMAIL: ${PRIVACY_CONTACT_EMAIL:-privacy@matchlivetv.it}
SUPPORT_CONTACT_EMAIL: ${SUPPORT_CONTACT_EMAIL:-}
MAILER_FROM: ${MAILER_FROM:-Match Live TV <noreply@matchlivetv.it>} MAILER_FROM: ${MAILER_FROM:-Match Live TV <noreply@matchlivetv.it>}
APP_PUBLIC_URL: ${APP_PUBLIC_URL:-http://localhost:3000} APP_PUBLIC_URL: ${APP_PUBLIC_URL:-http://localhost:3000}
STRIPE_SECRET_KEY: ${STRIPE_SECRET_KEY:-} STRIPE_SECRET_KEY: ${STRIPE_SECRET_KEY:-}
+1
View File
@@ -5,6 +5,7 @@
*.iml *.iml
/captures/ /captures/
/app/build/ /app/build/
/dist/
.DS_Store .DS_Store
keystore.properties keystore.properties
keystore/ keystore/
@@ -2,6 +2,7 @@ package com.matchlivetv.match_live_tv.data
import android.content.Context import android.content.Context
import com.matchlivetv.match_live_tv.core.AppConfig import com.matchlivetv.match_live_tv.core.AppConfig
import com.matchlivetv.match_live_tv.core.AppLocale
import com.matchlivetv.match_live_tv.core.TokenStore import com.matchlivetv.match_live_tv.core.TokenStore
import com.matchlivetv.match_live_tv.data.api.MatchLiveApi import com.matchlivetv.match_live_tv.data.api.MatchLiveApi
import com.matchlivetv.match_live_tv.data.cable.SessionCableService import com.matchlivetv.match_live_tv.data.cable.SessionCableService
@@ -37,10 +38,20 @@ class AppContainer(context: Context) {
accessToken?.let { header("Authorization", "Bearer $it") } accessToken?.let { header("Authorization", "Bearer $it") }
header("Accept", "application/json") header("Accept", "application/json")
header("Content-Type", "application/json") header("Content-Type", "application/json")
header("Accept-Language", apiLanguageTag())
}.build() }.build()
chain.proceed(request) chain.proceed(request)
} }
private fun apiLanguageTag(): String {
val tag = AppLocale.currentTag(appContext)
return if (tag.isBlank()) {
java.util.Locale.getDefault().language.ifBlank { "it" }
} else {
tag
}
}
private val okHttp = OkHttpClient.Builder() private val okHttp = OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS) .connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS) .readTimeout(60, TimeUnit.SECONDS)
@@ -57,7 +57,6 @@ import com.matchlivetv.match_live_tv.ui.components.LanguagePickerDialog
import com.matchlivetv.match_live_tv.ui.components.MatchLiveWordmark import com.matchlivetv.match_live_tv.ui.components.MatchLiveWordmark
import com.matchlivetv.match_live_tv.ui.components.MatchPrimaryButton import com.matchlivetv.match_live_tv.ui.components.MatchPrimaryButton
import com.matchlivetv.match_live_tv.ui.components.MatchScreenScaffold import com.matchlivetv.match_live_tv.ui.components.MatchScreenScaffold
import com.matchlivetv.match_live_tv.ui.components.MatchSecondaryButton
import com.matchlivetv.match_live_tv.ui.theme.MatchColors import com.matchlivetv.match_live_tv.ui.theme.MatchColors
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -237,18 +236,11 @@ fun MatchesScreen(
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
) )
Spacer(Modifier.height(16.dp)) Spacer(Modifier.height(16.dp))
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
MatchSecondaryButton(
label = stringResource(R.string.matches_schedule).uppercase(),
onClick = { showScheduleSheet = true },
modifier = Modifier.weight(1f),
)
MatchPrimaryButton( MatchPrimaryButton(
label = stringResource(R.string.matches_new).uppercase(), label = stringResource(R.string.matches_new).uppercase(),
onClick = { showNewMatchSheet = true }, onClick = { showNewMatchSheet = true },
modifier = Modifier.weight(1f), modifier = Modifier.fillMaxWidth(),
) )
}
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
activeTeam?.let { team -> activeTeam?.let { team ->
TeamPickerBar( TeamPickerBar(
@@ -21,13 +21,12 @@
<string name="login_error_generic">Anmeldung fehlgeschlagen</string> <string name="login_error_generic">Anmeldung fehlgeschlagen</string>
<string name="matches_title">Spiele</string> <string name="matches_title">Spiele</string>
<string name="matches_hello">Hallo, %1$s</string> <string name="matches_hello">Hallo, %1$s</string>
<string name="matches_subtitle">Nimm einen laufenden Livestream wieder auf oder starte ein geplantes Spiel.</string> <string name="matches_subtitle">Nimm einen laufenden Livestream wieder auf oder starte ein neues Spiel.</string>
<string name="matches_schedule">Geplantes Spiel</string>
<string name="matches_new">Neues Spiel</string> <string name="matches_new">Neues Spiel</string>
<string name="matches_empty_title">Keine Spiele im Kalender</string> <string name="matches_empty_title">Keine Spiele im Kalender</string>
<string name="matches_scheduled_title">Geplante Spiele</string> <string name="matches_scheduled_title">Geplante Spiele</string>
<string name="matches_ready_title">Bereit zum Start</string> <string name="matches_ready_title">Bereit zum Start</string>
<string name="matches_empty_hint">Plane ein Spiel oder starte ein neues mit «Neues Spiel».</string> <string name="matches_empty_hint">Tippe auf «Neues Spiel», um im Voraus zu planen oder sofort zu starten.</string>
<string name="matches_active_team">Aktives Team: %1$s.</string> <string name="matches_active_team">Aktives Team: %1$s.</string>
<string name="matches_multi_team_hint">Du hast mehrere Teams: prüfe die oben ausgewählte.</string> <string name="matches_multi_team_hint">Du hast mehrere Teams: prüfe die oben ausgewählte.</string>
<string name="matches_no_other">Keine weiteren Spiele im Kalender.</string> <string name="matches_no_other">Keine weiteren Spiele im Kalender.</string>
@@ -252,7 +251,7 @@ Dies kann nicht rückgängig gemacht werden.</string>
<string name="account_name_label">Name</string> <string name="account_name_label">Name</string>
<string name="account_role">Rolle: %1$s</string> <string name="account_role">Rolle: %1$s</string>
<string name="account_current_password">Aktuelles Passwort</string> <string name="account_current_password">Aktuelles Passwort</string>
<string name="account_new_password">Neues Passwort (mind. 8 Zeichen)</string> <string name="account_new_password">Neues Passwort (mind. 8, mindestens 3 Zeichenarten)</string>
<string name="account_confirm_password">Passwort bestätigen</string> <string name="account_confirm_password">Passwort bestätigen</string>
<string name="account_save_profile">Profil speichern</string> <string name="account_save_profile">Profil speichern</string>
<string name="account_save_password">Passwort aktualisieren</string> <string name="account_save_password">Passwort aktualisieren</string>
@@ -21,13 +21,12 @@
<string name="login_error_generic">Login failed</string> <string name="login_error_generic">Login failed</string>
<string name="matches_title">Matches</string> <string name="matches_title">Matches</string>
<string name="matches_hello">Hi, %1$s</string> <string name="matches_hello">Hi, %1$s</string>
<string name="matches_subtitle">Resume a live stream or start a scheduled match.</string> <string name="matches_subtitle">Resume a live stream or start a new match.</string>
<string name="matches_schedule">Scheduled match</string>
<string name="matches_new">New match</string> <string name="matches_new">New match</string>
<string name="matches_empty_title">No matches on the calendar</string> <string name="matches_empty_title">No matches on the calendar</string>
<string name="matches_scheduled_title">Scheduled matches</string> <string name="matches_scheduled_title">Scheduled matches</string>
<string name="matches_ready_title">Ready to start</string> <string name="matches_ready_title">Ready to start</string>
<string name="matches_empty_hint">Schedule a match or start a new one with «New match».</string> <string name="matches_empty_hint">Tap «New match» to schedule ahead or start right away.</string>
<string name="matches_active_team">Active team: %1$s.</string> <string name="matches_active_team">Active team: %1$s.</string>
<string name="matches_multi_team_hint">You have multiple teams: check the one selected above.</string> <string name="matches_multi_team_hint">You have multiple teams: check the one selected above.</string>
<string name="matches_no_other">No other matches on the calendar.</string> <string name="matches_no_other">No other matches on the calendar.</string>
@@ -252,7 +251,7 @@ This cannot be undone.</string>
<string name="account_name_label">Name</string> <string name="account_name_label">Name</string>
<string name="account_role">Role: %1$s</string> <string name="account_role">Role: %1$s</string>
<string name="account_current_password">Current password</string> <string name="account_current_password">Current password</string>
<string name="account_new_password">New password (min. 8 characters)</string> <string name="account_new_password">New password (min. 8, at least 3 character types)</string>
<string name="account_confirm_password">Confirm password</string> <string name="account_confirm_password">Confirm password</string>
<string name="account_save_profile">Save profile</string> <string name="account_save_profile">Save profile</string>
<string name="account_save_password">Update password</string> <string name="account_save_password">Update password</string>
@@ -21,13 +21,12 @@
<string name="login_error_generic">Error de acceso</string> <string name="login_error_generic">Error de acceso</string>
<string name="matches_title">Partidos</string> <string name="matches_title">Partidos</string>
<string name="matches_hello">Hola, %1$s</string> <string name="matches_hello">Hola, %1$s</string>
<string name="matches_subtitle">Reanuda un directo en curso o inicia un partido programado.</string> <string name="matches_subtitle">Reanuda un directo en curso o inicia un nuevo partido.</string>
<string name="matches_schedule">Partido programado</string>
<string name="matches_new">Nuevo partido</string> <string name="matches_new">Nuevo partido</string>
<string name="matches_empty_title">Ningún partido en el calendario</string> <string name="matches_empty_title">Ningún partido en el calendario</string>
<string name="matches_scheduled_title">Partidos programados</string> <string name="matches_scheduled_title">Partidos programados</string>
<string name="matches_ready_title">Listos para empezar</string> <string name="matches_ready_title">Listos para empezar</string>
<string name="matches_empty_hint">Programa un partido o inicia uno nuevo con «Nuevo partido».</string> <string name="matches_empty_hint">Toca «Nuevo partido» para programar con antelación o empezar ya.</string>
<string name="matches_active_team">Equipo activo: %1$s.</string> <string name="matches_active_team">Equipo activo: %1$s.</string>
<string name="matches_multi_team_hint">Tienes varios equipos: comprueba el seleccionado arriba.</string> <string name="matches_multi_team_hint">Tienes varios equipos: comprueba el seleccionado arriba.</string>
<string name="matches_no_other">Ningún otro partido en el calendario.</string> <string name="matches_no_other">Ningún otro partido en el calendario.</string>
@@ -252,7 +251,7 @@ Esta acción no se puede deshacer.</string>
<string name="account_name_label">Nombre</string> <string name="account_name_label">Nombre</string>
<string name="account_role">Rol: %1$s</string> <string name="account_role">Rol: %1$s</string>
<string name="account_current_password">Contraseña actual</string> <string name="account_current_password">Contraseña actual</string>
<string name="account_new_password">Nueva contraseña (mín. 8 caracteres)</string> <string name="account_new_password">Nueva contraseña (mín. 8, al menos 3 tipos de caracteres)</string>
<string name="account_confirm_password">Confirmar contraseña</string> <string name="account_confirm_password">Confirmar contraseña</string>
<string name="account_save_profile">Guardar perfil</string> <string name="account_save_profile">Guardar perfil</string>
<string name="account_save_password">Actualizar contraseña</string> <string name="account_save_password">Actualizar contraseña</string>
@@ -21,13 +21,12 @@
<string name="login_error_generic">Échec de la connexion</string> <string name="login_error_generic">Échec de la connexion</string>
<string name="matches_title">Matchs</string> <string name="matches_title">Matchs</string>
<string name="matches_hello">Bonjour, %1$s</string> <string name="matches_hello">Bonjour, %1$s</string>
<string name="matches_subtitle">Reprenez un direct en cours ou démarrez un match programmé.</string> <string name="matches_subtitle">Reprenez un direct en cours ou démarrez un nouveau match.</string>
<string name="matches_schedule">Match programmé</string>
<string name="matches_new">Nouveau match</string> <string name="matches_new">Nouveau match</string>
<string name="matches_empty_title">Aucun match au calendrier</string> <string name="matches_empty_title">Aucun match au calendrier</string>
<string name="matches_scheduled_title">Matchs programmés</string> <string name="matches_scheduled_title">Matchs programmés</string>
<string name="matches_ready_title">Prêts à démarrer</string> <string name="matches_ready_title">Prêts à démarrer</string>
<string name="matches_empty_hint">Programmez un match ou démarrez-en un avec « Nouveau match ».</string> <string name="matches_empty_hint">Touchez « Nouveau match » pour planifier à l\'avance ou démarrer tout de suite.</string>
<string name="matches_active_team">Équipe active : %1$s.</string> <string name="matches_active_team">Équipe active : %1$s.</string>
<string name="matches_multi_team_hint">Vous avez plusieurs équipes : vérifiez celle sélectionnée ci-dessus.</string> <string name="matches_multi_team_hint">Vous avez plusieurs équipes : vérifiez celle sélectionnée ci-dessus.</string>
<string name="matches_no_other">Aucun autre match au calendrier.</string> <string name="matches_no_other">Aucun autre match au calendrier.</string>
@@ -252,7 +251,7 @@ Cette action est irréversible.</string>
<string name="account_name_label">Nom</string> <string name="account_name_label">Nom</string>
<string name="account_role">Rôle : %1$s</string> <string name="account_role">Rôle : %1$s</string>
<string name="account_current_password">Mot de passe actuel</string> <string name="account_current_password">Mot de passe actuel</string>
<string name="account_new_password">Nouveau mot de passe (min. 8 caractères)</string> <string name="account_new_password">Nouveau mot de passe (min. 8, au moins 3 types de caractères)</string>
<string name="account_confirm_password">Confirmer le mot de passe</string> <string name="account_confirm_password">Confirmer le mot de passe</string>
<string name="account_save_profile">Enregistrer le profil</string> <string name="account_save_profile">Enregistrer le profil</string>
<string name="account_save_password">Mettre à jour le mot de passe</string> <string name="account_save_password">Mettre à jour le mot de passe</string>
@@ -21,13 +21,12 @@
<string name="login_error_generic">Login fallito</string> <string name="login_error_generic">Login fallito</string>
<string name="matches_title">Partite</string> <string name="matches_title">Partite</string>
<string name="matches_hello">Ciao, %1$s</string> <string name="matches_hello">Ciao, %1$s</string>
<string name="matches_subtitle">Riprendi una diretta in corso o avvia una partita programmata.</string> <string name="matches_subtitle">Riprendi una diretta in corso o avvia una nuova partita.</string>
<string name="matches_schedule">Partita programmata</string>
<string name="matches_new">Nuova partita</string> <string name="matches_new">Nuova partita</string>
<string name="matches_empty_title">Nessuna partita in calendario</string> <string name="matches_empty_title">Nessuna partita in calendario</string>
<string name="matches_scheduled_title">Partite programmate</string> <string name="matches_scheduled_title">Partite programmate</string>
<string name="matches_ready_title">Pronte da avviare</string> <string name="matches_ready_title">Pronte da avviare</string>
<string name="matches_empty_hint">Programma una partita o avviane una nuova con «Nuova partita».</string> <string name="matches_empty_hint">Tocca «Nuova partita» per programmare in anticipo o avviare subito.</string>
<string name="matches_active_team">Squadra attiva: %1$s.</string> <string name="matches_active_team">Squadra attiva: %1$s.</string>
<string name="matches_multi_team_hint">Hai più squadre: verifica quella selezionata sopra.</string> <string name="matches_multi_team_hint">Hai più squadre: verifica quella selezionata sopra.</string>
<string name="matches_no_other">Nessuna altra partita in calendario.</string> <string name="matches_no_other">Nessuna altra partita in calendario.</string>
@@ -252,7 +251,7 @@ L\'operazione non si può annullare.</string>
<string name="account_name_label">Nome</string> <string name="account_name_label">Nome</string>
<string name="account_role">Ruolo: %1$s</string> <string name="account_role">Ruolo: %1$s</string>
<string name="account_current_password">Password attuale</string> <string name="account_current_password">Password attuale</string>
<string name="account_new_password">Nuova password (min. 8 caratteri)</string> <string name="account_new_password">Nuova password (min. 8, almeno 3 tipi di caratteri)</string>
<string name="account_confirm_password">Conferma password</string> <string name="account_confirm_password">Conferma password</string>
<string name="account_save_profile">Salva profilo</string> <string name="account_save_profile">Salva profilo</string>
<string name="account_save_password">Aggiorna password</string> <string name="account_save_password">Aggiorna password</string>
File diff suppressed because it is too large Load Diff
@@ -1,32 +1,102 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme LastUpgradeVersion="1600" version="1.7"> <Scheme
<BuildAction parallelizeBuildables="YES" buildImplicitDependencies="YES"> LastUpgradeVersion = "2660"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries> <BuildActionEntries>
<BuildActionEntry buildForTesting="YES" buildForRunning="YES" buildForProfiling="YES" buildForArchiving="YES" buildForAnalyzing="YES"> <BuildActionEntry
<BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="768EC47B451F4731A05BE1F9" BuildableName="MatchLiveTv.app" BlueprintName="MatchLiveTv" ReferencedContainer="container:MatchLiveTv.xcodeproj"/> buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "F54F6C97361C4E8A96AEDD11"
BuildableName = "MatchLiveTv.app"
BlueprintName = "MatchLiveTv"
ReferencedContainer = "container:MatchLiveTv.xcodeproj">
</BuildableReference>
</BuildActionEntry> </BuildActionEntry>
<BuildActionEntry buildForTesting="YES" buildForRunning="NO" buildForProfiling="NO" buildForArchiving="NO" buildForAnalyzing="NO"> <BuildActionEntry
<BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="F6079500E62048DB89AE3866" BuildableName="MatchLiveTvTests.xctest" BlueprintName="MatchLiveTvTests" ReferencedContainer="container:MatchLiveTv.xcodeproj"/> buildForTesting = "YES"
buildForRunning = "NO"
buildForProfiling = "NO"
buildForArchiving = "NO"
buildForAnalyzing = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "20C695DBFB4042879308B0F5"
BuildableName = "MatchLiveTvTests.xctest"
BlueprintName = "MatchLiveTvTests"
ReferencedContainer = "container:MatchLiveTv.xcodeproj">
</BuildableReference>
</BuildActionEntry> </BuildActionEntry>
</BuildActionEntries> </BuildActionEntries>
</BuildAction> </BuildAction>
<TestAction buildConfiguration="Debug" selectedDebuggerIdentifier="Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier="Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv="YES"> <TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables> <Testables>
<TestableReference skipped="NO"> <TestableReference
<BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="F6079500E62048DB89AE3866" BuildableName="MatchLiveTvTests.xctest" BlueprintName="MatchLiveTvTests" ReferencedContainer="container:MatchLiveTv.xcodeproj"/> skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "20C695DBFB4042879308B0F5"
BuildableName = "MatchLiveTvTests.xctest"
BlueprintName = "MatchLiveTvTests"
ReferencedContainer = "container:MatchLiveTv.xcodeproj">
</BuildableReference>
</TestableReference> </TestableReference>
</Testables> </Testables>
</TestAction> </TestAction>
<LaunchAction buildConfiguration="Debug" selectedDebuggerIdentifier="Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier="Xcode.DebuggerFoundation.Launcher.LLDB" launchStyle="0" useCustomWorkingDirectory="NO" ignoresPersistentStateOnLaunch="NO" debugDocumentVersioning="YES" debugServiceExtension="internal" allowLocationSimulation="YES"> <LaunchAction
<BuildableProductRunnable runnableDebuggingMode="0"> buildConfiguration = "Debug"
<BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="768EC47B451F4731A05BE1F9" BuildableName="MatchLiveTv.app" BlueprintName="MatchLiveTv" ReferencedContainer="container:MatchLiveTv.xcodeproj"/> selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "F54F6C97361C4E8A96AEDD11"
BuildableName = "MatchLiveTv.app"
BlueprintName = "MatchLiveTv"
ReferencedContainer = "container:MatchLiveTv.xcodeproj">
</BuildableReference>
</BuildableProductRunnable> </BuildableProductRunnable>
</LaunchAction> </LaunchAction>
<ProfileAction buildConfiguration="Release" shouldUseLaunchSchemeArgsEnv="YES" savedToolIdentifier="" useCustomWorkingDirectory="NO" debugDocumentVersioning="YES"> <ProfileAction
<BuildableProductRunnable runnableDebuggingMode="0"> buildConfiguration = "Release"
<BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="768EC47B451F4731A05BE1F9" BuildableName="MatchLiveTv.app" BlueprintName="MatchLiveTv" ReferencedContainer="container:MatchLiveTv.xcodeproj"/> shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "F54F6C97361C4E8A96AEDD11"
BuildableName = "MatchLiveTv.app"
BlueprintName = "MatchLiveTv"
ReferencedContainer = "container:MatchLiveTv.xcodeproj">
</BuildableReference>
</BuildableProductRunnable> </BuildableProductRunnable>
</ProfileAction> </ProfileAction>
<AnalyzeAction buildConfiguration="Debug"/> <AnalyzeAction
<ArchiveAction buildConfiguration="Release" revealArchiveInOrganizer="YES"/> buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme> </Scheme>
+16 -21
View File
@@ -81,7 +81,7 @@ enum L10n {
"account.confirm.password": "Conferma password", "account.confirm.password": "Conferma password",
"account.current.password": "Password attuale", "account.current.password": "Password attuale",
"account.name.label": "Nome", "account.name.label": "Nome",
"account.new.password": "Nuova password (min. 8 caratteri)", "account.new.password": "Nuova password (min. 8, almeno 3 tipi di caratteri)",
"account.password.heading": "Cambia password", "account.password.heading": "Cambia password",
"account.password.saved": "Password aggiornata", "account.password.saved": "Password aggiornata",
"account.profile.heading": "Profilo", "account.profile.heading": "Profilo",
@@ -194,7 +194,7 @@ enum L10n {
"match.status.start": "AVVIA", "match.status.start": "AVVIA",
"matches.active.team": "Squadra attiva: %1$@.", "matches.active.team": "Squadra attiva: %1$@.",
"matches.change": "Cambia", "matches.change": "Cambia",
"matches.empty.hint": "Programma una partita o avviane una nuova con «Nuova partita».", "matches.empty.hint": "Tocca «Nuova partita» per programmare in anticipo o avviare subito.",
"matches.empty.title": "Nessuna partita in calendario", "matches.empty.title": "Nessuna partita in calendario",
"matches.hello": "Ciao, %1$@", "matches.hello": "Ciao, %1$@",
"matches.load.error": "Errore caricamento", "matches.load.error": "Errore caricamento",
@@ -212,9 +212,8 @@ enum L10n {
"matches.no.team.title": "Nessuna squadra assegnata", "matches.no.team.title": "Nessuna squadra assegnata",
"matches.ready.title": "Pronte da avviare", "matches.ready.title": "Pronte da avviare",
"matches.retry": "Riprova", "matches.retry": "Riprova",
"matches.schedule": "Partita programmata",
"matches.scheduled.title": "Partite programmate", "matches.scheduled.title": "Partite programmate",
"matches.subtitle": "Riprendi una diretta in corso o avvia una partita programmata.", "matches.subtitle": "Riprendi una diretta in corso o avvia una nuova partita.",
"matches.tap.change.team": "Tocca per cambiare squadra", "matches.tap.change.team": "Tocca per cambiare squadra",
"matches.team.for.live": "Squadra per la diretta", "matches.team.for.live": "Squadra per la diretta",
"matches.title": "Partite", "matches.title": "Partite",
@@ -372,7 +371,7 @@ enum L10n {
"account.confirm.password": "Confirm password", "account.confirm.password": "Confirm password",
"account.current.password": "Current password", "account.current.password": "Current password",
"account.name.label": "Name", "account.name.label": "Name",
"account.new.password": "New password (min. 8 characters)", "account.new.password": "New password (min. 8, at least 3 character types)",
"account.password.heading": "Change password", "account.password.heading": "Change password",
"account.password.saved": "Password updated", "account.password.saved": "Password updated",
"account.profile.heading": "Profile", "account.profile.heading": "Profile",
@@ -381,7 +380,7 @@ enum L10n {
"account.save.password": "Update password", "account.save.password": "Update password",
"account.save.profile": "Save profile", "account.save.profile": "Save profile",
"account.title": "Account", "account.title": "Account",
"forgot.password.lead": "Enter your account email: we'll send you a link to reset your password.", "forgot.password.lead": "Enter your account email: we will send you a link to reset your password.",
"forgot.password.submit": "Send reset link", "forgot.password.submit": "Send reset link",
"forgot.password.success": "If the email is registered, you will receive a password reset link shortly.", "forgot.password.success": "If the email is registered, you will receive a password reset link shortly.",
"forgot.password.title": "Forgot password", "forgot.password.title": "Forgot password",
@@ -485,7 +484,7 @@ enum L10n {
"match.status.start": "START", "match.status.start": "START",
"matches.active.team": "Active team: %1$@.", "matches.active.team": "Active team: %1$@.",
"matches.change": "Change", "matches.change": "Change",
"matches.empty.hint": "Schedule a match or start a new one with «New match».", "matches.empty.hint": "Tap «New match» to schedule ahead or start right away.",
"matches.empty.title": "No matches on the calendar", "matches.empty.title": "No matches on the calendar",
"matches.hello": "Hi, %1$@", "matches.hello": "Hi, %1$@",
"matches.load.error": "Loading error", "matches.load.error": "Loading error",
@@ -503,9 +502,8 @@ enum L10n {
"matches.no.team.title": "No team assigned", "matches.no.team.title": "No team assigned",
"matches.ready.title": "Ready to start", "matches.ready.title": "Ready to start",
"matches.retry": "Retry", "matches.retry": "Retry",
"matches.schedule": "Scheduled match",
"matches.scheduled.title": "Scheduled matches", "matches.scheduled.title": "Scheduled matches",
"matches.subtitle": "Resume a live stream or start a scheduled match.", "matches.subtitle": "Resume a live stream or start a new match.",
"matches.tap.change.team": "Tap to change team", "matches.tap.change.team": "Tap to change team",
"matches.team.for.live": "Team for the live stream", "matches.team.for.live": "Team for the live stream",
"matches.title": "Matches", "matches.title": "Matches",
@@ -663,7 +661,7 @@ enum L10n {
"account.confirm.password": "Confirmer le mot de passe", "account.confirm.password": "Confirmer le mot de passe",
"account.current.password": "Mot de passe actuel", "account.current.password": "Mot de passe actuel",
"account.name.label": "Nom", "account.name.label": "Nom",
"account.new.password": "Nouveau mot de passe (min. 8 caractères)", "account.new.password": "Nouveau mot de passe (min. 8, au moins 3 types de caractères)",
"account.password.heading": "Changer le mot de passe", "account.password.heading": "Changer le mot de passe",
"account.password.saved": "Mot de passe mis à jour", "account.password.saved": "Mot de passe mis à jour",
"account.profile.heading": "Profil", "account.profile.heading": "Profil",
@@ -776,7 +774,7 @@ enum L10n {
"match.status.start": "DÉMARRER", "match.status.start": "DÉMARRER",
"matches.active.team": "Équipe active : %1$@.", "matches.active.team": "Équipe active : %1$@.",
"matches.change": "Changer", "matches.change": "Changer",
"matches.empty.hint": "Programmez un match ou démarrez-en un avec « Nouveau match ».", "matches.empty.hint": "Touchez « Nouveau match » pour planifier à l'avance ou démarrer tout de suite.",
"matches.empty.title": "Aucun match au calendrier", "matches.empty.title": "Aucun match au calendrier",
"matches.hello": "Bonjour, %1$@", "matches.hello": "Bonjour, %1$@",
"matches.load.error": "Erreur de chargement", "matches.load.error": "Erreur de chargement",
@@ -794,9 +792,8 @@ enum L10n {
"matches.no.team.title": "Aucune équipe assignée", "matches.no.team.title": "Aucune équipe assignée",
"matches.ready.title": "Prêts à démarrer", "matches.ready.title": "Prêts à démarrer",
"matches.retry": "Réessayer", "matches.retry": "Réessayer",
"matches.schedule": "Match programmé",
"matches.scheduled.title": "Matchs programmés", "matches.scheduled.title": "Matchs programmés",
"matches.subtitle": "Reprenez un direct en cours ou démarrez un match programmé.", "matches.subtitle": "Reprenez un direct en cours ou démarrez un nouveau match.",
"matches.tap.change.team": "Appuyez pour changer d'équipe", "matches.tap.change.team": "Appuyez pour changer d'équipe",
"matches.team.for.live": "Équipe pour le direct", "matches.team.for.live": "Équipe pour le direct",
"matches.title": "Matchs", "matches.title": "Matchs",
@@ -954,7 +951,7 @@ enum L10n {
"account.confirm.password": "Passwort bestätigen", "account.confirm.password": "Passwort bestätigen",
"account.current.password": "Aktuelles Passwort", "account.current.password": "Aktuelles Passwort",
"account.name.label": "Name", "account.name.label": "Name",
"account.new.password": "Neues Passwort (mind. 8 Zeichen)", "account.new.password": "Neues Passwort (mind. 8, mindestens 3 Zeichenarten)",
"account.password.heading": "Passwort ändern", "account.password.heading": "Passwort ändern",
"account.password.saved": "Passwort aktualisiert", "account.password.saved": "Passwort aktualisiert",
"account.profile.heading": "Profil", "account.profile.heading": "Profil",
@@ -1067,7 +1064,7 @@ enum L10n {
"match.status.start": "STARTEN", "match.status.start": "STARTEN",
"matches.active.team": "Aktives Team: %1$@.", "matches.active.team": "Aktives Team: %1$@.",
"matches.change": "Wechseln", "matches.change": "Wechseln",
"matches.empty.hint": "Plane ein Spiel oder starte ein neues mit «Neues Spiel».", "matches.empty.hint": "Tippe auf «Neues Spiel», um im Voraus zu planen oder sofort zu starten.",
"matches.empty.title": "Keine Spiele im Kalender", "matches.empty.title": "Keine Spiele im Kalender",
"matches.hello": "Hallo, %1$@", "matches.hello": "Hallo, %1$@",
"matches.load.error": "Ladefehler", "matches.load.error": "Ladefehler",
@@ -1085,9 +1082,8 @@ enum L10n {
"matches.no.team.title": "Kein Team zugewiesen", "matches.no.team.title": "Kein Team zugewiesen",
"matches.ready.title": "Bereit zum Start", "matches.ready.title": "Bereit zum Start",
"matches.retry": "Erneut versuchen", "matches.retry": "Erneut versuchen",
"matches.schedule": "Geplantes Spiel",
"matches.scheduled.title": "Geplante Spiele", "matches.scheduled.title": "Geplante Spiele",
"matches.subtitle": "Nimm einen laufenden Livestream wieder auf oder starte ein geplantes Spiel.", "matches.subtitle": "Nimm einen laufenden Livestream wieder auf oder starte ein neues Spiel.",
"matches.tap.change.team": "Tippen zum Teamwechsel", "matches.tap.change.team": "Tippen zum Teamwechsel",
"matches.team.for.live": "Team für den Livestream", "matches.team.for.live": "Team für den Livestream",
"matches.title": "Spiele", "matches.title": "Spiele",
@@ -1245,7 +1241,7 @@ enum L10n {
"account.confirm.password": "Confirmar contraseña", "account.confirm.password": "Confirmar contraseña",
"account.current.password": "Contraseña actual", "account.current.password": "Contraseña actual",
"account.name.label": "Nombre", "account.name.label": "Nombre",
"account.new.password": "Nueva contraseña (mín. 8 caracteres)", "account.new.password": "Nueva contraseña (mín. 8, al menos 3 tipos de caracteres)",
"account.password.heading": "Cambiar contraseña", "account.password.heading": "Cambiar contraseña",
"account.password.saved": "Contraseña actualizada", "account.password.saved": "Contraseña actualizada",
"account.profile.heading": "Perfil", "account.profile.heading": "Perfil",
@@ -1358,7 +1354,7 @@ enum L10n {
"match.status.start": "INICIAR", "match.status.start": "INICIAR",
"matches.active.team": "Equipo activo: %1$@.", "matches.active.team": "Equipo activo: %1$@.",
"matches.change": "Cambiar", "matches.change": "Cambiar",
"matches.empty.hint": "Programa un partido o inicia uno nuevo con «Nuevo partido».", "matches.empty.hint": "Toca «Nuevo partido» para programar con antelación o empezar ya.",
"matches.empty.title": "Ningún partido en el calendario", "matches.empty.title": "Ningún partido en el calendario",
"matches.hello": "Hola, %1$@", "matches.hello": "Hola, %1$@",
"matches.load.error": "Error de carga", "matches.load.error": "Error de carga",
@@ -1376,9 +1372,8 @@ enum L10n {
"matches.no.team.title": "Ningún equipo asignado", "matches.no.team.title": "Ningún equipo asignado",
"matches.ready.title": "Listos para empezar", "matches.ready.title": "Listos para empezar",
"matches.retry": "Reintentar", "matches.retry": "Reintentar",
"matches.schedule": "Partido programado",
"matches.scheduled.title": "Partidos programados", "matches.scheduled.title": "Partidos programados",
"matches.subtitle": "Reanuda un directo en curso o inicia un partido programado.", "matches.subtitle": "Reanuda un directo en curso o inicia un nuevo partido.",
"matches.tap.change.team": "Toca para cambiar de equipo", "matches.tap.change.team": "Toca para cambiar de equipo",
"matches.team.for.live": "Equipo para el directo", "matches.team.for.live": "Equipo para el directo",
"matches.title": "Partidos", "matches.title": "Partidos",
@@ -240,6 +240,7 @@ final class MatchLiveAPI: @unchecked Sendable {
request.httpMethod = "PATCH" request.httpMethod = "PATCH"
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
request.setValue("application/json", forHTTPHeaderField: "Accept") request.setValue("application/json", forHTTPHeaderField: "Accept")
request.setValue(AppLanguage.resolvedCode, forHTTPHeaderField: "Accept-Language")
if let accessToken { request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization") } if let accessToken { request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization") }
request.httpBody = MultipartBuilder.build(fields: fields, boundary: boundary) request.httpBody = MultipartBuilder.build(fields: fields, boundary: boundary)
return try await execute(request) return try await execute(request)
@@ -253,6 +254,7 @@ final class MatchLiveAPI: @unchecked Sendable {
var request = URLRequest(url: baseURL.appendingPathComponent(path)) var request = URLRequest(url: baseURL.appendingPathComponent(path))
request.httpMethod = method request.httpMethod = method
request.setValue("application/json", forHTTPHeaderField: "Accept") request.setValue("application/json", forHTTPHeaderField: "Accept")
request.setValue(AppLanguage.resolvedCode, forHTTPHeaderField: "Accept-Language")
if body != nil { if body != nil {
request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try ApiInstant.encoder.encode(body) request.httpBody = try ApiInstant.encoder.encode(body)

Some files were not shown because too many files have changed in this diff Show More