From f48936f80046499a8498ea092ecdaee8f9049788 Mon Sep 17 00:00:00 2001 From: jordi fita mas Date: Tue, 12 Sep 2023 20:20:23 +0200 Subject: [PATCH] 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. --- demo/demo.sql | 12 +- deploy/campsite_type_i18n.sql | 23 ++++ pkg/app/admin.go | 5 +- pkg/app/app.go | 2 +- pkg/app/public.go | 11 +- pkg/campsite/admin.go | 4 +- pkg/campsite/types/admin.go | 84 +++++++++++++- pkg/campsite/types/l10n.go | 107 ++++++++++++++++++ pkg/campsite/types/public.go | 13 ++- pkg/template/page.go | 9 +- po/ca.po | 54 +++++++-- po/es.po | 54 +++++++-- revert/campsite_type_i18n.sql | 7 ++ sqitch.plan | 1 + test/add_campsite_type.sql | 8 +- test/campsite_type_i18n.sql | 49 ++++++++ verify/campsite_type_i18n.sql | 12 ++ web/static/camper.css | 4 + .../admin/campsite/type/index.gohtml | 12 +- web/templates/admin/campsite/type/l10n.gohtml | 49 ++++++++ 20 files changed, 478 insertions(+), 42 deletions(-) create mode 100644 deploy/campsite_type_i18n.sql create mode 100644 pkg/campsite/types/l10n.go create mode 100644 revert/campsite_type_i18n.sql create mode 100644 test/campsite_type_i18n.sql create mode 100644 verify/campsite_type_i18n.sql create mode 100644 web/templates/admin/campsite/type/l10n.gohtml diff --git a/demo/demo.sql b/demo/demo.sql index 295dedb..f24db0f 100644 --- a/demo/demo.sql +++ b/demo/demo.sql @@ -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) values (52, 'Parceŀles', 62, '') , (52, 'Safari Tents', 63, '') - , (52, 'Bungalows', 64, '') + , (52, 'Bungalous', 64, '') , (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; diff --git a/deploy/campsite_type_i18n.sql b/deploy/campsite_type_i18n.sql new file mode 100644 index 0000000..7f9b1ff --- /dev/null +++ b/deploy/campsite_type_i18n.sql @@ -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; diff --git a/pkg/app/admin.go b/pkg/app/admin.go index 20f641b..6847549 100644 --- a/pkg/app/admin.go +++ b/pkg/app/admin.go @@ -13,6 +13,7 @@ import ( "dev.tandem.ws/tandem/camper/pkg/company" "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/season" "dev.tandem.ws/tandem/camper/pkg/template" ) @@ -23,9 +24,9 @@ type adminHandler struct { season *season.AdminHandler } -func newAdminHandler() *adminHandler { +func newAdminHandler(locales locale.Locales) *adminHandler { return &adminHandler{ - campsite: campsite.NewAdminHandler(), + campsite: campsite.NewAdminHandler(locales), company: company.NewAdminHandler(), season: season.NewAdminHandler(), } diff --git a/pkg/app/app.go b/pkg/app/app.go index 2b1d545..4b00427 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -47,7 +47,7 @@ func New(db *database.DB, avatarsDir string, mediaDir string) (http.Handler, err db: db, fileHandler: static, profile: profile, - admin: newAdminHandler(), + admin: newAdminHandler(locales), public: newPublicHandler(), media: media, locales: locales, diff --git a/pkg/app/public.go b/pkg/app/public.go index 6f50441..25ed24a 100644 --- a/pkg/app/public.go +++ b/pkg/app/public.go @@ -65,7 +65,16 @@ type campsiteType struct { } 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 { panic(err) } diff --git a/pkg/campsite/admin.go b/pkg/campsite/admin.go index 902597a..17ef4dd 100644 --- a/pkg/campsite/admin.go +++ b/pkg/campsite/admin.go @@ -23,9 +23,9 @@ type AdminHandler struct { types *types.AdminHandler } -func NewAdminHandler() *AdminHandler { +func NewAdminHandler(locales locale.Locales) *AdminHandler { return &AdminHandler{ - types: &types.AdminHandler{}, + types: types.NewAdminHandler(locales), } } diff --git a/pkg/campsite/types/admin.go b/pkg/campsite/types/admin.go index f113064..c95c463 100644 --- a/pkg/campsite/types/admin.go +++ b/pkg/campsite/types/admin.go @@ -10,6 +10,10 @@ import ( "io" "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/database" "dev.tandem.ws/tandem/camper/pkg/form" @@ -20,6 +24,11 @@ import ( ) 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 { @@ -58,6 +67,18 @@ func (h *AdminHandler) Handler(user *auth.User, company *auth.Company, conn *dat } 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 { case http.MethodGet: f.MustRender(w, r, user, company) @@ -66,6 +87,29 @@ func (h *AdminHandler) Handler(user *auth.User, company *auth.Company, conn *dat default: 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 { - Slug string - Name string - Active bool + Slug string + Name string + 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) { - 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 { return nil, err } @@ -97,9 +163,17 @@ func collectTypeEntries(ctx context.Context, company *auth.Company, conn *databa var types []*typeEntry for rows.Next() { 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 } + 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) } diff --git a/pkg/campsite/types/l10n.go b/pkg/campsite/types/l10n.go new file mode 100644 index 0000000..5c43ea4 --- /dev/null +++ b/pkg/campsite/types/l10n.go @@ -0,0 +1,107 @@ +/* + * SPDX-FileCopyrightText: 2023 jordi fita mas + * 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 +} diff --git a/pkg/campsite/types/public.go b/pkg/campsite/types/public.go index 01939ba..72cfa13 100644 --- a/pkg/campsite/types/public.go +++ b/pkg/campsite/types/public.go @@ -31,7 +31,7 @@ func (h *PublicHandler) Handler(user *auth.User, company *auth.Company, conn *da http.NotFound(w, r) return } - page, err := newPublicPage(r.Context(), conn, head) + page, err := newPublicPage(r.Context(), user, conn, head) if database.ErrorIsNotFound(err) { http.NotFound(w, r) return @@ -51,11 +51,18 @@ type publicPage struct { 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{ 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 { return nil, err } diff --git a/pkg/template/page.go b/pkg/template/page.go index 76071a6..9005d8c 100644 --- a/pkg/template/page.go +++ b/pkg/template/page.go @@ -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 }) 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), } } diff --git a/po/ca.po b/po/ca.po index 0b31a51..a33a3ae 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-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" "Last-Translator: jordi fita mas \n" "Language-Team: Catalan \n" @@ -18,7 +18,7 @@ msgstr "" "Content-Transfer-Encoding: 8bit\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" msgid "Home" msgstr "Inici" @@ -69,7 +69,7 @@ msgid "Come and enjoy!" msgstr "Vine a gaudir!" #: 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" msgstr "Càmping Montagut" @@ -77,7 +77,7 @@ msgstr "Càmping Montagut" msgid "Skip to main content" msgstr "Salta al contingut principal" -#: web/templates/public/layout.gohtml:44 +#: web/templates/public/layout.gohtml:32 msgid "Singular Lodges" msgstr "Allotjaments singulars" @@ -150,13 +150,13 @@ msgid "Type" msgstr "Tipus" #: 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 msgid "Yes" msgstr "Sí" #: 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 msgid "No" msgstr "No" @@ -178,12 +178,13 @@ msgid "New Campsite Type" msgstr "Nou tipus d’allotjament" #: 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" msgid "Active" msgstr "Actiu" #: 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/profile.gohtml:26 msgctxt "input" @@ -196,6 +197,7 @@ msgid "Cover image" msgstr "Imatge de portada" #: web/templates/admin/campsite/type/form.gohtml:68 +#: web/templates/admin/campsite/type/l10n.gohtml:33 msgctxt "input" msgid "Description" msgstr "Descripció" @@ -218,10 +220,37 @@ msgctxt "header" msgid "Name" 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." msgstr "No s’ha afegit cap tipus d’allotjament 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 d’allotjament 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:26 msgctxt "title" @@ -434,7 +463,8 @@ msgctxt "language option" msgid "Automatic" 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." 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." msgstr "L’idioma 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." 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" msgstr "Accés prohibit" -#: pkg/campsite/types/admin.go:231 +#: pkg/campsite/types/admin.go:305 msgid "Cover image can not be empty." msgstr "No podeu deixar la imatge de portada en blanc." diff --git a/po/es.po b/po/es.po index bc46387..1dfec60 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-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" "Last-Translator: jordi fita mas \n" "Language-Team: Spanish \n" @@ -18,7 +18,7 @@ msgstr "" "Content-Transfer-Encoding: 8bit\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" msgid "Home" msgstr "Inicio" @@ -69,7 +69,7 @@ msgid "Come and enjoy!" msgstr "¡Ven a disfrutar!" #: 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" msgstr "Camping Montagut" @@ -77,7 +77,7 @@ msgstr "Camping Montagut" msgid "Skip to main content" msgstr "Saltar al contenido principal" -#: web/templates/public/layout.gohtml:44 +#: web/templates/public/layout.gohtml:32 msgid "Singular Lodges" msgstr "Alojamientos singulares" @@ -150,13 +150,13 @@ msgid "Type" msgstr "Tipo" #: 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 msgid "Yes" msgstr "Sí" #: 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 msgid "No" msgstr "No" @@ -178,12 +178,13 @@ msgid "New Campsite Type" msgstr "Nuevo tipo de alojamiento" #: 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" msgid "Active" msgstr "Activo" #: 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/profile.gohtml:26 msgctxt "input" @@ -196,6 +197,7 @@ msgid "Cover image" msgstr "Imagen de portada" #: web/templates/admin/campsite/type/form.gohtml:68 +#: web/templates/admin/campsite/type/l10n.gohtml:33 msgctxt "input" msgid "Description" msgstr "Descripción" @@ -218,10 +220,37 @@ msgctxt "header" msgid "Name" 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." 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:26 msgctxt "title" @@ -434,7 +463,8 @@ msgctxt "language option" msgid "Automatic" 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." 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." 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." 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" msgstr "Acceso prohibido" -#: pkg/campsite/types/admin.go:231 +#: pkg/campsite/types/admin.go:305 msgid "Cover image can not be empty." msgstr "No podéis dejar la imagen de portada en blanco." diff --git a/revert/campsite_type_i18n.sql b/revert/campsite_type_i18n.sql new file mode 100644 index 0000000..59da049 --- /dev/null +++ b/revert/campsite_type_i18n.sql @@ -0,0 +1,7 @@ +-- Revert camper:campsite_type_i18n from pg + +begin; + +drop table if exists camper.campsite_type_i18n; + +commit; diff --git a/sqitch.plan b/sqitch.plan index 2495afe..c127b6b 100644 --- a/sqitch.plan +++ b/sqitch.plan @@ -43,6 +43,7 @@ media_type [schema_camper] 2023-09-08T17:17:02Z jordi fita mas # Add relation of uploaded media add_media [roles schema_camper media media_type] 2023-09-08T17:40:28Z jordi fita mas # Add function to create media campsite_type [roles schema_camper company media user_profile] 2023-07-31T11:20:29Z jordi fita mas # Add relation of campsite type +campsite_type_i18n [roles schema_camper campsite_type language] 2023-09-12T10:31:29Z jordi fita mas # Add relation for campsite_type translations add_campsite_type [roles schema_camper campsite_type company] 2023-08-04T16:14:48Z jordi fita mas # Add function to create campsite types edit_campsite_type [roles schema_camper campsite_type company] 2023-08-07T22:21:34Z jordi fita mas # Add function to edit campsite types campsite [roles schema_camper company campsite_type user_profile] 2023-08-14T10:11:51Z jordi fita mas # Add campsite relation diff --git a/test/add_campsite_type.sql b/test/add_campsite_type.sql index 4b91d29..e09885b 100644 --- a/test/add_campsite_type.sql +++ b/test/add_campsite_type.sql @@ -7,7 +7,7 @@ begin; 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 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; +truncate campsite_type_i18n cascade; truncate campsite_type cascade; truncate media cascade; truncate company cascade; @@ -55,6 +56,11 @@ select bag_eq( '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 * from finish(); diff --git a/test/campsite_type_i18n.sql b/test/campsite_type_i18n.sql new file mode 100644 index 0000000..92996d4 --- /dev/null +++ b/test/campsite_type_i18n.sql @@ -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; + diff --git a/verify/campsite_type_i18n.sql b/verify/campsite_type_i18n.sql new file mode 100644 index 0000000..bfa49e1 --- /dev/null +++ b/verify/campsite_type_i18n.sql @@ -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; diff --git a/web/static/camper.css b/web/static/camper.css index d545fb6..929a0c5 100644 --- a/web/static/camper.css +++ b/web/static/camper.css @@ -44,3 +44,7 @@ p, h1, h2, h3, h4, h5, h6 { :any-link { color: #0000ff; } + +a.missing-translation { + color: #ff0000; +} diff --git a/web/templates/admin/campsite/type/index.gohtml b/web/templates/admin/campsite/type/index.gohtml index 0786213..4375893 100644 --- a/web/templates/admin/campsite/type/index.gohtml +++ b/web/templates/admin/campsite/type/index.gohtml @@ -16,13 +16,23 @@ {{( pgettext "Name" "header" )}} + {{( pgettext "Translations" "campsite type" )}} {{( pgettext "Active" "campsite type" )}} - {{ range .Types -}} + {{ range $type := .Types -}} {{ .Name }} + + {{ range .Translations }} + {{ .Endonym }} + {{ end }} + {{ if .Active }}{{( gettext "Yes" )}}{{ else }}{{( gettext "No" )}}{{ end }} {{- end }} diff --git a/web/templates/admin/campsite/type/l10n.gohtml b/web/templates/admin/campsite/type/l10n.gohtml new file mode 100644 index 0000000..a77178c --- /dev/null +++ b/web/templates/admin/campsite/type/l10n.gohtml @@ -0,0 +1,49 @@ + +{{ 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" }} +
+

+ {{printf (pgettext "Translate Campsite Type to %s" "title") .Locale.Endonym }} +

+ {{ CSRFInput }} +
+ {{ with .Name -}} +
+ {{( pgettext "Name" "input")}} + {{( gettext "Source:" )}} {{ .Source }}
+ + {{ template "error-message" . }} +
+ {{- end }} + {{ with .Description -}} +
+ + {{( gettext "Source:" )}}
+ {{ .Source | raw }}
+ + {{ template "error-message" . }} +
+ {{- end }} +
+ +
+{{- end }}