From 5f38ab8fd3f99628b0a9e85af1f8b7f01bfb9307 Mon Sep 17 00:00:00 2001 From: jordi fita mas Date: Tue, 3 Oct 2023 21:14:37 +0200 Subject: [PATCH] Add internationalization and localization to seasons --- demo/demo.sql | 9 +++ deploy/season_i18n.sql | 22 ++++++ deploy/translate_season.sql | 27 +++++++ pkg/app/admin.go | 2 +- pkg/campsite/types/public.go | 15 ++-- pkg/season/admin.go | 87 +++++++++++++++++----- pkg/season/l10n.go | 71 ++++++++++++++++++ po/ca.po | 65 +++++++++------- po/es.po | 65 +++++++++------- revert/season_i18n.sql | 7 ++ revert/translate_season.sql | 7 ++ sqitch.plan | 2 + test/season_i18n.sql | 44 +++++++++++ test/translate_season.sql | 68 +++++++++++++++++ verify/season_i18n.sql | 11 +++ verify/translate_season.sql | 7 ++ web/templates/admin/season/calendar.gohtml | 2 +- web/templates/admin/season/index.gohtml | 10 ++- web/templates/admin/season/l10n.gohtml | 35 +++++++++ 19 files changed, 472 insertions(+), 84 deletions(-) create mode 100644 deploy/season_i18n.sql create mode 100644 deploy/translate_season.sql create mode 100644 pkg/season/l10n.go create mode 100644 revert/season_i18n.sql create mode 100644 revert/translate_season.sql create mode 100644 test/season_i18n.sql create mode 100644 test/translate_season.sql create mode 100644 verify/season_i18n.sql create mode 100644 verify/translate_season.sql create mode 100644 web/templates/admin/season/l10n.gohtml diff --git a/demo/demo.sql b/demo/demo.sql index cff8cb0..2c528f1 100644 --- a/demo/demo.sql +++ b/demo/demo.sql @@ -242,6 +242,15 @@ select add_season(52, 'Temporada alta', '#ff926c'); select add_season(52, 'Temporada mitjana', '#ffe37f'); 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(94, '[2023-04-11, 2023-04-27]'); select set_season_range(93, '[2023-04-28, 2023-04-30]'); diff --git a/deploy/season_i18n.sql b/deploy/season_i18n.sql new file mode 100644 index 0000000..97befd8 --- /dev/null +++ b/deploy/season_i18n.sql @@ -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; diff --git a/deploy/translate_season.sql b/deploy/translate_season.sql new file mode 100644 index 0000000..26cdaed --- /dev/null +++ b/deploy/translate_season.sql @@ -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; diff --git a/pkg/app/admin.go b/pkg/app/admin.go index c1ec7a8..4fbf642 100644 --- a/pkg/app/admin.go +++ b/pkg/app/admin.go @@ -36,7 +36,7 @@ func newAdminHandler(locales locale.Locales, mediaDir string) *adminHandler { company: company.NewAdminHandler(), home: home.NewAdminHandler(locales), media: media.NewAdminHandler(mediaDir), - season: season.NewAdminHandler(), + season: season.NewAdminHandler(locales), services: services.NewAdminHandler(locales), } } diff --git a/pkg/campsite/types/public.go b/pkg/campsite/types/public.go index 12ec185..d3e92ac 100644 --- a/pkg/campsite/types/public.go +++ b/pkg/campsite/types/public.go @@ -13,6 +13,7 @@ import ( "dev.tandem.ws/tandem/camper/pkg/auth" "dev.tandem.ws/tandem/camper/pkg/database" 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/uuid" ) @@ -31,7 +32,7 @@ func (h *PublicHandler) Handler(user *auth.User, company *auth.Company, conn *da http.NotFound(w, r) return } - page, err := newPublicPage(r.Context(), user, conn, head) + page, err := newPublicPage(r.Context(), conn, user.Locale, head) if database.ErrorIsNotFound(err) { http.NotFound(w, r) return @@ -59,7 +60,7 @@ type typePrice struct { 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{ 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 where slug = $2 and active - `, user.Locale.Language, slug) + `, loc.Language, slug) if err := row.Scan(&page.Name, &page.Description); err != nil { return nil, err } 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) , coalesce(min_nights, 1) , to_price(coalesce(cost_per_night, 0))::text 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 ( - 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 where season.active - `, slug) + `, loc.Language, slug) if err != nil { return nil, err } diff --git a/pkg/season/admin.go b/pkg/season/admin.go index 5ab5e38..72fa925 100644 --- a/pkg/season/admin.go +++ b/pkg/season/admin.go @@ -15,6 +15,7 @@ import ( "time" "github.com/jackc/pgtype" + "github.com/jackc/pgx/v4" "dev.tandem.ws/tandem/camper/pkg/auth" "dev.tandem.ws/tandem/camper/pkg/database" @@ -28,10 +29,13 @@ import ( const unsetColor = 13750495 type AdminHandler struct { + locales locale.Locales } -func NewAdminHandler() *AdminHandler { - return &AdminHandler{} +func NewAdminHandler(locales locale.Locales) *AdminHandler { + return &AdminHandler{ + locales: locales, + } } func (h *AdminHandler) Handler(user *auth.User, company *auth.Company, conn *database.Conn) http.HandlerFunc { @@ -79,13 +83,37 @@ func (h *AdminHandler) Handler(user *auth.User, company *auth.Company, conn *dat } panic(err) } - switch r.Method { - case http.MethodGet: - f.MustRender(w, r, user, company) - case http.MethodPut: - editSeason(w, r, user, company, conn, f) + var langTag string + langTag, r.URL.Path = httplib.ShiftPath(r.URL.Path) + + switch langTag { + case "": + switch r.Method { + case http.MethodGet: + f.MustRender(w, r, user, company) + case http.MethodPut: + editSeason(w, r, user, company, conn, f) + default: + httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPut) + } default: - httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPut) + 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) { rows, err := conn.Query(ctx, ` - select slug - , name + select '/admin/seasons/' || season.slug + , season.name , to_color(color)::text , 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 - where company_id = $1 - order by name`, company.ID) + join company using (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 { return nil, err } @@ -135,9 +172,17 @@ func collectSeasonEntries(ctx context.Context, company *auth.Company, conn *data var seasons []*seasonEntry for rows.Next() { 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 } + 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) } @@ -145,10 +190,12 @@ func collectSeasonEntries(ctx context.Context, company *auth.Company, conn *data } type seasonEntry struct { - Slug string - Name string - Color string - Active bool + ID int + URL string + Name string + Color string + Active bool + Translations []*locale.Translation } 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 { rows, err := conn.Query(ctx, ` - select '0' as slug + select 0 as season_id , $1 as name , to_color($2)::text , true , 0 as sort union all - select season_id::text + select season_id , name , to_color(color)::text , active @@ -420,7 +467,7 @@ func mustCollectCalendarSeasons(ctx context.Context, company *auth.Company, conn for rows.Next() { entry := &seasonEntry{} 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) } seasons = append(seasons, entry) diff --git a/pkg/season/l10n.go b/pkg/season/l10n.go new file mode 100644 index 0000000..6f61dec --- /dev/null +++ b/pkg/season/l10n.go @@ -0,0 +1,71 @@ +/* + * SPDX-FileCopyrightText: 2023 jordi fita mas + * 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 +} diff --git a/po/ca.po b/po/ca.po index eef19bb..91a651e 100644 --- a/po/ca.po +++ b/po/ca.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: camper\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" "Last-Translator: jordi fita mas \n" "Language-Team: Catalan \n" @@ -205,6 +205,7 @@ msgstr "Traducció de la diapositiva del carrusel a %s" #: web/templates/admin/carousel/l10n.gohtml:21 #: web/templates/admin/campsite/type/l10n.gohtml:21 #: 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:33 msgid "Source:" @@ -213,6 +214,7 @@ msgstr "Origen:" #: web/templates/admin/carousel/l10n.gohtml:23 #: web/templates/admin/campsite/type/l10n.gohtml:23 #: 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:36 msgctxt "input" @@ -221,6 +223,7 @@ msgstr "Traducció:" #: web/templates/admin/carousel/l10n.gohtml:32 #: web/templates/admin/campsite/type/l10n.gohtml:45 +#: web/templates/admin/season/l10n.gohtml:32 #: web/templates/admin/services/l10n.gohtml:45 msgctxt "action" msgid "Translate" @@ -282,13 +285,13 @@ msgstr "Tipus" #: web/templates/admin/campsite/index.gohtml:28 #: web/templates/admin/campsite/type/index.gohtml:35 -#: web/templates/admin/season/index.gohtml:31 +#: web/templates/admin/season/index.gohtml:39 msgid "Yes" msgstr "Sí" #: web/templates/admin/campsite/index.gohtml:28 #: web/templates/admin/campsite/type/index.gohtml:35 -#: web/templates/admin/season/index.gohtml:31 +#: web/templates/admin/season/index.gohtml:39 msgid "No" msgstr "No" @@ -317,6 +320,7 @@ msgstr "Actiu" #: web/templates/admin/campsite/type/form.gohtml:46 #: web/templates/admin/campsite/type/l10n.gohtml:20 #: 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/l10n.gohtml:20 #: web/templates/admin/profile.gohtml:26 @@ -371,6 +375,7 @@ msgid "Name" msgstr "Nom" #: 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:60 #: web/templates/admin/home/index.gohtml:19 @@ -401,7 +406,7 @@ msgid "New Season" msgstr "Nova temporada" #: web/templates/admin/season/form.gohtml:37 -#: web/templates/admin/season/index.gohtml:19 +#: web/templates/admin/season/index.gohtml:20 msgctxt "season" msgid "Active" msgstr "Activa" @@ -428,15 +433,21 @@ msgctxt "header" msgid "Color" msgstr "Color" -#: web/templates/admin/season/index.gohtml:37 +#: web/templates/admin/season/index.gohtml:45 msgid "No seasons added yet." msgstr "No s’ha afegit cap temporada encara." -#: web/templates/admin/season/index.gohtml:40 +#: web/templates/admin/season/index.gohtml:48 msgctxt "title" msgid "Calendar" 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 msgctxt "day" msgid "Mon" @@ -836,8 +847,8 @@ msgid "Automatic" msgstr "Automàtic" #: pkg/app/user.go:249 pkg/campsite/types/l10n.go:82 -#: pkg/campsite/types/admin.go:387 pkg/season/admin.go:335 -#: pkg/services/l10n.go:73 pkg/services/admin.go:266 +#: pkg/campsite/types/admin.go:387 pkg/season/l10n.go:69 +#: pkg/season/admin.go:382 pkg/services/l10n.go:73 pkg/services/admin.go:266 msgid "Name can not be empty." msgstr "No podeu deixar el nom en blanc." @@ -923,92 +934,92 @@ msgstr "El tipus d’allotjament escollit no és vàlid." msgid "Label can not be empty." msgstr "No podeu deixar l’etiqueta en blanc." -#: pkg/season/admin.go:155 +#: pkg/season/admin.go:202 msgctxt "month" msgid "January" msgstr "gener" -#: pkg/season/admin.go:156 +#: pkg/season/admin.go:203 msgctxt "month" msgid "February" msgstr "febrer" -#: pkg/season/admin.go:157 +#: pkg/season/admin.go:204 msgctxt "month" msgid "March" msgstr "març" -#: pkg/season/admin.go:158 +#: pkg/season/admin.go:205 msgctxt "month" msgid "April" msgstr "abril" -#: pkg/season/admin.go:159 +#: pkg/season/admin.go:206 msgctxt "month" msgid "May" msgstr "maig" -#: pkg/season/admin.go:160 +#: pkg/season/admin.go:207 msgctxt "month" msgid "June" msgstr "juny" -#: pkg/season/admin.go:161 +#: pkg/season/admin.go:208 msgctxt "month" msgid "July" msgstr "juliol" -#: pkg/season/admin.go:162 +#: pkg/season/admin.go:209 msgctxt "month" msgid "August" msgstr "agost" -#: pkg/season/admin.go:163 +#: pkg/season/admin.go:210 msgctxt "month" msgid "September" msgstr "setembre" -#: pkg/season/admin.go:164 +#: pkg/season/admin.go:211 msgctxt "month" msgid "October" msgstr "octubre" -#: pkg/season/admin.go:165 +#: pkg/season/admin.go:212 msgctxt "month" msgid "November" msgstr "novembre" -#: pkg/season/admin.go:166 +#: pkg/season/admin.go:213 msgctxt "month" msgid "December" msgstr "desembre" -#: pkg/season/admin.go:336 +#: pkg/season/admin.go:383 msgid "Color can not be empty." 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." 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" msgid "Unset" msgstr "Desassigna" -#: pkg/season/admin.go:444 +#: pkg/season/admin.go:491 msgid "Start date can not be empty." msgstr "No podeu deixar la data d’inici en blanc." -#: pkg/season/admin.go:445 +#: pkg/season/admin.go:492 msgid "Start date must be a valid date." msgstr "La data d’inici ha de ser una data vàlida." -#: pkg/season/admin.go:447 +#: pkg/season/admin.go:494 msgid "End date can not be empty." 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." msgstr "La data de fi ha de ser una data vàlida." diff --git a/po/es.po b/po/es.po index 712252a..e67a9ce 100644 --- a/po/es.po +++ b/po/es.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: camper\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" "Last-Translator: jordi fita mas \n" "Language-Team: Spanish \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/campsite/type/l10n.gohtml:21 #: 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:33 msgid "Source:" @@ -213,6 +214,7 @@ msgstr "Origen:" #: web/templates/admin/carousel/l10n.gohtml:23 #: web/templates/admin/campsite/type/l10n.gohtml:23 #: 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:36 msgctxt "input" @@ -221,6 +223,7 @@ msgstr "Traducción" #: web/templates/admin/carousel/l10n.gohtml:32 #: web/templates/admin/campsite/type/l10n.gohtml:45 +#: web/templates/admin/season/l10n.gohtml:32 #: web/templates/admin/services/l10n.gohtml:45 msgctxt "action" msgid "Translate" @@ -282,13 +285,13 @@ msgstr "Tipo" #: web/templates/admin/campsite/index.gohtml:28 #: web/templates/admin/campsite/type/index.gohtml:35 -#: web/templates/admin/season/index.gohtml:31 +#: web/templates/admin/season/index.gohtml:39 msgid "Yes" msgstr "Sí" #: web/templates/admin/campsite/index.gohtml:28 #: web/templates/admin/campsite/type/index.gohtml:35 -#: web/templates/admin/season/index.gohtml:31 +#: web/templates/admin/season/index.gohtml:39 msgid "No" msgstr "No" @@ -317,6 +320,7 @@ msgstr "Activo" #: web/templates/admin/campsite/type/form.gohtml:46 #: web/templates/admin/campsite/type/l10n.gohtml:20 #: 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/l10n.gohtml:20 #: web/templates/admin/profile.gohtml:26 @@ -371,6 +375,7 @@ msgid "Name" msgstr "Nombre" #: 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:60 #: web/templates/admin/home/index.gohtml:19 @@ -401,7 +406,7 @@ msgid "New Season" msgstr "Nueva temporada" #: web/templates/admin/season/form.gohtml:37 -#: web/templates/admin/season/index.gohtml:19 +#: web/templates/admin/season/index.gohtml:20 msgctxt "season" msgid "Active" msgstr "Activa" @@ -428,15 +433,21 @@ msgctxt "header" msgid "Color" msgstr "Color" -#: web/templates/admin/season/index.gohtml:37 +#: web/templates/admin/season/index.gohtml:45 msgid "No seasons added yet." 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" msgid "Calendar" 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 msgctxt "day" msgid "Mon" @@ -836,8 +847,8 @@ msgid "Automatic" msgstr "Automático" #: pkg/app/user.go:249 pkg/campsite/types/l10n.go:82 -#: pkg/campsite/types/admin.go:387 pkg/season/admin.go:335 -#: pkg/services/l10n.go:73 pkg/services/admin.go:266 +#: pkg/campsite/types/admin.go:387 pkg/season/l10n.go:69 +#: pkg/season/admin.go:382 pkg/services/l10n.go:73 pkg/services/admin.go:266 msgid "Name can not be empty." 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." msgstr "No podéis dejar la etiqueta en blanco." -#: pkg/season/admin.go:155 +#: pkg/season/admin.go:202 msgctxt "month" msgid "January" msgstr "enero" -#: pkg/season/admin.go:156 +#: pkg/season/admin.go:203 msgctxt "month" msgid "February" msgstr "febrero" -#: pkg/season/admin.go:157 +#: pkg/season/admin.go:204 msgctxt "month" msgid "March" msgstr "marzo" -#: pkg/season/admin.go:158 +#: pkg/season/admin.go:205 msgctxt "month" msgid "April" msgstr "abril" -#: pkg/season/admin.go:159 +#: pkg/season/admin.go:206 msgctxt "month" msgid "May" msgstr "mayo" -#: pkg/season/admin.go:160 +#: pkg/season/admin.go:207 msgctxt "month" msgid "June" msgstr "junio" -#: pkg/season/admin.go:161 +#: pkg/season/admin.go:208 msgctxt "month" msgid "July" msgstr "julio" -#: pkg/season/admin.go:162 +#: pkg/season/admin.go:209 msgctxt "month" msgid "August" msgstr "agosto" -#: pkg/season/admin.go:163 +#: pkg/season/admin.go:210 msgctxt "month" msgid "September" msgstr "septiembre" -#: pkg/season/admin.go:164 +#: pkg/season/admin.go:211 msgctxt "month" msgid "October" msgstr "octubre" -#: pkg/season/admin.go:165 +#: pkg/season/admin.go:212 msgctxt "month" msgid "November" msgstr "noviembre" -#: pkg/season/admin.go:166 +#: pkg/season/admin.go:213 msgctxt "month" msgid "December" msgstr "diciembre" -#: pkg/season/admin.go:336 +#: pkg/season/admin.go:383 msgid "Color can not be empty." 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." 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" msgid "Unset" msgstr "Desasignar" -#: pkg/season/admin.go:444 +#: pkg/season/admin.go:491 msgid "Start date can not be empty." 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." 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." 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." msgstr "La fecha final tiene que ser una fecha válida." diff --git a/revert/season_i18n.sql b/revert/season_i18n.sql new file mode 100644 index 0000000..e128109 --- /dev/null +++ b/revert/season_i18n.sql @@ -0,0 +1,7 @@ +-- Revert camper:season_i18n from pg + +begin; + +drop table if exists camper.season_i18n; + +commit; diff --git a/revert/translate_season.sql b/revert/translate_season.sql new file mode 100644 index 0000000..003e760 --- /dev/null +++ b/revert/translate_season.sql @@ -0,0 +1,7 @@ +-- Revert camper:translate_season from pg + +begin; + +drop function if exists camper.translate_season(uuid, text, text); + +commit; diff --git a/sqitch.plan b/sqitch.plan index 5d5b6c4..9049e97 100644 --- a/sqitch.plan +++ b/sqitch.plan @@ -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 # Add function to format cents to prices to_price [roles schema_camper] 2023-10-01T16:30:40Z jordi fita mas # 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 # 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 # Add relation for season translations +translate_season [roles schema_camper season season_i18n] 2023-10-03T18:37:19Z jordi fita mas # Add function to translate seasons diff --git a/test/season_i18n.sql b/test/season_i18n.sql new file mode 100644 index 0000000..0a3844a --- /dev/null +++ b/test/season_i18n.sql @@ -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; + diff --git a/test/translate_season.sql b/test/translate_season.sql new file mode 100644 index 0000000..e912bb4 --- /dev/null +++ b/test/translate_season.sql @@ -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; diff --git a/verify/season_i18n.sql b/verify/season_i18n.sql new file mode 100644 index 0000000..02d8ae6 --- /dev/null +++ b/verify/season_i18n.sql @@ -0,0 +1,11 @@ +-- Verify camper:season_i18n on pg + +begin; + +select season_id + , lang_tag + , name +from camper.season_i18n +where false; + +rollback; diff --git a/verify/translate_season.sql b/verify/translate_season.sql new file mode 100644 index 0000000..4e3b920 --- /dev/null +++ b/verify/translate_season.sql @@ -0,0 +1,7 @@ +-- Verify camper:translate_season on pg + +begin; + +select has_function_privilege('camper.translate_season(uuid, text, text)', 'execute'); + +rollback; diff --git a/web/templates/admin/season/calendar.gohtml b/web/templates/admin/season/calendar.gohtml index 2b44d5b..e331ffd 100644 --- a/web/templates/admin/season/calendar.gohtml +++ b/web/templates/admin/season/calendar.gohtml @@ -48,7 +48,7 @@ + +{{- end }}