Add internationalization and localization of campsite types

I am not happy with the localization interface for admins, but it is the
easier that i could think of (for me, i guess), with a separate for
each language.

I am not at all proud of the use of RecordArray, but i did not see the
need to create and register a type just to show the translation links.
I might change my mind when i need to add more and more translation
links, but only it the current interface remains, which i am not that
sure at the moment.
This commit is contained in:
jordi fita mas 2023-09-12 20:20:23 +02:00
parent 7d8cf5439b
commit f48936f800
20 changed files with 478 additions and 42 deletions

View File

@ -35,8 +35,18 @@ alter sequence campsite_type_campsite_type_id_seq restart with 72;
insert into campsite_type (company_id, name, media_id, description) insert into campsite_type (company_id, name, media_id, description)
values (52, 'Parceŀles', 62, '') values (52, 'Parceŀles', 62, '')
, (52, 'Safari Tents', 63, '') , (52, 'Safari Tents', 63, '')
, (52, 'Bungalows', 64, '') , (52, 'Bungalous', 64, '')
, (52, 'Cabanes de fusta', 65, '') , (52, 'Cabanes de fusta', 65, '')
; ;
insert into campsite_type_i18n (campsite_type_id, lang_tag, name, description)
values (72, 'en', 'Plots', '')
, (72, 'es', 'Parcelas', '')
, (73, 'en', 'Safari Tents', '')
, (73, 'es', 'Tiendas Safari', '')
, (74, 'en', 'Bungalows', '')
, (74, 'es', 'Bungalós', '')
, (75, 'en', 'Wooden Lodges', '')
, (75, 'es', 'Cabañas de madera', '')
;
commit; commit;

View File

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

View File

@ -13,6 +13,7 @@ import (
"dev.tandem.ws/tandem/camper/pkg/company" "dev.tandem.ws/tandem/camper/pkg/company"
"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/season" "dev.tandem.ws/tandem/camper/pkg/season"
"dev.tandem.ws/tandem/camper/pkg/template" "dev.tandem.ws/tandem/camper/pkg/template"
) )
@ -23,9 +24,9 @@ type adminHandler struct {
season *season.AdminHandler season *season.AdminHandler
} }
func newAdminHandler() *adminHandler { func newAdminHandler(locales locale.Locales) *adminHandler {
return &adminHandler{ return &adminHandler{
campsite: campsite.NewAdminHandler(), campsite: campsite.NewAdminHandler(locales),
company: company.NewAdminHandler(), company: company.NewAdminHandler(),
season: season.NewAdminHandler(), season: season.NewAdminHandler(),
} }

View File

@ -47,7 +47,7 @@ func New(db *database.DB, avatarsDir string, mediaDir string) (http.Handler, err
db: db, db: db,
fileHandler: static, fileHandler: static,
profile: profile, profile: profile,
admin: newAdminHandler(), admin: newAdminHandler(locales),
public: newPublicHandler(), public: newPublicHandler(),
media: media, media: media,
locales: locales, locales: locales,

View File

@ -65,7 +65,16 @@ type campsiteType struct {
} }
func mustCollectCampsiteTypes(ctx context.Context, company *auth.Company, conn *database.Conn, loc *locale.Locale) []*campsiteType { func mustCollectCampsiteTypes(ctx context.Context, company *auth.Company, conn *database.Conn, loc *locale.Locale) []*campsiteType {
rows, err := conn.Query(ctx, "select name, '/campsites/types/' || slug, '/media/' || encode(hash, 'hex') || '/' || original_filename from campsite_type join media using (media_id) where campsite_type.company_id = $1 and campsite_type.active", company.ID) rows, err := conn.Query(ctx, `
select coalesce(i18n.name, campsite_type.name) as l10_name
, '/campsites/types/' || slug
, '/media/' || encode(hash, 'hex') || '/' || original_filename
from campsite_type
left join campsite_type_i18n as i18n on campsite_type.campsite_type_id = i18n.campsite_type_id and lang_tag = $1
join media using (media_id)
where campsite_type.company_id = $2
and campsite_type.active
`, loc.Language, company.ID)
if err != nil { if err != nil {
panic(err) panic(err)
} }

View File

@ -23,9 +23,9 @@ type AdminHandler struct {
types *types.AdminHandler types *types.AdminHandler
} }
func NewAdminHandler() *AdminHandler { func NewAdminHandler(locales locale.Locales) *AdminHandler {
return &AdminHandler{ return &AdminHandler{
types: &types.AdminHandler{}, types: types.NewAdminHandler(locales),
} }
} }

View File

@ -10,6 +10,10 @@ import (
"io" "io"
"net/http" "net/http"
"github.com/jackc/pgtype"
"github.com/jackc/pgx/v4"
"golang.org/x/text/language"
"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"
"dev.tandem.ws/tandem/camper/pkg/form" "dev.tandem.ws/tandem/camper/pkg/form"
@ -20,6 +24,11 @@ import (
) )
type AdminHandler struct { type AdminHandler struct {
locales locale.Locales
}
func NewAdminHandler(locales locale.Locales) *AdminHandler {
return &AdminHandler{locales}
} }
func (h *AdminHandler) Handler(user *auth.User, company *auth.Company, conn *database.Conn) http.Handler { func (h *AdminHandler) Handler(user *auth.User, company *auth.Company, conn *database.Conn) http.Handler {
@ -58,6 +67,18 @@ func (h *AdminHandler) Handler(user *auth.User, company *auth.Company, conn *dat
} }
panic(err) panic(err)
} }
h.TypeHandler(user, company, conn, f).ServeHTTP(w, r)
}
})
}
func (h *AdminHandler) TypeHandler(user *auth.User, company *auth.Company, conn *database.Conn, f *typeForm) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var head string
head, r.URL.Path = httplib.ShiftPath(r.URL.Path)
switch head {
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)
@ -66,6 +87,29 @@ 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:
tag, err := language.Parse(head)
if err != nil {
http.NotFound(w, r)
return
}
loc, ok := h.locales[tag]
if !ok {
http.NotFound(w, r)
return
}
l10n := newTypeL10nForm(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:
editTypeL10n(w, r, user, company, conn, l10n)
default:
httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPut)
}
} }
}) })
} }
@ -82,13 +126,35 @@ func serveTypeIndex(w http.ResponseWriter, r *http.Request, user *auth.User, com
} }
type typeEntry struct { type typeEntry struct {
Slug string Slug string
Name string Name string
Active bool Active bool
Translations []*translation
}
type translation struct {
Language string
Endonym string
Missing bool
} }
func collectTypeEntries(ctx context.Context, company *auth.Company, conn *database.Conn) ([]*typeEntry, error) { func collectTypeEntries(ctx context.Context, company *auth.Company, conn *database.Conn) ([]*typeEntry, error) {
rows, err := conn.Query(ctx, "select slug, name, active from campsite_type where company_id = $1 order by name", company.ID) rows, err := conn.Query(ctx, `
select campsite_type.slug
, campsite_type.name
, campsite_type.active
, array_agg((lang_tag, endonym, not exists (select 1 from campsite_type_i18n as i18n where i18n.campsite_type_id = campsite_type.campsite_type_id and i18n.lang_tag = language.lang_tag)) order by endonym)
from campsite_type
join company using (company_id)
, language
where lang_tag <> default_lang_tag
and language.selectable
and campsite_type.company_id = $1
group by campsite_type.slug
, campsite_type.name
, campsite_type.active
order by name
`, pgx.QueryResultFormats{pgx.BinaryFormatCode}, company.ID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -97,9 +163,17 @@ func collectTypeEntries(ctx context.Context, company *auth.Company, conn *databa
var types []*typeEntry var types []*typeEntry
for rows.Next() { for rows.Next() {
entry := &typeEntry{} entry := &typeEntry{}
if err = rows.Scan(&entry.Slug, &entry.Name, &entry.Active); err != nil { var translations pgtype.RecordArray
if err = rows.Scan(&entry.Slug, &entry.Name, &entry.Active, &translations); err != nil {
return nil, err return nil, err
} }
for _, el := range translations.Elements {
entry.Translations = append(entry.Translations, &translation{
el.Fields[0].Get().(string),
el.Fields[1].Get().(string),
el.Fields[2].Get().(bool),
})
}
types = append(types, entry) types = append(types, entry)
} }

107
pkg/campsite/types/l10n.go Normal file
View File

@ -0,0 +1,107 @@
/*
* SPDX-FileCopyrightText: 2023 jordi fita mas <jfita@peritasoft.com>
* SPDX-License-Identifier: AGPL-3.0-only
*/
package types
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 L10nInput struct {
form.Input
Source string
}
type typeL10nForm struct {
Locale *locale.Locale
Slug string
Name *L10nInput
Description *L10nInput
}
func newTypeL10nForm(f *typeForm, loc *locale.Locale) *typeL10nForm {
return &typeL10nForm{
Locale: loc,
Slug: f.Slug,
Name: &L10nInput{
Input: form.Input{
Name: f.Name.Name,
},
Source: f.Name.Val,
},
Description: &L10nInput{
Input: form.Input{
Name: f.Description.Name,
},
Source: f.Description.Val,
},
}
}
func (l10n *typeL10nForm) FillFromDatabase(ctx context.Context, conn *database.Conn) error {
row := conn.QueryRow(ctx, `
select coalesce(i18n.name, '') as l10n_name
, coalesce(i18n.description, '') as l10n_description
from campsite_type
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
`, l10n.Locale.Language, l10n.Slug)
return row.Scan(&l10n.Name.Val, &l10n.Description.Val)
}
func (l10n *typeL10nForm) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
template.MustRenderAdmin(w, r, user, company, "campsite/type/l10n.gohtml", l10n)
}
func editTypeL10n(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, l10n *typeL10nForm) {
if err := l10n.Parse(r); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := user.VerifyCSRFToken(r); err != nil {
http.Error(w, err.Error(), http.StatusForbidden)
return
}
if !l10n.Valid(user.Locale) {
if !httplib.IsHTMxRequest(r) {
w.WriteHeader(http.StatusUnprocessableEntity)
}
l10n.MustRender(w, r, user, company)
return
}
conn.MustExec(r.Context(), `
insert into campsite_type_i18n (campsite_type_id, lang_tag, name, description)
select campsite_type_id, $1, $2, $3
from campsite_type
where slug = $4
on conflict (campsite_type_id, lang_tag) do update
set name = excluded.name
, description = excluded.description
`, l10n.Locale.Language, l10n.Name, l10n.Description, l10n.Slug)
httplib.Redirect(w, r, "/admin/campsites/types", http.StatusSeeOther)
}
func (l10n *typeL10nForm) Parse(r *http.Request) error {
if err := r.ParseForm(); err != nil {
return err
}
l10n.Name.FillValue(r)
l10n.Description.FillValue(r)
return nil
}
func (l10n *typeL10nForm) 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

@ -31,7 +31,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(), conn, head) page, err := newPublicPage(r.Context(), user, conn, head)
if database.ErrorIsNotFound(err) { if database.ErrorIsNotFound(err) {
http.NotFound(w, r) http.NotFound(w, r)
return return
@ -51,11 +51,18 @@ type publicPage struct {
Description gotemplate.HTML Description gotemplate.HTML
} }
func newPublicPage(ctx context.Context, conn *database.Conn, slug string) (*publicPage, error) { func newPublicPage(ctx context.Context, user *auth.User, conn *database.Conn, slug string) (*publicPage, error) {
page := &publicPage{ page := &publicPage{
PublicPage: template.NewPublicPage(), PublicPage: template.NewPublicPage(),
} }
row := conn.QueryRow(ctx, "select name, description::text from campsite_type where slug = $1 and active", slug) row := conn.QueryRow(ctx, `
select coalesce(i18n.name, campsite_type.name) as l10n_name
, coalesce(i18n.description, campsite_type.description)::text as l10n_description
from campsite_type
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)
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
} }

View File

@ -40,7 +40,14 @@ func (p *PublicPage) Setup(r *http.Request, user *auth.User, company *auth.Compa
sort.Slice(p.LocalizedAlternates, func(i, j int) bool { return p.LocalizedAlternates[i].Lang < p.LocalizedAlternates[j].Lang }) sort.Slice(p.LocalizedAlternates, func(i, j int) bool { return p.LocalizedAlternates[i].Lang < p.LocalizedAlternates[j].Lang })
p.Menu = &siteMenu{ p.Menu = &siteMenu{
CampsiteTypes: mustCollectMenuItems(r.Context(), conn, user.Locale, "select name, '/campsites/types/' || slug from campsite_type where company_id = $1 and active", company.ID), CampsiteTypes: mustCollectMenuItems(r.Context(), conn, user.Locale, `
select coalesce(i18n.name, campsite_type.name) as l10n_name
, '/campsites/types/' || slug
from campsite_type
left join campsite_type_i18n as i18n on campsite_type.campsite_type_id = i18n.campsite_type_id and i18n.lang_tag = $1
where company_id = $2
and active
`, user.Locale.Language, company.ID),
} }
} }

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-09-11 04:08+0200\n" "POT-Creation-Date: 2023-09-12 20:18+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"
@ -18,7 +18,7 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: web/templates/public/home.gohtml:6 web/templates/public/layout.gohtml:39 #: web/templates/public/home.gohtml:6 web/templates/public/layout.gohtml:28
msgctxt "title" msgctxt "title"
msgid "Home" msgid "Home"
msgstr "Inici" msgstr "Inici"
@ -69,7 +69,7 @@ msgid "Come and enjoy!"
msgstr "Vine a gaudir!" msgstr "Vine a gaudir!"
#: web/templates/public/layout.gohtml:11 web/templates/public/layout.gohtml:23 #: web/templates/public/layout.gohtml:11 web/templates/public/layout.gohtml:23
#: web/templates/public/layout.gohtml:60 #: web/templates/public/layout.gohtml:58
msgid "Campsite Montagut" msgid "Campsite Montagut"
msgstr "Càmping Montagut" msgstr "Càmping Montagut"
@ -77,7 +77,7 @@ msgstr "Càmping Montagut"
msgid "Skip to main content" msgid "Skip to main content"
msgstr "Salta al contingut principal" msgstr "Salta al contingut principal"
#: web/templates/public/layout.gohtml:44 #: web/templates/public/layout.gohtml:32
msgid "Singular Lodges" msgid "Singular Lodges"
msgstr "Allotjaments singulars" msgstr "Allotjaments singulars"
@ -150,13 +150,13 @@ msgid "Type"
msgstr "Tipus" msgstr "Tipus"
#: web/templates/admin/campsite/index.gohtml:28 #: web/templates/admin/campsite/index.gohtml:28
#: web/templates/admin/campsite/type/index.gohtml:26 #: web/templates/admin/campsite/type/index.gohtml:36
#: web/templates/admin/season/index.gohtml:32 #: web/templates/admin/season/index.gohtml:32
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:26 #: web/templates/admin/campsite/type/index.gohtml:36
#: web/templates/admin/season/index.gohtml:32 #: web/templates/admin/season/index.gohtml:32
msgid "No" msgid "No"
msgstr "No" msgstr "No"
@ -178,12 +178,13 @@ msgid "New Campsite Type"
msgstr "Nou tipus dallotjament" msgstr "Nou tipus dallotjament"
#: web/templates/admin/campsite/type/form.gohtml:39 #: web/templates/admin/campsite/type/form.gohtml:39
#: web/templates/admin/campsite/type/index.gohtml:19 #: web/templates/admin/campsite/type/index.gohtml:20
msgctxt "campsite type" msgctxt "campsite type"
msgid "Active" msgid "Active"
msgstr "Actiu" msgstr "Actiu"
#: web/templates/admin/campsite/type/form.gohtml:48 #: web/templates/admin/campsite/type/form.gohtml:48
#: web/templates/admin/campsite/type/l10n.gohtml:21
#: web/templates/admin/season/form.gohtml:47 #: web/templates/admin/season/form.gohtml:47
#: web/templates/admin/profile.gohtml:26 #: web/templates/admin/profile.gohtml:26
msgctxt "input" msgctxt "input"
@ -196,6 +197,7 @@ msgid "Cover image"
msgstr "Imatge de portada" msgstr "Imatge de portada"
#: web/templates/admin/campsite/type/form.gohtml:68 #: web/templates/admin/campsite/type/form.gohtml:68
#: web/templates/admin/campsite/type/l10n.gohtml:33
msgctxt "input" msgctxt "input"
msgid "Description" msgid "Description"
msgstr "Descripció" msgstr "Descripció"
@ -218,10 +220,37 @@ msgctxt "header"
msgid "Name" msgid "Name"
msgstr "Nom" msgstr "Nom"
#: web/templates/admin/campsite/type/index.gohtml:32 #: web/templates/admin/campsite/type/index.gohtml:19
msgctxt "campsite type"
msgid "Translations"
msgstr "Traduccions"
#: web/templates/admin/campsite/type/index.gohtml:42
msgid "No campsite types added yet." msgid "No campsite types added yet."
msgstr "No sha afegit cap tipus dallotjament encara." msgstr "No sha afegit cap tipus dallotjament encara."
#: web/templates/admin/campsite/type/l10n.gohtml:7
#: web/templates/admin/campsite/type/l10n.gohtml:15
msgctxt "title"
msgid "Translate Campsite Type to %s"
msgstr "Traducció del tipus dallotjament a %s"
#: web/templates/admin/campsite/type/l10n.gohtml:22
#: web/templates/admin/campsite/type/l10n.gohtml:34
msgid "Source:"
msgstr "Origen:"
#: web/templates/admin/campsite/type/l10n.gohtml:24
#: web/templates/admin/campsite/type/l10n.gohtml:37
msgctxt "input"
msgid "Translation:"
msgstr "Traducció:"
#: web/templates/admin/campsite/type/l10n.gohtml:46
msgctxt "action"
msgid "Translate"
msgstr "Tradueix"
#: web/templates/admin/season/form.gohtml:8 #: web/templates/admin/season/form.gohtml:8
#: web/templates/admin/season/form.gohtml:26 #: web/templates/admin/season/form.gohtml:26
msgctxt "title" msgctxt "title"
@ -434,7 +463,8 @@ msgctxt "language option"
msgid "Automatic" msgid "Automatic"
msgstr "Automàtic" msgstr "Automàtic"
#: pkg/app/user.go:249 pkg/campsite/types/admin.go:227 pkg/season/admin.go:203 #: pkg/app/user.go:249 pkg/campsite/types/l10n.go:105
#: pkg/campsite/types/admin.go:301 pkg/season/admin.go:203
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."
@ -446,15 +476,15 @@ msgstr "La confirmació no es correspon amb la contrasenya."
msgid "Selected language is not valid." msgid "Selected language is not valid."
msgstr "Lidioma escollit no és vàlid." msgstr "Lidioma escollit no és vàlid."
#: pkg/app/user.go:253 pkg/campsite/types/admin.go:229 #: pkg/app/user.go:253 pkg/campsite/types/admin.go:303
msgid "File must be a valid PNG or JPEG image." msgid "File must be a valid PNG or JPEG image."
msgstr "El fitxer has de ser una imatge PNG o JPEG vàlida." msgstr "El fitxer has de ser una imatge PNG o JPEG vàlida."
#: pkg/app/admin.go:43 #: pkg/app/admin.go:44
msgid "Access forbidden" msgid "Access forbidden"
msgstr "Accés prohibit" msgstr "Accés prohibit"
#: pkg/campsite/types/admin.go:231 #: pkg/campsite/types/admin.go:305
msgid "Cover image can not be empty." msgid "Cover image can not be empty."
msgstr "No podeu deixar la imatge de portada en blanc." msgstr "No podeu deixar la imatge de portada en blanc."

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-09-11 04:08+0200\n" "POT-Creation-Date: 2023-09-12 20:18+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"
@ -18,7 +18,7 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: web/templates/public/home.gohtml:6 web/templates/public/layout.gohtml:39 #: web/templates/public/home.gohtml:6 web/templates/public/layout.gohtml:28
msgctxt "title" msgctxt "title"
msgid "Home" msgid "Home"
msgstr "Inicio" msgstr "Inicio"
@ -69,7 +69,7 @@ msgid "Come and enjoy!"
msgstr "¡Ven a disfrutar!" msgstr "¡Ven a disfrutar!"
#: web/templates/public/layout.gohtml:11 web/templates/public/layout.gohtml:23 #: web/templates/public/layout.gohtml:11 web/templates/public/layout.gohtml:23
#: web/templates/public/layout.gohtml:60 #: web/templates/public/layout.gohtml:58
msgid "Campsite Montagut" msgid "Campsite Montagut"
msgstr "Camping Montagut" msgstr "Camping Montagut"
@ -77,7 +77,7 @@ msgstr "Camping Montagut"
msgid "Skip to main content" msgid "Skip to main content"
msgstr "Saltar al contenido principal" msgstr "Saltar al contenido principal"
#: web/templates/public/layout.gohtml:44 #: web/templates/public/layout.gohtml:32
msgid "Singular Lodges" msgid "Singular Lodges"
msgstr "Alojamientos singulares" msgstr "Alojamientos singulares"
@ -150,13 +150,13 @@ msgid "Type"
msgstr "Tipo" msgstr "Tipo"
#: web/templates/admin/campsite/index.gohtml:28 #: web/templates/admin/campsite/index.gohtml:28
#: web/templates/admin/campsite/type/index.gohtml:26 #: web/templates/admin/campsite/type/index.gohtml:36
#: web/templates/admin/season/index.gohtml:32 #: web/templates/admin/season/index.gohtml:32
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:26 #: web/templates/admin/campsite/type/index.gohtml:36
#: web/templates/admin/season/index.gohtml:32 #: web/templates/admin/season/index.gohtml:32
msgid "No" msgid "No"
msgstr "No" msgstr "No"
@ -178,12 +178,13 @@ msgid "New Campsite Type"
msgstr "Nuevo tipo de alojamiento" msgstr "Nuevo tipo de alojamiento"
#: web/templates/admin/campsite/type/form.gohtml:39 #: web/templates/admin/campsite/type/form.gohtml:39
#: web/templates/admin/campsite/type/index.gohtml:19 #: web/templates/admin/campsite/type/index.gohtml:20
msgctxt "campsite type" msgctxt "campsite type"
msgid "Active" msgid "Active"
msgstr "Activo" msgstr "Activo"
#: web/templates/admin/campsite/type/form.gohtml:48 #: web/templates/admin/campsite/type/form.gohtml:48
#: web/templates/admin/campsite/type/l10n.gohtml:21
#: web/templates/admin/season/form.gohtml:47 #: web/templates/admin/season/form.gohtml:47
#: web/templates/admin/profile.gohtml:26 #: web/templates/admin/profile.gohtml:26
msgctxt "input" msgctxt "input"
@ -196,6 +197,7 @@ msgid "Cover image"
msgstr "Imagen de portada" msgstr "Imagen de portada"
#: web/templates/admin/campsite/type/form.gohtml:68 #: web/templates/admin/campsite/type/form.gohtml:68
#: web/templates/admin/campsite/type/l10n.gohtml:33
msgctxt "input" msgctxt "input"
msgid "Description" msgid "Description"
msgstr "Descripción" msgstr "Descripción"
@ -218,10 +220,37 @@ msgctxt "header"
msgid "Name" msgid "Name"
msgstr "Nombre" msgstr "Nombre"
#: web/templates/admin/campsite/type/index.gohtml:32 #: web/templates/admin/campsite/type/index.gohtml:19
msgctxt "campsite type"
msgid "Translations"
msgstr "Traducciones"
#: web/templates/admin/campsite/type/index.gohtml:42
msgid "No campsite types added yet." msgid "No campsite types added yet."
msgstr "No se ha añadido ningún tipo de alojamiento todavía." msgstr "No se ha añadido ningún tipo de alojamiento todavía."
#: web/templates/admin/campsite/type/l10n.gohtml:7
#: web/templates/admin/campsite/type/l10n.gohtml:15
msgctxt "title"
msgid "Translate Campsite Type to %s"
msgstr "Traducción de tipo de alojamiento a %s"
#: web/templates/admin/campsite/type/l10n.gohtml:22
#: web/templates/admin/campsite/type/l10n.gohtml:34
msgid "Source:"
msgstr "Origen:"
#: web/templates/admin/campsite/type/l10n.gohtml:24
#: web/templates/admin/campsite/type/l10n.gohtml:37
msgctxt "input"
msgid "Translation:"
msgstr "Traducción"
#: web/templates/admin/campsite/type/l10n.gohtml:46
msgctxt "action"
msgid "Translate"
msgstr "Traducir"
#: web/templates/admin/season/form.gohtml:8 #: web/templates/admin/season/form.gohtml:8
#: web/templates/admin/season/form.gohtml:26 #: web/templates/admin/season/form.gohtml:26
msgctxt "title" msgctxt "title"
@ -434,7 +463,8 @@ msgctxt "language option"
msgid "Automatic" msgid "Automatic"
msgstr "Automático" msgstr "Automático"
#: pkg/app/user.go:249 pkg/campsite/types/admin.go:227 pkg/season/admin.go:203 #: pkg/app/user.go:249 pkg/campsite/types/l10n.go:105
#: pkg/campsite/types/admin.go:301 pkg/season/admin.go:203
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."
@ -446,15 +476,15 @@ msgstr "La confirmación no se corresponde con la contraseña."
msgid "Selected language is not valid." msgid "Selected language is not valid."
msgstr "El idioma escogido no es válido." msgstr "El idioma escogido no es válido."
#: pkg/app/user.go:253 pkg/campsite/types/admin.go:229 #: pkg/app/user.go:253 pkg/campsite/types/admin.go:303
msgid "File must be a valid PNG or JPEG image." msgid "File must be a valid PNG or JPEG image."
msgstr "El archivo tiene que ser una imagen PNG o JPEG válida." msgstr "El archivo tiene que ser una imagen PNG o JPEG válida."
#: pkg/app/admin.go:43 #: pkg/app/admin.go:44
msgid "Access forbidden" msgid "Access forbidden"
msgstr "Acceso prohibido" msgstr "Acceso prohibido"
#: pkg/campsite/types/admin.go:231 #: pkg/campsite/types/admin.go:305
msgid "Cover image can not be empty." msgid "Cover image can not be empty."
msgstr "No podéis dejar la imagen de portada en blanco." msgstr "No podéis dejar la imagen de portada en blanco."

View File

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

View File

@ -43,6 +43,7 @@ media_type [schema_camper] 2023-09-08T17:17:02Z jordi fita mas <jordi@tandem.blo
media [roles schema_camper company user_profile media_type] 2023-09-08T16:50:55Z jordi fita mas <jordi@tandem.blog> # Add relation of uploaded media media [roles schema_camper company user_profile media_type] 2023-09-08T16:50:55Z jordi fita mas <jordi@tandem.blog> # Add relation of uploaded media
add_media [roles schema_camper media media_type] 2023-09-08T17:40:28Z jordi fita mas <jordi@tandem.blog> # Add function to create media add_media [roles schema_camper media media_type] 2023-09-08T17:40:28Z jordi fita mas <jordi@tandem.blog> # Add function to create media
campsite_type [roles schema_camper company media user_profile] 2023-07-31T11:20:29Z jordi fita mas <jordi@tandem.blog> # Add relation of campsite type campsite_type [roles schema_camper company media user_profile] 2023-07-31T11:20:29Z jordi fita mas <jordi@tandem.blog> # Add relation of campsite type
campsite_type_i18n [roles schema_camper campsite_type language] 2023-09-12T10:31:29Z jordi fita mas <jordi@tandem.blog> # Add relation for campsite_type translations
add_campsite_type [roles schema_camper campsite_type company] 2023-08-04T16:14:48Z jordi fita mas <jordi@tandem.blog> # Add function to create campsite types add_campsite_type [roles schema_camper campsite_type company] 2023-08-04T16:14:48Z jordi fita mas <jordi@tandem.blog> # Add function to create campsite types
edit_campsite_type [roles schema_camper campsite_type company] 2023-08-07T22:21:34Z jordi fita mas <jordi@tandem.blog> # Add function to edit campsite types edit_campsite_type [roles schema_camper campsite_type company] 2023-08-07T22:21:34Z jordi fita mas <jordi@tandem.blog> # Add function to edit campsite types
campsite [roles schema_camper company campsite_type user_profile] 2023-08-14T10:11:51Z jordi fita mas <jordi@tandem.blog> # Add campsite relation campsite [roles schema_camper company campsite_type user_profile] 2023-08-14T10:11:51Z jordi fita mas <jordi@tandem.blog> # Add campsite relation

View File

@ -7,7 +7,7 @@ begin;
set search_path to camper, public; set search_path to camper, public;
select plan(12); select plan(13);
select has_function('camper', 'add_campsite_type', array ['integer', 'integer', 'text', 'text']); select has_function('camper', 'add_campsite_type', array ['integer', 'integer', 'text', 'text']);
select function_lang_is('camper', 'add_campsite_type', array ['integer', 'integer', 'text', 'text'], 'sql'); select function_lang_is('camper', 'add_campsite_type', array ['integer', 'integer', 'text', 'text'], 'sql');
@ -21,6 +21,7 @@ select function_privs_are('camper', 'add_campsite_type', array ['integer', 'inte
set client_min_messages to warning; set client_min_messages to warning;
truncate campsite_type_i18n cascade;
truncate campsite_type cascade; truncate campsite_type cascade;
truncate media cascade; truncate media cascade;
truncate company cascade; truncate company cascade;
@ -55,6 +56,11 @@ select bag_eq(
'Should have added all two campsite type' 'Should have added all two campsite type'
); );
select is_empty(
$$ select * from campsite_type_i18n $$,
'Should not have added any translation for campsite types.'
);
select * select *
from finish(); from finish();

View File

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

View File

@ -0,0 +1,12 @@
-- Verify camper:campsite_type_i18n on pg
begin;
select campsite_type_id
, lang_tag
, name
, description
from camper.campsite_type_i18n
where false;
rollback;

View File

@ -44,3 +44,7 @@ p, h1, h2, h3, h4, h5, h6 {
:any-link { :any-link {
color: #0000ff; color: #0000ff;
} }
a.missing-translation {
color: #ff0000;
}

View File

@ -16,13 +16,23 @@
<thead> <thead>
<tr> <tr>
<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" "campsite type" )}}</th> <th scope="col">{{( pgettext "Active" "campsite type" )}}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{{ range .Types -}} {{ range $type := .Types -}}
<tr> <tr>
<td><a href="/admin/campsites/types/{{ .Slug }}">{{ .Name }}</a></td> <td><a href="/admin/campsites/types/{{ .Slug }}">{{ .Name }}</a></td>
<td>
{{ range .Translations }}
<a
{{ if .Missing }}
class="missing-translation"
{{ end }}
href="/admin/campsites/types/{{ $type.Slug }}/{{ .Language }}">{{ .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,49 @@
<!--
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/campsite/types.typeL10nForm*/ -}}
{{printf (pgettext "Translate Campsite Type to %s" "title") .Locale.Endonym }}
{{- end }}
{{ define "content" -}}
{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/campsite/types.typeL10nForm*/ -}}
{{ template "settings-tabs" "campsiteTypes" }}
<form data-hx-put="/admin/campsites/types/{{ .Slug }}/{{ .Locale.Language }}">
<h2>
{{printf (pgettext "Translate Campsite Type 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 }}
{{ with .Description -}}
<fieldset>
<legend{{( pgettext "Description" "input")}}></legend>
{{( gettext "Source:" )}}<br>
{{ .Source | raw }}<br>
<label>
{{( pgettext "Translation:" "input" )}}
<textarea class="html"
name="{{ .Name }}" {{ template "error-attrs" . }}>{{ .Val }}</textarea><br>
</label>
{{ template "error-message" . }}
</fieldset>
{{- end }}
</fieldset>
<footer>
<button type="submit">{{( pgettext "Translate" "action" )}}</button>
</footer>
</form>
{{- end }}