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 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]');

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(),
home: home.NewAdminHandler(locales),
media: media.NewAdminHandler(mediaDir),
season: season.NewAdminHandler(),
season: season.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/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
}

View File

@ -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,6 +83,11 @@ func (h *AdminHandler) Handler(user *auth.User, company *auth.Company, conn *dat
}
panic(err)
}
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)
@ -87,6 +96,25 @@ func (h *AdminHandler) Handler(user *auth.User, company *auth.Company, conn *dat
default:
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) {
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
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)

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 ""
"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 <jordi@tandem.blog>\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/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 sha 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 dallotjament escollit no és vàlid."
msgid "Label can not be empty."
msgstr "No podeu deixar letiqueta 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 dinici en blanc."
#: pkg/season/admin.go:445
#: pkg/season/admin.go:492
msgid "Start date must be a valid date."
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."
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."

View File

@ -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 <jordi@tandem.blog>\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/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."

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

View File

@ -16,6 +16,7 @@
<tr>
<th scope="col">{{( pgettext "Color" "header" )}}</th>
<th scope="col">{{( pgettext "Name" "header" )}}</th>
<th scope="col">{{( pgettext "Translations" "campsite type" )}}</th>
<th scope="col">{{( pgettext "Active" "season" )}}</th>
</tr>
</thead>
@ -27,7 +28,14 @@
<circle cx="50%" cy="50%" r="49%" fill="{{ .Color }}" stroke="#000" stroke-width=".5"/>
</svg>
</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>
</tr>
{{- 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 }}