Add internationalization and localization to seasons

This commit is contained in:
jordi fita mas 2023-10-03 21:14:37 +02:00
parent 0ce1b4bd64
commit 5f38ab8fd3
19 changed files with 472 additions and 84 deletions

View File

@ -242,6 +242,15 @@ select add_season(52, 'Temporada alta', '#ff926c');
select add_season(52, 'Temporada mitjana', '#ffe37f'); select add_season(52, 'Temporada mitjana', '#ffe37f');
select add_season(52, 'Temporada baixa', '#00aa7d'); select add_season(52, 'Temporada baixa', '#00aa7d');
insert into season_i18n (season_id, lang_tag, name)
values (92, 'en', 'Peak season')
, (92, 'es', 'Temporada alta')
, (93, 'en', 'Shoulder season')
, (93, 'es', 'Temporada media')
, (94, 'en', 'Offseason')
, (94, 'es', 'Temporada baja')
;
select set_season_range(92, '[2023-04-06, 2023-04-10]'); select set_season_range(92, '[2023-04-06, 2023-04-10]');
select set_season_range(94, '[2023-04-11, 2023-04-27]'); select set_season_range(94, '[2023-04-11, 2023-04-27]');
select set_season_range(93, '[2023-04-28, 2023-04-30]'); select set_season_range(93, '[2023-04-28, 2023-04-30]');

22
deploy/season_i18n.sql Normal file
View File

@ -0,0 +1,22 @@
-- Deploy camper:season_i18n to pg
-- requires: roles
-- requires: schema_camper
-- requires: season
-- requires: language
begin;
set search_path to camper, public;
create table season_i18n (
season_id integer not null references season,
lang_tag text not null references language,
name text not null,
primary key (season_id, lang_tag)
);
grant select on table season_i18n to guest;
grant select on table season_i18n to employee;
grant select, insert, update, delete on table season_i18n to admin;
commit;

View File

@ -0,0 +1,27 @@
-- Deploy camper:translate_season to pg
-- requires: roles
-- requires: schema_camper
-- requires: season
-- requires: season_i18n
begin;
set search_path to camper, public;
create or replace function translate_season(slug uuid, lang_tag text, name text) returns void as
$$
insert into season_i18n (season_id, lang_tag, name)
select season_id, translate_season.lang_tag, translate_season.name
from season
where slug = translate_season.slug
on conflict (season_id, lang_tag) do update
set name = excluded.name
;
$$
language sql
;
revoke execute on function translate_season(uuid, text, text) from public;
grant execute on function translate_season(uuid, text, text) to admin;
commit;

View File

@ -36,7 +36,7 @@ func newAdminHandler(locales locale.Locales, mediaDir string) *adminHandler {
company: company.NewAdminHandler(), company: company.NewAdminHandler(),
home: home.NewAdminHandler(locales), home: home.NewAdminHandler(locales),
media: media.NewAdminHandler(mediaDir), media: media.NewAdminHandler(mediaDir),
season: season.NewAdminHandler(), season: season.NewAdminHandler(locales),
services: services.NewAdminHandler(locales), services: services.NewAdminHandler(locales),
} }
} }

View File

@ -13,6 +13,7 @@ import (
"dev.tandem.ws/tandem/camper/pkg/auth" "dev.tandem.ws/tandem/camper/pkg/auth"
"dev.tandem.ws/tandem/camper/pkg/database" "dev.tandem.ws/tandem/camper/pkg/database"
httplib "dev.tandem.ws/tandem/camper/pkg/http" httplib "dev.tandem.ws/tandem/camper/pkg/http"
"dev.tandem.ws/tandem/camper/pkg/locale"
"dev.tandem.ws/tandem/camper/pkg/template" "dev.tandem.ws/tandem/camper/pkg/template"
"dev.tandem.ws/tandem/camper/pkg/uuid" "dev.tandem.ws/tandem/camper/pkg/uuid"
) )
@ -31,7 +32,7 @@ func (h *PublicHandler) Handler(user *auth.User, company *auth.Company, conn *da
http.NotFound(w, r) http.NotFound(w, r)
return return
} }
page, err := newPublicPage(r.Context(), user, conn, head) page, err := newPublicPage(r.Context(), conn, user.Locale, head)
if database.ErrorIsNotFound(err) { if database.ErrorIsNotFound(err) {
http.NotFound(w, r) http.NotFound(w, r)
return return
@ -59,7 +60,7 @@ type typePrice struct {
PricePerNight string PricePerNight string
} }
func newPublicPage(ctx context.Context, user *auth.User, conn *database.Conn, slug string) (*publicPage, error) { func newPublicPage(ctx context.Context, conn *database.Conn, loc *locale.Locale, slug string) (*publicPage, error) {
page := &publicPage{ page := &publicPage{
PublicPage: template.NewPublicPage(), PublicPage: template.NewPublicPage(),
} }
@ -70,23 +71,23 @@ func newPublicPage(ctx context.Context, user *auth.User, conn *database.Conn, sl
left join campsite_type_i18n as i18n on campsite_type.campsite_type_id = i18n.campsite_type_id and i18n.lang_tag = $1 left join campsite_type_i18n as i18n on campsite_type.campsite_type_id = i18n.campsite_type_id and i18n.lang_tag = $1
where slug = $2 where slug = $2
and active and active
`, user.Locale.Language, slug) `, loc.Language, slug)
if err := row.Scan(&page.Name, &page.Description); err != nil { if err := row.Scan(&page.Name, &page.Description); err != nil {
return nil, err return nil, err
} }
rows, err := conn.Query(ctx, ` rows, err := conn.Query(ctx, `
select season.name --coalesce(i18n.name, season.name) as l10n_name select coalesce(i18n.name, season.name) as l10n_name
, to_color(season.color) , to_color(season.color)
, coalesce(min_nights, 1) , coalesce(min_nights, 1)
, to_price(coalesce(cost_per_night, 0))::text , to_price(coalesce(cost_per_night, 0))::text
from season from season
--left join season_i18n as i18n on season.season_id = i18n.season_id and i18n.lang_tag = $1 left join season_i18n as i18n on season.season_id = i18n.season_id and i18n.lang_tag = $1
left join ( left join (
campsite_type_cost as cost join campsite_type as type on cost.campsite_type_id = type.campsite_type_id and type.slug = $1 campsite_type_cost as cost join campsite_type as type on cost.campsite_type_id = type.campsite_type_id and type.slug = $2
) as cost on cost.season_id = season.season_id ) as cost on cost.season_id = season.season_id
where season.active where season.active
`, slug) `, loc.Language, slug)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -15,6 +15,7 @@ import (
"time" "time"
"github.com/jackc/pgtype" "github.com/jackc/pgtype"
"github.com/jackc/pgx/v4"
"dev.tandem.ws/tandem/camper/pkg/auth" "dev.tandem.ws/tandem/camper/pkg/auth"
"dev.tandem.ws/tandem/camper/pkg/database" "dev.tandem.ws/tandem/camper/pkg/database"
@ -28,10 +29,13 @@ import (
const unsetColor = 13750495 const unsetColor = 13750495
type AdminHandler struct { type AdminHandler struct {
locales locale.Locales
} }
func NewAdminHandler() *AdminHandler { func NewAdminHandler(locales locale.Locales) *AdminHandler {
return &AdminHandler{} return &AdminHandler{
locales: locales,
}
} }
func (h *AdminHandler) Handler(user *auth.User, company *auth.Company, conn *database.Conn) http.HandlerFunc { func (h *AdminHandler) Handler(user *auth.User, company *auth.Company, conn *database.Conn) http.HandlerFunc {
@ -79,6 +83,11 @@ func (h *AdminHandler) Handler(user *auth.User, company *auth.Company, conn *dat
} }
panic(err) panic(err)
} }
var langTag string
langTag, r.URL.Path = httplib.ShiftPath(r.URL.Path)
switch langTag {
case "":
switch r.Method { switch r.Method {
case http.MethodGet: case http.MethodGet:
f.MustRender(w, r, user, company) f.MustRender(w, r, user, company)
@ -87,6 +96,25 @@ func (h *AdminHandler) Handler(user *auth.User, company *auth.Company, conn *dat
default: default:
httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPut) httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPut)
} }
default:
loc, ok := h.locales.Get(langTag)
if !ok {
http.NotFound(w, r)
return
}
l10n := newSeasonL10nForm(f, loc)
if err := l10n.FillFromDatabase(r.Context(), conn); err != nil {
panic(err)
}
switch r.Method {
case http.MethodGet:
l10n.MustRender(w, r, user, company)
case http.MethodPut:
editSeasonL10n(w, r, user, company, conn, l10n)
default:
httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPut)
}
}
} }
} }
} }
@ -120,13 +148,22 @@ func getCalendarYear(query url.Values) int {
func collectSeasonEntries(ctx context.Context, company *auth.Company, conn *database.Conn) ([]*seasonEntry, error) { func collectSeasonEntries(ctx context.Context, company *auth.Company, conn *database.Conn) ([]*seasonEntry, error) {
rows, err := conn.Query(ctx, ` rows, err := conn.Query(ctx, `
select slug select '/admin/seasons/' || season.slug
, name , season.name
, to_color(color)::text , to_color(color)::text
, active , active
, array_agg((lang_tag, endonym, not exists (select 1 from season_i18n as i18n where i18n.season_id = season.season_id and i18n.lang_tag = language.lang_tag)) order by endonym)
from season from season
where company_id = $1 join company using (company_id)
order by name`, company.ID) , language
where lang_tag <> default_lang_tag
and language.selectable
and season.company_id = $1
group by season.slug
, season.name
, to_color(color)::text
, active
order by name`, pgx.QueryResultFormats{pgx.BinaryFormatCode}, company.ID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -135,9 +172,17 @@ func collectSeasonEntries(ctx context.Context, company *auth.Company, conn *data
var seasons []*seasonEntry var seasons []*seasonEntry
for rows.Next() { for rows.Next() {
entry := &seasonEntry{} entry := &seasonEntry{}
if err = rows.Scan(&entry.Slug, &entry.Name, &entry.Color, &entry.Active); err != nil { var translations database.RecordArray
if err = rows.Scan(&entry.URL, &entry.Name, &entry.Color, &entry.Active, &translations); err != nil {
return nil, err return nil, err
} }
for _, el := range translations.Elements {
entry.Translations = append(entry.Translations, &locale.Translation{
URL: entry.URL + "/" + el.Fields[0].Get().(string),
Endonym: el.Fields[1].Get().(string),
Missing: el.Fields[2].Get().(bool),
})
}
seasons = append(seasons, entry) seasons = append(seasons, entry)
} }
@ -145,10 +190,12 @@ func collectSeasonEntries(ctx context.Context, company *auth.Company, conn *data
} }
type seasonEntry struct { type seasonEntry struct {
Slug string ID int
URL string
Name string Name string
Color string Color string
Active bool Active bool
Translations []*locale.Translation
} }
var longMonthNames = []string{ var longMonthNames = []string{
@ -396,13 +443,13 @@ func newCalendarForm(ctx context.Context, company *auth.Company, conn *database.
func mustCollectCalendarSeasons(ctx context.Context, company *auth.Company, conn *database.Conn) []*seasonEntry { func mustCollectCalendarSeasons(ctx context.Context, company *auth.Company, conn *database.Conn) []*seasonEntry {
rows, err := conn.Query(ctx, ` rows, err := conn.Query(ctx, `
select '0' as slug select 0 as season_id
, $1 as name , $1 as name
, to_color($2)::text , to_color($2)::text
, true , true
, 0 as sort , 0 as sort
union all union all
select season_id::text select season_id
, name , name
, to_color(color)::text , to_color(color)::text
, active , active
@ -420,7 +467,7 @@ func mustCollectCalendarSeasons(ctx context.Context, company *auth.Company, conn
for rows.Next() { for rows.Next() {
entry := &seasonEntry{} entry := &seasonEntry{}
var sort int var sort int
if err = rows.Scan(&entry.Slug, &entry.Name, &entry.Color, &entry.Active, &sort); err != nil { if err = rows.Scan(&entry.ID, &entry.Name, &entry.Color, &entry.Active, &sort); err != nil {
panic(err) panic(err)
} }
seasons = append(seasons, entry) seasons = append(seasons, entry)

71
pkg/season/l10n.go Normal file
View File

@ -0,0 +1,71 @@
/*
* SPDX-FileCopyrightText: 2023 jordi fita mas <jfita@peritasoft.com>
* SPDX-License-Identifier: AGPL-3.0-only
*/
package season
import (
"context"
"net/http"
"dev.tandem.ws/tandem/camper/pkg/auth"
"dev.tandem.ws/tandem/camper/pkg/database"
"dev.tandem.ws/tandem/camper/pkg/form"
httplib "dev.tandem.ws/tandem/camper/pkg/http"
"dev.tandem.ws/tandem/camper/pkg/locale"
"dev.tandem.ws/tandem/camper/pkg/template"
)
type seasonL10nForm struct {
Locale *locale.Locale
Slug string
Name *form.L10nInput
}
func newSeasonL10nForm(f *seasonForm, loc *locale.Locale) *seasonL10nForm {
return &seasonL10nForm{
Locale: loc,
Slug: f.Slug,
Name: f.Name.L10nInput(),
}
}
func (l10n *seasonL10nForm) FillFromDatabase(ctx context.Context, conn *database.Conn) error {
row := conn.QueryRow(ctx, `
select coalesce(i18n.name, '') as l10n_name
from season
left join season_i18n as i18n on season.season_id = i18n.season_id and i18n.lang_tag = $1
where slug = $2
`, l10n.Locale.Language, l10n.Slug)
return row.Scan(&l10n.Name.Val)
}
func (l10n *seasonL10nForm) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
template.MustRenderAdmin(w, r, user, company, "season/l10n.gohtml", l10n)
}
func editSeasonL10n(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, l10n *seasonL10nForm) {
if ok, err := form.Handle(l10n, w, r, user); err != nil {
return
} else if !ok {
l10n.MustRender(w, r, user, company)
return
}
conn.MustExec(r.Context(), "select translate_season($1, $2, $3)", l10n.Slug, l10n.Locale.Language, l10n.Name)
httplib.Redirect(w, r, "/admin/seasons", http.StatusSeeOther)
}
func (l10n *seasonL10nForm) Parse(r *http.Request) error {
if err := r.ParseForm(); err != nil {
return err
}
l10n.Name.FillValue(r)
return nil
}
func (l10n *seasonL10nForm) Valid(l *locale.Locale) bool {
v := form.NewValidator(l)
v.CheckRequired(&l10n.Name.Input, l.GettextNoop("Name can not be empty."))
return v.AllOK
}

View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: camper\n" "Project-Id-Version: camper\n"
"Report-Msgid-Bugs-To: jordi@tandem.blog\n" "Report-Msgid-Bugs-To: jordi@tandem.blog\n"
"POT-Creation-Date: 2023-10-01 21:09+0200\n" "POT-Creation-Date: 2023-10-03 21:05+0200\n"
"PO-Revision-Date: 2023-07-22 23:45+0200\n" "PO-Revision-Date: 2023-07-22 23:45+0200\n"
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n" "Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
"Language-Team: Catalan <ca@dodds.net>\n" "Language-Team: Catalan <ca@dodds.net>\n"
@ -205,6 +205,7 @@ msgstr "Traducció de la diapositiva del carrusel a %s"
#: web/templates/admin/carousel/l10n.gohtml:21 #: web/templates/admin/carousel/l10n.gohtml:21
#: web/templates/admin/campsite/type/l10n.gohtml:21 #: web/templates/admin/campsite/type/l10n.gohtml:21
#: web/templates/admin/campsite/type/l10n.gohtml:33 #: web/templates/admin/campsite/type/l10n.gohtml:33
#: web/templates/admin/season/l10n.gohtml:21
#: web/templates/admin/services/l10n.gohtml:21 #: web/templates/admin/services/l10n.gohtml:21
#: web/templates/admin/services/l10n.gohtml:33 #: web/templates/admin/services/l10n.gohtml:33
msgid "Source:" msgid "Source:"
@ -213,6 +214,7 @@ msgstr "Origen:"
#: web/templates/admin/carousel/l10n.gohtml:23 #: web/templates/admin/carousel/l10n.gohtml:23
#: web/templates/admin/campsite/type/l10n.gohtml:23 #: web/templates/admin/campsite/type/l10n.gohtml:23
#: web/templates/admin/campsite/type/l10n.gohtml:36 #: web/templates/admin/campsite/type/l10n.gohtml:36
#: web/templates/admin/season/l10n.gohtml:23
#: web/templates/admin/services/l10n.gohtml:23 #: web/templates/admin/services/l10n.gohtml:23
#: web/templates/admin/services/l10n.gohtml:36 #: web/templates/admin/services/l10n.gohtml:36
msgctxt "input" msgctxt "input"
@ -221,6 +223,7 @@ msgstr "Traducció:"
#: web/templates/admin/carousel/l10n.gohtml:32 #: web/templates/admin/carousel/l10n.gohtml:32
#: web/templates/admin/campsite/type/l10n.gohtml:45 #: web/templates/admin/campsite/type/l10n.gohtml:45
#: web/templates/admin/season/l10n.gohtml:32
#: web/templates/admin/services/l10n.gohtml:45 #: web/templates/admin/services/l10n.gohtml:45
msgctxt "action" msgctxt "action"
msgid "Translate" msgid "Translate"
@ -282,13 +285,13 @@ msgstr "Tipus"
#: web/templates/admin/campsite/index.gohtml:28 #: web/templates/admin/campsite/index.gohtml:28
#: web/templates/admin/campsite/type/index.gohtml:35 #: web/templates/admin/campsite/type/index.gohtml:35
#: web/templates/admin/season/index.gohtml:31 #: web/templates/admin/season/index.gohtml:39
msgid "Yes" msgid "Yes"
msgstr "Sí" msgstr "Sí"
#: web/templates/admin/campsite/index.gohtml:28 #: web/templates/admin/campsite/index.gohtml:28
#: web/templates/admin/campsite/type/index.gohtml:35 #: web/templates/admin/campsite/type/index.gohtml:35
#: web/templates/admin/season/index.gohtml:31 #: web/templates/admin/season/index.gohtml:39
msgid "No" msgid "No"
msgstr "No" msgstr "No"
@ -317,6 +320,7 @@ msgstr "Actiu"
#: web/templates/admin/campsite/type/form.gohtml:46 #: web/templates/admin/campsite/type/form.gohtml:46
#: web/templates/admin/campsite/type/l10n.gohtml:20 #: web/templates/admin/campsite/type/l10n.gohtml:20
#: web/templates/admin/season/form.gohtml:46 #: web/templates/admin/season/form.gohtml:46
#: web/templates/admin/season/l10n.gohtml:20
#: web/templates/admin/services/form.gohtml:52 #: web/templates/admin/services/form.gohtml:52
#: web/templates/admin/services/l10n.gohtml:20 #: web/templates/admin/services/l10n.gohtml:20
#: web/templates/admin/profile.gohtml:26 #: web/templates/admin/profile.gohtml:26
@ -371,6 +375,7 @@ msgid "Name"
msgstr "Nom" msgstr "Nom"
#: web/templates/admin/campsite/type/index.gohtml:18 #: web/templates/admin/campsite/type/index.gohtml:18
#: web/templates/admin/season/index.gohtml:19
#: web/templates/admin/services/index.gohtml:19 #: web/templates/admin/services/index.gohtml:19
#: web/templates/admin/services/index.gohtml:60 #: web/templates/admin/services/index.gohtml:60
#: web/templates/admin/home/index.gohtml:19 #: web/templates/admin/home/index.gohtml:19
@ -401,7 +406,7 @@ msgid "New Season"
msgstr "Nova temporada" msgstr "Nova temporada"
#: web/templates/admin/season/form.gohtml:37 #: web/templates/admin/season/form.gohtml:37
#: web/templates/admin/season/index.gohtml:19 #: web/templates/admin/season/index.gohtml:20
msgctxt "season" msgctxt "season"
msgid "Active" msgid "Active"
msgstr "Activa" msgstr "Activa"
@ -428,15 +433,21 @@ msgctxt "header"
msgid "Color" msgid "Color"
msgstr "Color" msgstr "Color"
#: web/templates/admin/season/index.gohtml:37 #: web/templates/admin/season/index.gohtml:45
msgid "No seasons added yet." msgid "No seasons added yet."
msgstr "No sha afegit cap temporada encara." msgstr "No sha afegit cap temporada encara."
#: web/templates/admin/season/index.gohtml:40 #: web/templates/admin/season/index.gohtml:48
msgctxt "title" msgctxt "title"
msgid "Calendar" msgid "Calendar"
msgstr "Calendari" msgstr "Calendari"
#: web/templates/admin/season/l10n.gohtml:7
#: web/templates/admin/season/l10n.gohtml:14
msgctxt "title"
msgid "Translate Season to %s"
msgstr "Traducció de la temporada a %s"
#: web/templates/admin/season/calendar.gohtml:16 #: web/templates/admin/season/calendar.gohtml:16
msgctxt "day" msgctxt "day"
msgid "Mon" msgid "Mon"
@ -836,8 +847,8 @@ msgid "Automatic"
msgstr "Automàtic" msgstr "Automàtic"
#: pkg/app/user.go:249 pkg/campsite/types/l10n.go:82 #: pkg/app/user.go:249 pkg/campsite/types/l10n.go:82
#: pkg/campsite/types/admin.go:387 pkg/season/admin.go:335 #: pkg/campsite/types/admin.go:387 pkg/season/l10n.go:69
#: pkg/services/l10n.go:73 pkg/services/admin.go:266 #: pkg/season/admin.go:382 pkg/services/l10n.go:73 pkg/services/admin.go:266
msgid "Name can not be empty." msgid "Name can not be empty."
msgstr "No podeu deixar el nom en blanc." msgstr "No podeu deixar el nom en blanc."
@ -923,92 +934,92 @@ msgstr "El tipus dallotjament escollit no és vàlid."
msgid "Label can not be empty." msgid "Label can not be empty."
msgstr "No podeu deixar letiqueta en blanc." msgstr "No podeu deixar letiqueta en blanc."
#: pkg/season/admin.go:155 #: pkg/season/admin.go:202
msgctxt "month" msgctxt "month"
msgid "January" msgid "January"
msgstr "gener" msgstr "gener"
#: pkg/season/admin.go:156 #: pkg/season/admin.go:203
msgctxt "month" msgctxt "month"
msgid "February" msgid "February"
msgstr "febrer" msgstr "febrer"
#: pkg/season/admin.go:157 #: pkg/season/admin.go:204
msgctxt "month" msgctxt "month"
msgid "March" msgid "March"
msgstr "març" msgstr "març"
#: pkg/season/admin.go:158 #: pkg/season/admin.go:205
msgctxt "month" msgctxt "month"
msgid "April" msgid "April"
msgstr "abril" msgstr "abril"
#: pkg/season/admin.go:159 #: pkg/season/admin.go:206
msgctxt "month" msgctxt "month"
msgid "May" msgid "May"
msgstr "maig" msgstr "maig"
#: pkg/season/admin.go:160 #: pkg/season/admin.go:207
msgctxt "month" msgctxt "month"
msgid "June" msgid "June"
msgstr "juny" msgstr "juny"
#: pkg/season/admin.go:161 #: pkg/season/admin.go:208
msgctxt "month" msgctxt "month"
msgid "July" msgid "July"
msgstr "juliol" msgstr "juliol"
#: pkg/season/admin.go:162 #: pkg/season/admin.go:209
msgctxt "month" msgctxt "month"
msgid "August" msgid "August"
msgstr "agost" msgstr "agost"
#: pkg/season/admin.go:163 #: pkg/season/admin.go:210
msgctxt "month" msgctxt "month"
msgid "September" msgid "September"
msgstr "setembre" msgstr "setembre"
#: pkg/season/admin.go:164 #: pkg/season/admin.go:211
msgctxt "month" msgctxt "month"
msgid "October" msgid "October"
msgstr "octubre" msgstr "octubre"
#: pkg/season/admin.go:165 #: pkg/season/admin.go:212
msgctxt "month" msgctxt "month"
msgid "November" msgid "November"
msgstr "novembre" msgstr "novembre"
#: pkg/season/admin.go:166 #: pkg/season/admin.go:213
msgctxt "month" msgctxt "month"
msgid "December" msgid "December"
msgstr "desembre" msgstr "desembre"
#: pkg/season/admin.go:336 #: pkg/season/admin.go:383
msgid "Color can not be empty." msgid "Color can not be empty."
msgstr "No podeu deixar el color en blanc." msgstr "No podeu deixar el color en blanc."
#: pkg/season/admin.go:337 #: pkg/season/admin.go:384
msgid "This color is not valid. It must be like #123abc." msgid "This color is not valid. It must be like #123abc."
msgstr "Aquest color no és vàlid. Hauria de ser similar a #123abc." msgstr "Aquest color no és vàlid. Hauria de ser similar a #123abc."
#: pkg/season/admin.go:413 #: pkg/season/admin.go:460
msgctxt "action" msgctxt "action"
msgid "Unset" msgid "Unset"
msgstr "Desassigna" msgstr "Desassigna"
#: pkg/season/admin.go:444 #: pkg/season/admin.go:491
msgid "Start date can not be empty." msgid "Start date can not be empty."
msgstr "No podeu deixar la data dinici en blanc." msgstr "No podeu deixar la data dinici en blanc."
#: pkg/season/admin.go:445 #: pkg/season/admin.go:492
msgid "Start date must be a valid date." msgid "Start date must be a valid date."
msgstr "La data dinici ha de ser una data vàlida." msgstr "La data dinici ha de ser una data vàlida."
#: pkg/season/admin.go:447 #: pkg/season/admin.go:494
msgid "End date can not be empty." msgid "End date can not be empty."
msgstr "No podeu deixar la data de fi en blanc." msgstr "No podeu deixar la data de fi en blanc."
#: pkg/season/admin.go:448 #: pkg/season/admin.go:495
msgid "End date must be a valid date." msgid "End date must be a valid date."
msgstr "La data de fi ha de ser una data vàlida." msgstr "La data de fi ha de ser una data vàlida."

View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: camper\n" "Project-Id-Version: camper\n"
"Report-Msgid-Bugs-To: jordi@tandem.blog\n" "Report-Msgid-Bugs-To: jordi@tandem.blog\n"
"POT-Creation-Date: 2023-10-01 21:09+0200\n" "POT-Creation-Date: 2023-10-03 21:05+0200\n"
"PO-Revision-Date: 2023-07-22 23:46+0200\n" "PO-Revision-Date: 2023-07-22 23:46+0200\n"
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n" "Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
"Language-Team: Spanish <es@tp.org.es>\n" "Language-Team: Spanish <es@tp.org.es>\n"
@ -205,6 +205,7 @@ msgstr "Traducción de la diapositiva de carrusel a %s"
#: web/templates/admin/carousel/l10n.gohtml:21 #: web/templates/admin/carousel/l10n.gohtml:21
#: web/templates/admin/campsite/type/l10n.gohtml:21 #: web/templates/admin/campsite/type/l10n.gohtml:21
#: web/templates/admin/campsite/type/l10n.gohtml:33 #: web/templates/admin/campsite/type/l10n.gohtml:33
#: web/templates/admin/season/l10n.gohtml:21
#: web/templates/admin/services/l10n.gohtml:21 #: web/templates/admin/services/l10n.gohtml:21
#: web/templates/admin/services/l10n.gohtml:33 #: web/templates/admin/services/l10n.gohtml:33
msgid "Source:" msgid "Source:"
@ -213,6 +214,7 @@ msgstr "Origen:"
#: web/templates/admin/carousel/l10n.gohtml:23 #: web/templates/admin/carousel/l10n.gohtml:23
#: web/templates/admin/campsite/type/l10n.gohtml:23 #: web/templates/admin/campsite/type/l10n.gohtml:23
#: web/templates/admin/campsite/type/l10n.gohtml:36 #: web/templates/admin/campsite/type/l10n.gohtml:36
#: web/templates/admin/season/l10n.gohtml:23
#: web/templates/admin/services/l10n.gohtml:23 #: web/templates/admin/services/l10n.gohtml:23
#: web/templates/admin/services/l10n.gohtml:36 #: web/templates/admin/services/l10n.gohtml:36
msgctxt "input" msgctxt "input"
@ -221,6 +223,7 @@ msgstr "Traducción"
#: web/templates/admin/carousel/l10n.gohtml:32 #: web/templates/admin/carousel/l10n.gohtml:32
#: web/templates/admin/campsite/type/l10n.gohtml:45 #: web/templates/admin/campsite/type/l10n.gohtml:45
#: web/templates/admin/season/l10n.gohtml:32
#: web/templates/admin/services/l10n.gohtml:45 #: web/templates/admin/services/l10n.gohtml:45
msgctxt "action" msgctxt "action"
msgid "Translate" msgid "Translate"
@ -282,13 +285,13 @@ msgstr "Tipo"
#: web/templates/admin/campsite/index.gohtml:28 #: web/templates/admin/campsite/index.gohtml:28
#: web/templates/admin/campsite/type/index.gohtml:35 #: web/templates/admin/campsite/type/index.gohtml:35
#: web/templates/admin/season/index.gohtml:31 #: web/templates/admin/season/index.gohtml:39
msgid "Yes" msgid "Yes"
msgstr "Sí" msgstr "Sí"
#: web/templates/admin/campsite/index.gohtml:28 #: web/templates/admin/campsite/index.gohtml:28
#: web/templates/admin/campsite/type/index.gohtml:35 #: web/templates/admin/campsite/type/index.gohtml:35
#: web/templates/admin/season/index.gohtml:31 #: web/templates/admin/season/index.gohtml:39
msgid "No" msgid "No"
msgstr "No" msgstr "No"
@ -317,6 +320,7 @@ msgstr "Activo"
#: web/templates/admin/campsite/type/form.gohtml:46 #: web/templates/admin/campsite/type/form.gohtml:46
#: web/templates/admin/campsite/type/l10n.gohtml:20 #: web/templates/admin/campsite/type/l10n.gohtml:20
#: web/templates/admin/season/form.gohtml:46 #: web/templates/admin/season/form.gohtml:46
#: web/templates/admin/season/l10n.gohtml:20
#: web/templates/admin/services/form.gohtml:52 #: web/templates/admin/services/form.gohtml:52
#: web/templates/admin/services/l10n.gohtml:20 #: web/templates/admin/services/l10n.gohtml:20
#: web/templates/admin/profile.gohtml:26 #: web/templates/admin/profile.gohtml:26
@ -371,6 +375,7 @@ msgid "Name"
msgstr "Nombre" msgstr "Nombre"
#: web/templates/admin/campsite/type/index.gohtml:18 #: web/templates/admin/campsite/type/index.gohtml:18
#: web/templates/admin/season/index.gohtml:19
#: web/templates/admin/services/index.gohtml:19 #: web/templates/admin/services/index.gohtml:19
#: web/templates/admin/services/index.gohtml:60 #: web/templates/admin/services/index.gohtml:60
#: web/templates/admin/home/index.gohtml:19 #: web/templates/admin/home/index.gohtml:19
@ -401,7 +406,7 @@ msgid "New Season"
msgstr "Nueva temporada" msgstr "Nueva temporada"
#: web/templates/admin/season/form.gohtml:37 #: web/templates/admin/season/form.gohtml:37
#: web/templates/admin/season/index.gohtml:19 #: web/templates/admin/season/index.gohtml:20
msgctxt "season" msgctxt "season"
msgid "Active" msgid "Active"
msgstr "Activa" msgstr "Activa"
@ -428,15 +433,21 @@ msgctxt "header"
msgid "Color" msgid "Color"
msgstr "Color" msgstr "Color"
#: web/templates/admin/season/index.gohtml:37 #: web/templates/admin/season/index.gohtml:45
msgid "No seasons added yet." msgid "No seasons added yet."
msgstr "No se ha añadido ninguna temporada todavía." msgstr "No se ha añadido ninguna temporada todavía."
#: web/templates/admin/season/index.gohtml:40 #: web/templates/admin/season/index.gohtml:48
msgctxt "title" msgctxt "title"
msgid "Calendar" msgid "Calendar"
msgstr "Calendario" msgstr "Calendario"
#: web/templates/admin/season/l10n.gohtml:7
#: web/templates/admin/season/l10n.gohtml:14
msgctxt "title"
msgid "Translate Season to %s"
msgstr "Traducción de la temporada a %s"
#: web/templates/admin/season/calendar.gohtml:16 #: web/templates/admin/season/calendar.gohtml:16
msgctxt "day" msgctxt "day"
msgid "Mon" msgid "Mon"
@ -836,8 +847,8 @@ msgid "Automatic"
msgstr "Automático" msgstr "Automático"
#: pkg/app/user.go:249 pkg/campsite/types/l10n.go:82 #: pkg/app/user.go:249 pkg/campsite/types/l10n.go:82
#: pkg/campsite/types/admin.go:387 pkg/season/admin.go:335 #: pkg/campsite/types/admin.go:387 pkg/season/l10n.go:69
#: pkg/services/l10n.go:73 pkg/services/admin.go:266 #: pkg/season/admin.go:382 pkg/services/l10n.go:73 pkg/services/admin.go:266
msgid "Name can not be empty." msgid "Name can not be empty."
msgstr "No podéis dejar el nombre en blanco." msgstr "No podéis dejar el nombre en blanco."
@ -923,92 +934,92 @@ msgstr "El tipo de alojamiento escogido no es válido."
msgid "Label can not be empty." msgid "Label can not be empty."
msgstr "No podéis dejar la etiqueta en blanco." msgstr "No podéis dejar la etiqueta en blanco."
#: pkg/season/admin.go:155 #: pkg/season/admin.go:202
msgctxt "month" msgctxt "month"
msgid "January" msgid "January"
msgstr "enero" msgstr "enero"
#: pkg/season/admin.go:156 #: pkg/season/admin.go:203
msgctxt "month" msgctxt "month"
msgid "February" msgid "February"
msgstr "febrero" msgstr "febrero"
#: pkg/season/admin.go:157 #: pkg/season/admin.go:204
msgctxt "month" msgctxt "month"
msgid "March" msgid "March"
msgstr "marzo" msgstr "marzo"
#: pkg/season/admin.go:158 #: pkg/season/admin.go:205
msgctxt "month" msgctxt "month"
msgid "April" msgid "April"
msgstr "abril" msgstr "abril"
#: pkg/season/admin.go:159 #: pkg/season/admin.go:206
msgctxt "month" msgctxt "month"
msgid "May" msgid "May"
msgstr "mayo" msgstr "mayo"
#: pkg/season/admin.go:160 #: pkg/season/admin.go:207
msgctxt "month" msgctxt "month"
msgid "June" msgid "June"
msgstr "junio" msgstr "junio"
#: pkg/season/admin.go:161 #: pkg/season/admin.go:208
msgctxt "month" msgctxt "month"
msgid "July" msgid "July"
msgstr "julio" msgstr "julio"
#: pkg/season/admin.go:162 #: pkg/season/admin.go:209
msgctxt "month" msgctxt "month"
msgid "August" msgid "August"
msgstr "agosto" msgstr "agosto"
#: pkg/season/admin.go:163 #: pkg/season/admin.go:210
msgctxt "month" msgctxt "month"
msgid "September" msgid "September"
msgstr "septiembre" msgstr "septiembre"
#: pkg/season/admin.go:164 #: pkg/season/admin.go:211
msgctxt "month" msgctxt "month"
msgid "October" msgid "October"
msgstr "octubre" msgstr "octubre"
#: pkg/season/admin.go:165 #: pkg/season/admin.go:212
msgctxt "month" msgctxt "month"
msgid "November" msgid "November"
msgstr "noviembre" msgstr "noviembre"
#: pkg/season/admin.go:166 #: pkg/season/admin.go:213
msgctxt "month" msgctxt "month"
msgid "December" msgid "December"
msgstr "diciembre" msgstr "diciembre"
#: pkg/season/admin.go:336 #: pkg/season/admin.go:383
msgid "Color can not be empty." msgid "Color can not be empty."
msgstr "No podéis dejar el color en blanco." msgstr "No podéis dejar el color en blanco."
#: pkg/season/admin.go:337 #: pkg/season/admin.go:384
msgid "This color is not valid. It must be like #123abc." msgid "This color is not valid. It must be like #123abc."
msgstr "Este color no es válido. Tiene que ser parecido a #123abc." msgstr "Este color no es válido. Tiene que ser parecido a #123abc."
#: pkg/season/admin.go:413 #: pkg/season/admin.go:460
msgctxt "action" msgctxt "action"
msgid "Unset" msgid "Unset"
msgstr "Desasignar" msgstr "Desasignar"
#: pkg/season/admin.go:444 #: pkg/season/admin.go:491
msgid "Start date can not be empty." msgid "Start date can not be empty."
msgstr "No podéis dejar la fecha de inicio en blanco." msgstr "No podéis dejar la fecha de inicio en blanco."
#: pkg/season/admin.go:445 #: pkg/season/admin.go:492
msgid "Start date must be a valid date." msgid "Start date must be a valid date."
msgstr "La fecha de inicio tiene que ser una fecha válida." msgstr "La fecha de inicio tiene que ser una fecha válida."
#: pkg/season/admin.go:447 #: pkg/season/admin.go:494
msgid "End date can not be empty." msgid "End date can not be empty."
msgstr "No podéis dejar la fecha final en blanco." msgstr "No podéis dejar la fecha final en blanco."
#: pkg/season/admin.go:448 #: pkg/season/admin.go:495
msgid "End date must be a valid date." msgid "End date must be a valid date."
msgstr "La fecha final tiene que ser una fecha válida." msgstr "La fecha final tiene que ser una fecha válida."

7
revert/season_i18n.sql Normal file
View File

@ -0,0 +1,7 @@
-- Revert camper:season_i18n from pg
begin;
drop table if exists camper.season_i18n;
commit;

View File

@ -0,0 +1,7 @@
-- Revert camper:translate_season from pg
begin;
drop function if exists camper.translate_season(uuid, text, text);
commit;

View File

@ -88,3 +88,5 @@ campsite_type_cost [roles schema_camper campsite_type season user_profile] 2023-
parse_price [roles schema_camper] 2023-10-01T16:27:50Z jordi fita mas <jordi@tandem.blog> # Add function to format cents to prices parse_price [roles schema_camper] 2023-10-01T16:27:50Z jordi fita mas <jordi@tandem.blog> # Add function to format cents to prices
to_price [roles schema_camper] 2023-10-01T16:30:40Z jordi fita mas <jordi@tandem.blog> # Add function to format cents to prices to_price [roles schema_camper] 2023-10-01T16:30:40Z jordi fita mas <jordi@tandem.blog> # Add function to format cents to prices
set_campsite_type_cost [roles schema_camper campsite_type_cost parse_price] 2023-10-01T17:51:23Z jordi fita mas <jordi@tandem.blog> # Add function to set the cost of a campsite type for a given season set_campsite_type_cost [roles schema_camper campsite_type_cost parse_price] 2023-10-01T17:51:23Z jordi fita mas <jordi@tandem.blog> # Add function to set the cost of a campsite type for a given season
season_i18n [roles schema_camper season language] 2023-10-03T18:30:42Z jordi fita mas <jordi@tandem.blog> # Add relation for season translations
translate_season [roles schema_camper season season_i18n] 2023-10-03T18:37:19Z jordi fita mas <jordi@tandem.blog> # Add function to translate seasons

44
test/season_i18n.sql Normal file
View File

@ -0,0 +1,44 @@
-- Test season_i18n
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(23);
set search_path to camper, public;
select has_table('season_i18n');
select has_pk('season_i18n');
select col_is_pk('season_i18n', array['season_id', 'lang_tag']);
select table_privs_are('season_i18n', 'guest', array['SELECT']);
select table_privs_are('season_i18n', 'employee', array['SELECT']);
select table_privs_are('season_i18n', 'admin', array['SELECT', 'INSERT', 'UPDATE', 'DELETE']);
select table_privs_are('season_i18n', 'authenticator', array[]::text[]);
select has_column('season_i18n', 'season_id');
select col_is_fk('season_i18n', 'season_id');
select fk_ok('season_i18n', 'season_id', 'season', 'season_id');
select col_type_is('season_i18n', 'season_id', 'integer');
select col_not_null('season_i18n', 'season_id');
select col_hasnt_default('season_i18n', 'season_id');
select has_column('season_i18n', 'lang_tag');
select col_is_fk('season_i18n', 'lang_tag');
select fk_ok('season_i18n', 'lang_tag', 'language', 'lang_tag');
select col_type_is('season_i18n', 'lang_tag', 'text');
select col_not_null('season_i18n', 'lang_tag');
select col_hasnt_default('season_i18n', 'lang_tag');
select has_column('season_i18n', 'name');
select col_type_is('season_i18n', 'name', 'text');
select col_not_null('season_i18n', 'name');
select col_hasnt_default('season_i18n', 'name');
select *
from finish();
rollback;

68
test/translate_season.sql Normal file
View File

@ -0,0 +1,68 @@
-- Test translate_season
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(13);
set search_path to camper, public;
select has_function('camper', 'translate_season', array['uuid', 'text', 'text']);
select function_lang_is('camper', 'translate_season', array['uuid', 'text', 'text'], 'sql');
select function_returns('camper', 'translate_season', array['uuid', 'text', 'text'], 'void');
select isnt_definer('camper', 'translate_season', array['uuid', 'text', 'text']);
select volatility_is('camper', 'translate_season', array['uuid', 'text', 'text'], 'volatile');
select function_privs_are('camper', 'translate_season', array['uuid', 'text', 'text'], 'guest', array[]::text[]);
select function_privs_are('camper', 'translate_season', array['uuid', 'text', 'text'], 'employee', array[]::text[]);
select function_privs_are('camper', 'translate_season', array['uuid', 'text', 'text'], 'admin', array['EXECUTE']);
select function_privs_are('camper', 'translate_season', array['uuid', 'text', 'text'], 'authenticator', array[]::text[]);
set client_min_messages to warning;
truncate season_i18n cascade;
truncate season cascade;
truncate company cascade;
reset client_min_messages;
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_lang_tag)
values (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 'ca')
;
insert into season (season_id, company_id, slug, name, color, active)
values (2, 1, '87452b88-b48f-48d3-bb6c-0296de64164e', 'High', to_integer('#232323'), true)
, (3, 1, '9b6370f7-f941-46f2-bc6e-de455675bd0a', 'Low', to_integer('#323232'), false)
;
insert into season_i18n (season_id, lang_tag, name)
values (3, 'ca', 'baixa')
;
select lives_ok(
$$ select translate_season('87452b88-b48f-48d3-bb6c-0296de64164e', 'ca', 'Alta') $$,
'Should be able to translate the first season'
);
select lives_ok(
$$ select translate_season('9b6370f7-f941-46f2-bc6e-de455675bd0a', 'es', 'Baja') $$,
'Should be able to translate the second season'
);
select lives_ok(
$$ select translate_season('9b6370f7-f941-46f2-bc6e-de455675bd0a', 'ca', 'Baixa') $$,
'Should be able to overwrite the catalan translation of the second season'
);
select bag_eq(
$$ select slug::text, lang_tag, i18n.name from season_i18n as i18n join season using (season_id) $$,
$$ values ('87452b88-b48f-48d3-bb6c-0296de64164e', 'ca', 'Alta')
, ('9b6370f7-f941-46f2-bc6e-de455675bd0a', 'ca', 'Baixa')
, ('9b6370f7-f941-46f2-bc6e-de455675bd0a', 'es', 'Baja')
$$,
'Should have added and updated all translations.'
);
select *
from finish();
rollback;

11
verify/season_i18n.sql Normal file
View File

@ -0,0 +1,11 @@
-- Verify camper:season_i18n on pg
begin;
select season_id
, lang_tag
, name
from camper.season_i18n
where false;
rollback;

View File

@ -0,0 +1,7 @@
-- Verify camper:translate_season on pg
begin;
select has_function_privilege('camper.translate_season(uuid, text, text)', 'execute');
rollback;

View File

@ -48,7 +48,7 @@
<footer> <footer>
<button type="submit"><span class="sr-only">{{( pgettext "Cancel" "action" )}}</span></button> <button type="submit"><span class="sr-only">{{( pgettext "Cancel" "action" )}}</span></button>
{{ range .Seasons -}} {{ range .Seasons -}}
<button type="submit" name="season_id" value="{{ .Slug }}"> <button type="submit" name="season_id" value="{{ .ID }}">
<svg width="20px" height="20px"> <svg width="20px" height="20px">
<circle cx="50%" cy="50%" r="49%" fill="{{ .Color }}" stroke="#000" <circle cx="50%" cy="50%" r="49%" fill="{{ .Color }}" stroke="#000"
stroke-width=".5"/> stroke-width=".5"/>

View File

@ -16,6 +16,7 @@
<tr> <tr>
<th scope="col">{{( pgettext "Color" "header" )}}</th> <th scope="col">{{( pgettext "Color" "header" )}}</th>
<th scope="col">{{( pgettext "Name" "header" )}}</th> <th scope="col">{{( pgettext "Name" "header" )}}</th>
<th scope="col">{{( pgettext "Translations" "campsite type" )}}</th>
<th scope="col">{{( pgettext "Active" "season" )}}</th> <th scope="col">{{( pgettext "Active" "season" )}}</th>
</tr> </tr>
</thead> </thead>
@ -27,7 +28,14 @@
<circle cx="50%" cy="50%" r="49%" fill="{{ .Color }}" stroke="#000" stroke-width=".5"/> <circle cx="50%" cy="50%" r="49%" fill="{{ .Color }}" stroke="#000" stroke-width=".5"/>
</svg> </svg>
</td> </td>
<td><a href="/admin/seasons/{{ .Slug }}">{{ .Name }}</a></td> <td><a href="{{ .URL }}">{{ .Name }}</a></td>
<td>
{{ range .Translations }}
<a
{{ if .Missing }}class="missing-translation"{{ end }}
href="{{ .URL }}">{{ .Endonym }}</a>
{{ end }}
</td>
<td>{{ if .Active }}{{( gettext "Yes" )}}{{ else }}{{( gettext "No" )}}{{ end }}</td> <td>{{ if .Active }}{{( gettext "Yes" )}}{{ else }}{{( gettext "No" )}}{{ end }}</td>
</tr> </tr>
{{- end }} {{- end }}

View File

@ -0,0 +1,35 @@
<!--
SPDX-FileCopyrightText: 2023 jordi fita mas <jordi@tandem.blog>
SPDX-License-Identifier: AGPL-3.0-only
-->
{{ define "title" -}}
{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/season.seasonL10nForm*/ -}}
{{printf (pgettext "Translate Season to %s" "title") .Locale.Endonym }}
{{- end }}
{{ define "content" -}}
{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/season.seasonL10nForm*/ -}}
<form data-hx-put="/admin/seasons/{{ .Slug }}/{{ .Locale.Language }}">
<h2>
{{printf (pgettext "Translate Season to %s" "title") .Locale.Endonym }}
</h2>
{{ CSRFInput }}
<fieldset>
{{ with .Name -}}
<fieldset>
<legend>{{( pgettext "Name" "input")}}</legend>
{{( gettext "Source:" )}} {{ .Source }}<br>
<label>
{{( pgettext "Translation:" "input" )}}
<input type="text" name="{{ .Name }}" value="{{ .Val }}"
required {{ template "error-attrs" . }}><br>
</label>
{{ template "error-message" . }}
</fieldset>
{{- end }}
</fieldset>
<footer>
<button type="submit">{{( pgettext "Translate" "action" )}}</button>
</footer>
</form>
{{- end }}