Translate the strings the Go builders and templates hardcoded !17

merged merged by cmc on 2026-09-11 15:13 UTC · krz/skunky-art:feat/i18n-coverage into main

16 files changed, +131 −44

Layout: unified · split

.mailmap deleted −4
@@ -1,4 +0,0 @@
1lost+skunk <skunky@ebloid.ru> <skunky@macaw.me>
2lost+skunk <skunky@ebloid.ru> <me@lost-skunk.cc>
3lost+skunk <skunky@ebloid.ru> <skunky@noreply.git.macaw.me>
4lost+skunk <skunky@ebloid.ru> skunky <skunky@ebloid.ru>
app/cache.go +1 −1
@@ -234,7 +234,7 @@ func (s skunkyart) DownloadAndSendMedia(subdomain, path string) {
234 default: 234 default:
235 s.Writer.Header().Del("Cache-Control") 235 s.Writer.Header().Del("Cache-Control")
236 s.Writer.WriteHeader(403) 236 s.Writer.WriteHeader(403)
237 response = []byte("Sorry, butt proxy on this instance are disabled.") 237 response = []byte(esc(T(s.Lang, "error.proxy")))
238 } 238 }
239 239
240 _, _ = s.Writer.Write(response) 240 _, _ = s.Writer.Write(response)
app/escape_test.go +3 −3
@@ -43,9 +43,9 @@ func TestEveryPageTemplateRenders(t *testing.T) {
43 } 43 }
44 44
45 rec := httptest.NewRecorder() 45 rec := httptest.NewRecorder()
46 uri := "/" 46 s := skunkyart{Writer: rec, Host: "http://localhost", BasePath: "/", Lang: "en"}
47 skunkyart{Writer: rec}.ExecuteTemplate("index.htm", "html", &uri) 47 s.ExecuteTemplate("index.htm", "html", &s)
48 if rec.Code != 200 || !strings.Contains(rec.Body.String(), "</html>") { 48 if rec.Code != 200 || !strings.Contains(rec.Body.String(), "</html>") || !strings.Contains(rec.Body.String(), `lang="en"`) {
49 t.Errorf("index.htm: status %d, body %q", rec.Code, rec.Body.String()) 49 t.Errorf("index.htm: status %d, body %q", rec.Code, rec.Body.String())
50 } 50 }
51} 51}
app/i18n_test.go +47
@@ -116,3 +116,50 @@ func TestResolveLangReadsAcceptLanguage(t *testing.T) {
116 t.Errorf("pinned-but-missing = %q, want %q", got, DefaultLang) 116 t.Errorf("pinned-but-missing = %q, want %q", got, DefaultLang)
117 } 117 }
118} 118}
119
120// TestSpanishCatalogueMatchesEnglish keeps the two catalogues in step: a key
121// added to one and not the other falls back to English silently, which is
122// fine for a contributor's partial translation but not for the shipped one.
123func TestSpanishCatalogueMatchesEnglish(t *testing.T) {
124 read := func(name string) map[string]string {
125 body, err := os.ReadFile("../static/lang/" + name) //nolint:gosec // G304: the test reads the repository's own catalogues
126 if err != nil {
127 t.Fatal(err)
128 }
129 var c map[string]string
130 if err := json.Unmarshal(body, &c); err != nil {
131 t.Fatal(err)
132 }
133 return c
134 }
135 en, es := read("en.json"), read("es.json")
136 for k := range en {
137 if _, ok := es[k]; !ok {
138 t.Errorf("es.json lacks %q", k)
139 }
140 }
141 for k := range es {
142 if _, ok := en[k]; !ok {
143 t.Errorf("es.json has %q, which en.json does not", k)
144 }
145 }
146}
147
148// TestGoBuiltStringsAreInTheCatalogue lists every key the Go builders ask for,
149// so a typo in a T call shows up here rather than as the key on a page.
150func TestGoBuiltStringsAreInTheCatalogue(t *testing.T) {
151 src, err := os.ReadFile("../static/lang/en.json")
152 if err != nil {
153 t.Fatal(err)
154 }
155 var en map[string]string
156 if err := json.Unmarshal(src, &en); err != nil {
157 t.Fatal(err)
158 }
159 for _, key := range []string{"comments.failed", "comments.reply_to", "list.text", "nav.prev", "nav.next",
160 "gallery.folders", "gallery.content", "error.nsfw", "error.proxy", "error.upstream", "deviation.comments", "deviation.open"} {
161 if _, ok := en[key]; !ok {
162 t.Errorf("en.json lacks %q, which Go code asks for", key)
163 }
164 }
165}
app/parsers.go +9 −5
@@ -15,13 +15,15 @@ import (
15// fetch errored. 15// fetch errored.
16func (s skunkyart) ParseComments(c devianter.Comments, daError devianter.Error) string { 16func (s skunkyart) ParseComments(c devianter.Comments, daError devianter.Error) string {
17 if daError.RAW != nil { 17 if daError.RAW != nil {
18 return "Failed to fetch comments :(" 18 return esc(T(s.Lang, "comments.failed"))
19 } 19 }
20 20
21 var cmmts strings.Builder 21 var cmmts strings.Builder
22 replied := make(map[int]string) 22 replied := make(map[int]string)
23 23
24 cmmts.WriteString("<details><summary>Comments: <b>") 24 cmmts.WriteString("<details><summary>")
25 cmmts.WriteString(esc(T(s.Lang, "deviation.comments")))
26 cmmts.WriteString(": <b>")
25 cmmts.WriteString(strconv.Itoa(c.Total)) 27 cmmts.WriteString(strconv.Itoa(c.Total))
26 cmmts.WriteString("</b></summary>") 28 cmmts.WriteString("</b></summary>")
27 for _, x := range c.Thread { 29 for _, x := range c.Thread {
@@ -49,7 +51,9 @@ func (s skunkyart) ParseComments(c devianter.Comments, daError devianter.Error)
49 cmmts.WriteString("</b></a> ") 51 cmmts.WriteString("</b></a> ")
50 52
51 if x.Parent > 0 { 53 if x.Parent > 0 {
52 cmmts.WriteString(` In reply to <a href="`) 54 cmmts.WriteString(" ")
55 cmmts.WriteString(esc(T(s.Lang, "comments.reply_to")))
56 cmmts.WriteString(` <a href="`)
53 cmmts.WriteString(esc(s._pth)) 57 cmmts.WriteString(esc(s._pth))
54 cmmts.WriteString("#") 58 cmmts.WriteString("#")
55 cmmts.WriteString(strconv.Itoa(x.Parent)) 59 cmmts.WriteString(strconv.Itoa(x.Parent))
@@ -152,13 +156,13 @@ func (s skunkyart) DeviationList(devs []devianter.Deviation, allowAtom bool, con
152 } else { 156 } else {
153 listContent.WriteString(`<div class="block">`) 157 listContent.WriteString(`<div class="block">`)
154 if fullview != "" && preview != "" { 158 if fullview != "" && preview != "" {
155 listContent.WriteString(`<a title="open/download" href="`) 159 listContent.WriteString(`<a title="` + esc(T(s.Lang, "deviation.open")) + `" href="`)
156 listContent.WriteString(fullview) 160 listContent.WriteString(fullview)
157 listContent.WriteString(`"><img loading="lazy" src="`) 161 listContent.WriteString(`"><img loading="lazy" src="`)
158 listContent.WriteString(preview) 162 listContent.WriteString(preview)
159 listContent.WriteString(`" width="15%"></a>`) 163 listContent.WriteString(`" width="15%"></a>`)
160 } else { 164 } else {
161 listContent.WriteString(`<h1>[ TEXT ]</h1>`) 165 listContent.WriteString("<h1>[ " + esc(T(s.Lang, "list.text")) + " ]</h1>")
162 } 166 }
163 listContent.WriteString(`<br><a href="`) 167 listContent.WriteString(`<br><a href="`)
164 listContent.WriteString(postURL) 168 listContent.WriteString(postURL)
app/router.go +1 −1
@@ -146,7 +146,7 @@ func Handler() http.HandlerFunc {
146 switch skunky.Endpoint { 146 switch skunky.Endpoint {
147 // main 147 // main
148 case "": 148 case "":
149 skunky.ExecuteTemplate("index.htm", "html", &CFG.URI) 149 skunky.ExecuteTemplate("index.htm", "html", &skunky)
150 case "about": 150 case "about":
151 skunky.Templates.About = About 151 skunky.Templates.About = About
152 skunky.ExecuteTemplate("about.htm", "html", &skunky) 152 skunky.ExecuteTemplate("about.htm", "html", &skunky)
app/util.go +3 −3
@@ -233,7 +233,7 @@ func (s skunkyart) Error(dAerr devianter.Error) {
233 var msg strings.Builder 233 var msg strings.Builder
234 msg.WriteString(`<html><link rel="stylesheet" href="`) 234 msg.WriteString(`<html><link rel="stylesheet" href="`)
235 msg.WriteString(URLBuilder(s.Host, "stylesheet")) 235 msg.WriteString(URLBuilder(s.Host, "stylesheet"))
236 msg.WriteString(`" /><h3>DeviantArt error — '`) 236 msg.WriteString(`" /><h3>` + esc(T(s.Lang, "error.upstream")) + ` — '`)
237 msg.WriteString(esc(reason)) 237 msg.WriteString(esc(reason))
238 msg.WriteString("'</h3></html>") 238 msg.WriteString("'</h3></html>")
239 239
@@ -430,7 +430,7 @@ func (s skunkyart) NavBase(c DeviationList) string {
430 p := s.Page 430 p := s.Page
431 431
432 if p > 1 { 432 if p > 1 {
433 prevrev("<= Prev |", p-1, false) 433 prevrev("<= "+esc(T(s.Lang, "nav.prev"))+" |", p-1, false)
434 } else { 434 } else {
435 p = 1 435 p = 1
436 } 436 }
@@ -456,7 +456,7 @@ func (s skunkyart) NavBase(c DeviationList) string {
456 } 456 }
457 457
458 if c.More { 458 if c.More {
459 prevrev("| Next =>", p+1, false) 459 prevrev("| "+esc(T(s.Lang, "nav.next"))+" =>", p+1, false)
460 } 460 }
461 461
462 return list.String() 462 return list.String()
app/wrapper.go +3 −3
@@ -172,7 +172,7 @@ func (s skunkyart) GRUser() {
172 for _, x := range content.Content.Gruser.Page.Modules { 172 for _, x := range content.Content.Gruser.Page.Modules {
173 if len(x.ModuleData.Folders.Results) != 0 { 173 if len(x.ModuleData.Folders.Results) != 0 {
174 var folders strings.Builder 174 var folders strings.Builder
175 folders.WriteString(`<h1 id="folders"><a href="#folder">#</a> Folders</h1><div class="folders"><br>`) 175 folders.WriteString(`<h1 id="folders"><a href="#folders">#</a> ` + esc(T(s.Lang, "gallery.folders")) + `</h1><div class="folders"><br>`)
176 for _, x := range x.ModuleData.Folders.Results { 176 for _, x := range x.ModuleData.Folders.Results {
177 if x.FolderId != -1 && x.Size != 0 { 177 if x.FolderId != -1 && x.Size != 0 {
178 folders.WriteString(`<div class="block folder-item">`) 178 folders.WriteString(`<div class="block folder-item">`)
@@ -203,7 +203,7 @@ func (s skunkyart) GRUser() {
203 folders.WriteString("</div>") 203 folders.WriteString("</div>")
204 } 204 }
205 } 205 }
206 folders.WriteString(`</div><h1 id="content"><a href="#content">#</a> Content</h1>`) 206 folders.WriteString(`</div><h1 id="content"><a href="#content">#</a> ` + esc(T(s.Lang, "gallery.content")) + `</h1>`)
207 group.Gallery.Folders = template.HTML(folders.String()) //nolint:gosec // G203: escaped above 207 group.Gallery.Folders = template.HTML(folders.String()) //nolint:gosec // G203: escaped above
208 } 208 }
209 209
@@ -248,7 +248,7 @@ func (s skunkyart) Deviation(author, postname string) {
248 s.Writer.WriteHeader(403) 248 s.Writer.WriteHeader(403)
249 wr(s.Writer, `<html><link rel="stylesheet" href="`+ 249 wr(s.Writer, `<html><link rel="stylesheet" href="`+
250 URLBuilder(s.Host, "stylesheet")+ 250 URLBuilder(s.Host, "stylesheet")+
251 `" /><h1>NSFW content are disabled on this instance.</h1></html>`) 251 `" /><h1>`+esc(T(s.Lang, "error.nsfw"))+`</h1></html>`)
252 return 252 return
253 } 253 }
254 254
static/html/about.htm +2 −2
@@ -1,10 +1,10 @@
1<!DOCTYPE html> 1<!DOCTYPE html>
2<html lang="en"> 2<html lang="{{.Lang}}">
3 {{template "head" .}} 3 {{template "head" .}}
4 <main> 4 <main>
5 {{template "header" .}} 5 {{template "header" .}}
6 <p> 6 <p>
7 SkunkyArt is an alternative frontend for deviantart.com, written in Go. 7 {{T "about.blurb"}}
8 </p> 8 </p>
9 <h3><a href="https://github.com/krazywarez/skunky-art/issues" target="_blank">{{T "about.report"}}</a></h3> 9 <h3><a href="https://github.com/krazywarez/skunky-art/issues" target="_blank">{{T "about.report"}}</a></h3>
10 <b>{{T "about.settings"}}</b> 10 <b>{{T "about.settings"}}</b>
static/html/daily.htm +3 −3
@@ -1,13 +1,13 @@
1<!DOCTYPE html> 1<!DOCTYPE html>
2<html lang="en"> 2<html lang="{{.Lang}}">
3 {{template "head" . }} 3 {{template "head" . }}
4 <main> 4 <main>
5 {{template "header" . }} 5 {{template "header" . }}
6 {{if ne .Templates.DDStrips ""}} 6 {{if ne .Templates.DDStrips ""}}
7 <h2 id="strips"><a href="#strips">#</a> Strips</h2> 7 <h2 id="strips"><a href="#strips">#</a> {{T "daily.strips"}}</h2>
8 {{.Templates.DDStrips}} 8 {{.Templates.DDStrips}}
9 {{end}} 9 {{end}}
10 <h2 id="content"><a href="#content">#</a> Content</h2> 10 <h2 id="content"><a href="#content">#</a> {{T "gallery.content"}}</h2>
11 {{.Templates.SomeList}} 11 {{.Templates.SomeList}}
12 </main> 12 </main>
13</html>\ No newline at end of file 13</html>\ No newline at end of file
static/html/deviantion.htm +2 −2
@@ -1,5 +1,5 @@
1<!DOCTYPE html> 1<!DOCTYPE html>
2<html lang="en"> 2<html lang="{{.Lang}}">
3 {{template "head" . }} 3 {{template "head" . }}
4 <main> 4 <main>
5 {{template "header" . }} 5 {{template "header" . }}
@@ -19,7 +19,7 @@
19 {{if (ne .Templates.Deviation.Tags "")}} 19 {{if (ne .Templates.Deviation.Tags "")}}
20 {{.Templates.Deviation.Tags}}<br> 20 {{.Templates.Deviation.Tags}}<br>
21 {{end}} 21 {{end}}
22 <span>{{T "deviation.published"}}<strong>{{.Templates.Deviation.StringTime}}</strong>; Views: <strong>{{.Templates.Deviation.Post.Deviation.Stats.Views}}</strong>; Favourites: <strong>{{.Templates.Deviation.Post.Deviation.Stats.Favourites}}</strong>; Downloads: <strong>{{.Templates.Deviation.Post.Deviation.Stats.Downloads}}</strong> 22 <span>{{T "deviation.published"}}<strong>{{.Templates.Deviation.StringTime}}</strong>; {{T "deviation.views"}}: <strong>{{.Templates.Deviation.Post.Deviation.Stats.Views}}</strong>; {{T "nav.favourites"}}: <strong>{{.Templates.Deviation.Post.Deviation.Stats.Favourites}}</strong>; {{T "deviation.downloads"}}: <strong>{{.Templates.Deviation.Post.Deviation.Stats.Downloads}}</strong>
23 <br><a target="_blank" href="https://www.deviantart.com/{{.Templates.Deviation.Post.Deviation.Author.Username}}/art/art-{{.Templates.Deviation.Post.Deviation.ID}}">{{T "deviation.original"}}</a> 23 <br><a target="_blank" href="https://www.deviantart.com/{{.Templates.Deviation.Post.Deviation.Author.Username}}/art/art-{{.Templates.Deviation.Post.Deviation.ID}}">{{T "deviation.original"}}</a>
24 </span> 24 </span>
25 {{if (ne .Templates.Deviation.Description "")}} 25 {{if (ne .Templates.Deviation.Description "")}}
static/html/gruser.htm +6 −6
@@ -1,5 +1,5 @@
1<!DOCTYPE html> 1<!DOCTYPE html>
2<html lang="en"> 2<html lang="{{.Lang}}">
3 {{template "head" . }} 3 {{template "head" . }}
4 <main> 4 <main>
5 <header> 5 <header>
@@ -59,29 +59,29 @@
59 {{end}} 59 {{end}}
60 60
61 {{if ne .Templates.GroupUser.Admins ""}} 61 {{if ne .Templates.GroupUser.Admins ""}}
62 <h3 id="admins"><a href="#admins">#</a> Group admins</h3> 62 <h3 id="admins"><a href="#admins">#</a> {{T "user.admins"}}</h3>
63 <div class="plates admins"> 63 <div class="plates admins">
64 {{.Templates.GroupUser.Admins}} 64 {{.Templates.GroupUser.Admins}}
65 </div> 65 </div>
66 {{end}} 66 {{end}}
67 67
68 {{if ne .Templates.GroupUser.About.Interests ""}} 68 {{if ne .Templates.GroupUser.About.Interests ""}}
69 <h3 id="interests"><a href="#interests">#</a> Interests</h3> 69 <h3 id="interests"><a href="#interests">#</a> {{T "user.interests"}}</h3>
70 {{.Templates.GroupUser.About.Interests}} 70 {{.Templates.GroupUser.About.Interests}}
71 {{end}} 71 {{end}}
72 72
73 {{if ne .Templates.GroupUser.About.Social ""}} 73 {{if ne .Templates.GroupUser.About.Social ""}}
74 <h3 id="social"><a href="#social">#</a> Social Links</h3> 74 <h3 id="social"><a href="#social">#</a> {{T "user.social"}}</h3>
75 {{.Templates.GroupUser.About.Social}} 75 {{.Templates.GroupUser.About.Social}}
76 {{end}} 76 {{end}}
77 77
78 {{if ne .Templates.GroupUser.About.DescriptionFormatted ""}} 78 {{if ne .Templates.GroupUser.About.DescriptionFormatted ""}}
79 <h3 id="about"><a href="#about">#</a> About me</h3> 79 <h3 id="about"><a href="#about">#</a> {{T "user.about"}}</h3>
80 {{.Templates.GroupUser.About.DescriptionFormatted}} 80 {{.Templates.GroupUser.About.DescriptionFormatted}}
81 {{end}} 81 {{end}}
82 {{if ne .Templates.GroupUser.About.Comments ""}} 82 {{if ne .Templates.GroupUser.About.Comments ""}}
83 <br> 83 <br>
84 <h3 id="comments"><a href="#comments">#</a> Comments</h3> 84 <h3 id="comments"><a href="#comments">#</a> {{T "deviation.comments"}}</h3>
85 {{.Templates.GroupUser.About.Comments}} 85 {{.Templates.GroupUser.About.Comments}}
86 {{end}} 86 {{end}}
87 {{else}} 87 {{else}}
static/html/index.htm +6 −6
@@ -1,9 +1,9 @@
1<!DOCTYPE html> 1<!DOCTYPE html>
2<html lang="en"> 2<html lang="{{.Lang}}">
3 <head> 3 <head>
4 <title>SkunkyArt</title> 4 <title>SkunkyArt</title>
5 <link rel="stylesheet" href="{{.}}stylesheet"/> 5 <link rel="stylesheet" href="{{.BasePath}}stylesheet"/>
6 <link rel="icon" type="image/x-icon" href="{{.}}favicon.ico"> 6 <link rel="icon" type="image/x-icon" href="{{.BasePath}}favicon.ico">
7 <meta name="viewport" content="width=device-width, height=device-height, initial-scale=0.4"/> 7 <meta name="viewport" content="width=device-width, height=device-height, initial-scale=0.4"/>
8 8
9 <style> 9 <style>
@@ -70,10 +70,10 @@
70 </style> 70 </style>
71 </head> 71 </head>
72 <main> 72 <main>
73 <img src="{{.}}favicon.ico" title="{{T "index.logo"}}" alt="SkunkyArt logo" draggable="false"> 73 <img src="{{.BasePath}}favicon.ico" title="{{T "index.logo"}}" alt="SkunkyArt logo" draggable="false">
74 <div> 74 <div>
75 <h1><a href="{{.}}dd">{{T "nav.daily"}}</a> | <a href="{{.}}about">{{T "nav.about"}}</a></h1> 75 <h1><a href="{{.BasePath}}dd">{{T "nav.daily"}}</a> | <a href="{{.BasePath}}about">{{T "nav.about"}}</a></h1>
76 <form method="get" action="{{.}}search"> 76 <form method="get" action="{{.BasePath}}search">
77 <input type="text" name="q" aria-label="Search query" placeholder="{{T "search.placeholder"}}" autocomplete="off" autocapitalize="none" spellcheck="false"> 77 <input type="text" name="q" aria-label="Search query" placeholder="{{T "search.placeholder"}}" autocomplete="off" autocapitalize="none" spellcheck="false">
78 <select name="type" aria-label="Search type"> 78 <select name="type" aria-label="Search type">
79 <option value="all">{{T "search.all"}}</option> 79 <option value="all">{{T "search.all"}}</option>
static/html/search.htm +3 −3
@@ -1,16 +1,16 @@
1<!DOCTYPE html> 1<!DOCTYPE html>
2<html lang="en"> 2<html lang="{{.Lang}}">
3 {{template "head" . }} 3 {{template "head" . }}
4 <main> 4 <main>
5 {{template "header" . }} 5 {{template "header" . }}
6 6
7 {{if ne .Templates.Search.List ""}} 7 {{if ne .Templates.Search.List ""}}
8 {{if ne .Templates.Search.Content.Total 0}} 8 {{if ne .Templates.Search.Content.Total 0}}
9 <h1>Results by request '{{.QueryRaw}}': {{.Templates.Search.Content.Total}}</h1> 9 <h1>{{T "search.results"}} '{{.QueryRaw}}': {{.Templates.Search.Content.Total}}</h1>
10 {{end}} 10 {{end}}
11 {{.Templates.Search.List}} 11 {{.Templates.Search.List}}
12 {{else}} 12 {{else}}
13 <p>No results :(</p> 13 <p>{{T "search.none"}}</p>
14 {{end}} 14 {{end}}
15 </main> 15 </main>
16</html>\ No newline at end of file 16</html>\ No newline at end of file
static/lang/en.json +21 −1
@@ -43,5 +43,25 @@
43 "user.friends": "Friends", 43 "user.friends": "Friends",
44 "nav.daily": "Daily Deviations", 44 "nav.daily": "Daily Deviations",
45 "index.logo": "SkunkyArt logo", 45 "index.logo": "SkunkyArt logo",
46 "index.source": "Source Code" 46 "index.source": "Source Code",
47 "comments.failed": "Failed to fetch comments :(",
48 "comments.reply_to": "In reply to",
49 "list.text": "TEXT",
50 "nav.prev": "Prev",
51 "nav.next": "Next",
52 "gallery.folders": "Folders",
53 "gallery.content": "Content",
54 "daily.strips": "Strips",
55 "search.results": "Results for",
56 "search.none": "No results :(",
57 "user.admins": "Group admins",
58 "user.interests": "Interests",
59 "user.social": "Social links",
60 "user.about": "About me",
61 "deviation.views": "Views",
62 "deviation.downloads": "Downloads",
63 "about.blurb": "SkunkyArt is an alternative frontend for deviantart.com, written in Go.",
64 "error.nsfw": "NSFW content is disabled on this instance.",
65 "error.proxy": "Media proxying is disabled on this instance.",
66 "error.upstream": "DeviantArt error"
47} 67}
static/lang/es.json +21 −1
@@ -1,4 +1,5 @@
1{ 1{
2 "about.blurb": "SkunkyArt es una interfaz alternativa para deviantart.com, escrita en Go.",
2 "about.copyright": "Copyright ", 3 "about.copyright": "Copyright ",
3 "about.country": "País", 4 "about.country": "País",
4 "about.hideai": "Ocultar IA", 5 "about.hideai": "Ocultar IA",
@@ -10,35 +11,54 @@
10 "about.theme": "Tema", 11 "about.theme": "Tema",
11 "about.urls": "URLs", 12 "about.urls": "URLs",
12 "about.yes": "Sí", 13 "about.yes": "Sí",
14 "comments.failed": "No se pudieron cargar los comentarios :(",
15 "comments.reply_to": "En respuesta a",
13 "common.settings": "Ajustes", 16 "common.settings": "Ajustes",
17 "daily.strips": "Tiras",
18 "deviation.comments": "Comentarios",
14 "deviation.daily": "¡Desviación del día!", 19 "deviation.daily": "¡Desviación del día!",
15 "deviation.description": "Descripción", 20 "deviation.description": "Descripción",
21 "deviation.downloads": "Descargas",
16 "deviation.license": "Licencia", 22 "deviation.license": "Licencia",
17 "deviation.nsfw": "NSFW", 23 "deviation.nsfw": "NSFW",
18 "deviation.open": "abrir/descargar imagen", 24 "deviation.open": "abrir/descargar imagen",
19 "deviation.original": "Ir al original", 25 "deviation.original": "Ir al original",
20 "deviation.published": "Publicado: ", 26 "deviation.published": "Publicado: ",
21 "deviation.related": "Contenido relacionado", 27 "deviation.related": "Contenido relacionado",
22 "deviation.comments": "Comentarios", 28 "deviation.views": "Vistas",
29 "error.nsfw": "El contenido NSFW está desactivado en esta instancia.",
30 "error.proxy": "El proxy de medios está desactivado en esta instancia.",
31 "error.upstream": "Error de DeviantArt",
32 "gallery.content": "Contenido",
33 "gallery.folders": "Carpetas",
23 "index.logo": "Logo de SkunkyArt", 34 "index.logo": "Logo de SkunkyArt",
24 "index.source": "Código fuente", 35 "index.source": "Código fuente",
36 "list.text": "TEXTO",
25 "nav.about": "Acerca de", 37 "nav.about": "Acerca de",
26 "nav.daily": "Desviaciones del día", 38 "nav.daily": "Desviaciones del día",
27 "nav.dd": "DD", 39 "nav.dd": "DD",
28 "nav.favourites": "Favoritos", 40 "nav.favourites": "Favoritos",
29 "nav.gallery": "Galería", 41 "nav.gallery": "Galería",
30 "nav.home": "INICIO", 42 "nav.home": "INICIO",
43 "nav.next": "Siguiente",
44 "nav.prev": "Anterior",
31 "nav.rss": "RSS", 45 "nav.rss": "RSS",
32 "search.all": "Todo", 46 "search.all": "Todo",
33 "search.groups": "Grupos", 47 "search.groups": "Grupos",
48 "search.none": "Sin resultados :(",
34 "search.placeholder": "Buscar ...", 49 "search.placeholder": "Buscar ...",
50 "search.results": "Resultados para",
35 "search.submit": "¡Buscar!", 51 "search.submit": "¡Buscar!",
36 "search.tag": "Etiqueta", 52 "search.tag": "Etiqueta",
53 "user.about": "Sobre mí",
54 "user.admins": "Administradores del grupo",
37 "user.comments": "Comentarios", 55 "user.comments": "Comentarios",
38 "user.deviations": "Desviaciones", 56 "user.deviations": "Desviaciones",
39 "user.friends": "Amigos", 57 "user.friends": "Amigos",
58 "user.interests": "Intereses",
40 "user.pageviews": "Visitas", 59 "user.pageviews": "Visitas",
41 "user.registered": "Fecha de registro", 60 "user.registered": "Fecha de registro",
61 "user.social": "Enlaces sociales",
42 "user.statistics": "Estadísticas", 62 "user.statistics": "Estadísticas",
43 "user.tag": "Etiqueta del usuario", 63 "user.tag": "Etiqueta del usuario",
44 "user.uid": "UID", 64 "user.uid": "UID",