Commit 7bdc11dec1

7bdc11dec17903d518de056270a59e966e4c32bf

parent: f3e8d80307

Verified · cmc ci/build: success ci/lint: success ci/test: success

cmc <hello@cleberg.net> · 2026-09-11 14:33 UTC

Translate the strings the Go builders and templates hardcoded

Comment headers, pagination labels, folder and content headings, the
text placeholder, search headings, profile section headings, post
statistics, the about blurb and the NSFW, proxy and upstream error
pages now come from the catalogues. Every page declares the resolved
language in its lang attribute; the index page takes the request
struct like the others. Twenty new keys in both catalogues, with a test
that keeps them in step.

Closes #24

Layout: unified · split

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",