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:
parent
7d8cf5439b
commit
f48936f800
|
@ -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;
|
||||
|
|
|
@ -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;
|
|
@ -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(),
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
54
po/ca.po
54
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 <jordi@tandem.blog>\n"
|
||||
"Language-Team: Catalan <ca@dodds.net>\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."
|
||||
|
||||
|
|
54
po/es.po
54
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 <jordi@tandem.blog>\n"
|
||||
"Language-Team: Spanish <es@tp.org.es>\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."
|
||||
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
-- Revert camper:campsite_type_i18n from pg
|
||||
|
||||
begin;
|
||||
|
||||
drop table if exists camper.campsite_type_i18n;
|
||||
|
||||
commit;
|
|
@ -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
|
||||
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_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
|
||||
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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
@ -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;
|
|
@ -44,3 +44,7 @@ p, h1, h2, h3, h4, h5, h6 {
|
|||
:any-link {
|
||||
color: #0000ff;
|
||||
}
|
||||
|
||||
a.missing-translation {
|
||||
color: #ff0000;
|
||||
}
|
||||
|
|
|
@ -16,13 +16,23 @@
|
|||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{{( pgettext "Name" "header" )}}</th>
|
||||
<th scope="col">{{( pgettext "Translations" "campsite type" )}}</th>
|
||||
<th scope="col">{{( pgettext "Active" "campsite type" )}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ range .Types -}}
|
||||
{{ range $type := .Types -}}
|
||||
<tr>
|
||||
<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>
|
||||
</tr>
|
||||
{{- end }}
|
||||
|
|
|
@ -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 }}
|
Loading…
Reference in New Issue