Commit 7bdc11dec1
Verified · cmc ci/build: success ci/lint: success ci/test: success
Layout: unified · split
app/cache.go +1 −1
| @@ -234,7 +234,7 @@ func (s skunkyart) DownloadAndSendMedia(subdomain, path string) { | ||
| 234 | 234 | default: |
| 235 | 235 | s.Writer.Header().Del("Cache-Control") |
| 236 | 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 | 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 | 45 | rec := httptest.NewRecorder() |
| 46 | uri := "/" | |
| 47 | skunkyart{Writer: rec}.ExecuteTemplate("index.htm", "html", &uri) | |
| 48 | if rec.Code != 200 || !strings.Contains(rec.Body.String(), "</html>") { | |
| 46 | s := skunkyart{Writer: rec, Host: "http://localhost", BasePath: "/", Lang: "en"} | |
| 47 | s.ExecuteTemplate("index.htm", "html", &s) | |
| 48 | if rec.Code != 200 || !strings.Contains(rec.Body.String(), "</html>") || !strings.Contains(rec.Body.String(), `lang="en"`) { | |
| 49 | 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 | 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. | |
| 123 | func 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. | |
| 150 | func 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 | 15 | // fetch errored. |
| 16 | 16 | func (s skunkyart) ParseComments(c devianter.Comments, daError devianter.Error) string { |
| 17 | 17 | if daError.RAW != nil { |
| 18 | return "Failed to fetch comments :(" | |
| 18 | return esc(T(s.Lang, "comments.failed")) | |
| 19 | 19 | } |
| 20 | 20 | |
| 21 | 21 | var cmmts strings.Builder |
| 22 | 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 | 27 | cmmts.WriteString(strconv.Itoa(c.Total)) |
| 26 | 28 | cmmts.WriteString("</b></summary>") |
| 27 | 29 | for _, x := range c.Thread { |
| @@ -49,7 +51,9 @@ func (s skunkyart) ParseComments(c devianter.Comments, daError devianter.Error) | ||
| 49 | 51 | cmmts.WriteString("</b></a> ") |
| 50 | 52 | |
| 51 | 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 | 57 | cmmts.WriteString(esc(s._pth)) |
| 54 | 58 | cmmts.WriteString("#") |
| 55 | 59 | cmmts.WriteString(strconv.Itoa(x.Parent)) |
| @@ -152,13 +156,13 @@ func (s skunkyart) DeviationList(devs []devianter.Deviation, allowAtom bool, con | ||
| 152 | 156 | } else { |
| 153 | 157 | listContent.WriteString(`<div class="block">`) |
| 154 | 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 | 160 | listContent.WriteString(fullview) |
| 157 | 161 | listContent.WriteString(`"><img loading="lazy" src="`) |
| 158 | 162 | listContent.WriteString(preview) |
| 159 | 163 | listContent.WriteString(`" width="15%"></a>`) |
| 160 | 164 | } else { |
| 161 | listContent.WriteString(`<h1>[ TEXT ]</h1>`) | |
| 165 | listContent.WriteString("<h1>[ " + esc(T(s.Lang, "list.text")) + " ]</h1>") | |
| 162 | 166 | } |
| 163 | 167 | listContent.WriteString(`<br><a href="`) |
| 164 | 168 | listContent.WriteString(postURL) |
app/router.go +1 −1
| @@ -146,7 +146,7 @@ func Handler() http.HandlerFunc { | ||
| 146 | 146 | switch skunky.Endpoint { |
| 147 | 147 | // main |
| 148 | 148 | case "": |
| 149 | skunky.ExecuteTemplate("index.htm", "html", &CFG.URI) | |
| 149 | skunky.ExecuteTemplate("index.htm", "html", &skunky) | |
| 150 | 150 | case "about": |
| 151 | 151 | skunky.Templates.About = About |
| 152 | 152 | skunky.ExecuteTemplate("about.htm", "html", &skunky) |
app/util.go +3 −3
| @@ -233,7 +233,7 @@ func (s skunkyart) Error(dAerr devianter.Error) { | ||
| 233 | 233 | var msg strings.Builder |
| 234 | 234 | msg.WriteString(`<html><link rel="stylesheet" href="`) |
| 235 | 235 | msg.WriteString(URLBuilder(s.Host, "stylesheet")) |
| 236 | msg.WriteString(`" /><h3>DeviantArt error — '`) | |
| 236 | msg.WriteString(`" /><h3>` + esc(T(s.Lang, "error.upstream")) + ` — '`) | |
| 237 | 237 | msg.WriteString(esc(reason)) |
| 238 | 238 | msg.WriteString("'</h3></html>") |
| 239 | 239 | |
| @@ -430,7 +430,7 @@ func (s skunkyart) NavBase(c DeviationList) string { | ||
| 430 | 430 | p := s.Page |
| 431 | 431 | |
| 432 | 432 | if p > 1 { |
| 433 | prevrev("<= Prev |", p-1, false) | |
| 433 | prevrev("<= "+esc(T(s.Lang, "nav.prev"))+" |", p-1, false) | |
| 434 | 434 | } else { |
| 435 | 435 | p = 1 |
| 436 | 436 | } |
| @@ -456,7 +456,7 @@ func (s skunkyart) NavBase(c DeviationList) string { | ||
| 456 | 456 | } |
| 457 | 457 | |
| 458 | 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 | 462 | return list.String() |
app/wrapper.go +3 −3
| @@ -172,7 +172,7 @@ func (s skunkyart) GRUser() { | ||
| 172 | 172 | for _, x := range content.Content.Gruser.Page.Modules { |
| 173 | 173 | if len(x.ModuleData.Folders.Results) != 0 { |
| 174 | 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 | 176 | for _, x := range x.ModuleData.Folders.Results { |
| 177 | 177 | if x.FolderId != -1 && x.Size != 0 { |
| 178 | 178 | folders.WriteString(`<div class="block folder-item">`) |
| @@ -203,7 +203,7 @@ func (s skunkyart) GRUser() { | ||
| 203 | 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 | 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 | 248 | s.Writer.WriteHeader(403) |
| 249 | 249 | wr(s.Writer, `<html><link rel="stylesheet" href="`+ |
| 250 | 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 | 252 | return |
| 253 | 253 | } |
| 254 | 254 | |
static/html/about.htm +2 −2
| @@ -1,10 +1,10 @@ | ||
| 1 | 1 | <!DOCTYPE html> |
| 2 | <html lang="en"> | |
| 2 | <html lang="{{.Lang}}"> | |
| 3 | 3 | {{template "head" .}} |
| 4 | 4 | <main> |
| 5 | 5 | {{template "header" .}} |
| 6 | 6 | <p> |
| 7 | SkunkyArt is an alternative frontend for deviantart.com, written in Go. | |
| 7 | {{T "about.blurb"}} | |
| 8 | 8 | </p> |
| 9 | 9 | <h3><a href="https://github.com/krazywarez/skunky-art/issues" target="_blank">{{T "about.report"}}</a></h3> |
| 10 | 10 | <b>{{T "about.settings"}}</b> |
static/html/daily.htm +3 −3
| @@ -1,13 +1,13 @@ | ||
| 1 | 1 | <!DOCTYPE html> |
| 2 | <html lang="en"> | |
| 2 | <html lang="{{.Lang}}"> | |
| 3 | 3 | {{template "head" . }} |
| 4 | 4 | <main> |
| 5 | 5 | {{template "header" . }} |
| 6 | 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 | 8 | {{.Templates.DDStrips}} |
| 9 | 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 | 11 | {{.Templates.SomeList}} |
| 12 | 12 | </main> |
| 13 | 13 | </html> |
| \ No newline at end of file | ||
static/html/deviantion.htm +2 −2
| @@ -1,5 +1,5 @@ | ||
| 1 | 1 | <!DOCTYPE html> |
| 2 | <html lang="en"> | |
| 2 | <html lang="{{.Lang}}"> | |
| 3 | 3 | {{template "head" . }} |
| 4 | 4 | <main> |
| 5 | 5 | {{template "header" . }} |
| @@ -19,7 +19,7 @@ | ||
| 19 | 19 | {{if (ne .Templates.Deviation.Tags "")}} |
| 20 | 20 | {{.Templates.Deviation.Tags}}<br> |
| 21 | 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 | 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 | 24 | </span> |
| 25 | 25 | {{if (ne .Templates.Deviation.Description "")}} |
static/html/gruser.htm +6 −6
| @@ -1,5 +1,5 @@ | ||
| 1 | 1 | <!DOCTYPE html> |
| 2 | <html lang="en"> | |
| 2 | <html lang="{{.Lang}}"> | |
| 3 | 3 | {{template "head" . }} |
| 4 | 4 | <main> |
| 5 | 5 | <header> |
| @@ -59,29 +59,29 @@ | ||
| 59 | 59 | {{end}} |
| 60 | 60 | |
| 61 | 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 | 63 | <div class="plates admins"> |
| 64 | 64 | {{.Templates.GroupUser.Admins}} |
| 65 | 65 | </div> |
| 66 | 66 | {{end}} |
| 67 | 67 | |
| 68 | 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 | 70 | {{.Templates.GroupUser.About.Interests}} |
| 71 | 71 | {{end}} |
| 72 | 72 | |
| 73 | 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 | 75 | {{.Templates.GroupUser.About.Social}} |
| 76 | 76 | {{end}} |
| 77 | 77 | |
| 78 | 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 | 80 | {{.Templates.GroupUser.About.DescriptionFormatted}} |
| 81 | 81 | {{end}} |
| 82 | 82 | {{if ne .Templates.GroupUser.About.Comments ""}} |
| 83 | 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 | 85 | {{.Templates.GroupUser.About.Comments}} |
| 86 | 86 | {{end}} |
| 87 | 87 | {{else}} |
static/html/index.htm +6 −6
| @@ -1,9 +1,9 @@ | ||
| 1 | 1 | <!DOCTYPE html> |
| 2 | <html lang="en"> | |
| 2 | <html lang="{{.Lang}}"> | |
| 3 | 3 | <head> |
| 4 | 4 | <title>SkunkyArt</title> |
| 5 | <link rel="stylesheet" href="{{.}}stylesheet"/> | |
| 6 | <link rel="icon" type="image/x-icon" href="{{.}}favicon.ico"> | |
| 5 | <link rel="stylesheet" href="{{.BasePath}}stylesheet"/> | |
| 6 | <link rel="icon" type="image/x-icon" href="{{.BasePath}}favicon.ico"> | |
| 7 | 7 | <meta name="viewport" content="width=device-width, height=device-height, initial-scale=0.4"/> |
| 8 | 8 | |
| 9 | 9 | <style> |
| @@ -70,10 +70,10 @@ | ||
| 70 | 70 | </style> |
| 71 | 71 | </head> |
| 72 | 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 | 74 | <div> |
| 75 | <h1><a href="{{.}}dd">{{T "nav.daily"}}</a> | <a href="{{.}}about">{{T "nav.about"}}</a></h1> | |
| 76 | <form method="get" action="{{.}}search"> | |
| 75 | <h1><a href="{{.BasePath}}dd">{{T "nav.daily"}}</a> | <a href="{{.BasePath}}about">{{T "nav.about"}}</a></h1> | |
| 76 | <form method="get" action="{{.BasePath}}search"> | |
| 77 | 77 | <input type="text" name="q" aria-label="Search query" placeholder="{{T "search.placeholder"}}" autocomplete="off" autocapitalize="none" spellcheck="false"> |
| 78 | 78 | <select name="type" aria-label="Search type"> |
| 79 | 79 | <option value="all">{{T "search.all"}}</option> |
static/html/search.htm +3 −3
| @@ -1,16 +1,16 @@ | ||
| 1 | 1 | <!DOCTYPE html> |
| 2 | <html lang="en"> | |
| 2 | <html lang="{{.Lang}}"> | |
| 3 | 3 | {{template "head" . }} |
| 4 | 4 | <main> |
| 5 | 5 | {{template "header" . }} |
| 6 | 6 | |
| 7 | 7 | {{if ne .Templates.Search.List ""}} |
| 8 | 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 | 10 | {{end}} |
| 11 | 11 | {{.Templates.Search.List}} |
| 12 | 12 | {{else}} |
| 13 | <p>No results :(</p> | |
| 13 | <p>{{T "search.none"}}</p> | |
| 14 | 14 | {{end}} |
| 15 | 15 | </main> |
| 16 | 16 | </html> |
| \ No newline at end of file | ||
static/lang/en.json +21 −1
| @@ -43,5 +43,25 @@ | ||
| 43 | 43 | "user.friends": "Friends", |
| 44 | 44 | "nav.daily": "Daily Deviations", |
| 45 | 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 | 3 | "about.copyright": "Copyright ", |
| 3 | 4 | "about.country": "País", |
| 4 | 5 | "about.hideai": "Ocultar IA", |
| @@ -10,35 +11,54 @@ | ||
| 10 | 11 | "about.theme": "Tema", |
| 11 | 12 | "about.urls": "URLs", |
| 12 | 13 | "about.yes": "Sí", |
| 14 | "comments.failed": "No se pudieron cargar los comentarios :(", | |
| 15 | "comments.reply_to": "En respuesta a", | |
| 13 | 16 | "common.settings": "Ajustes", |
| 17 | "daily.strips": "Tiras", | |
| 18 | "deviation.comments": "Comentarios", | |
| 14 | 19 | "deviation.daily": "¡Desviación del día!", |
| 15 | 20 | "deviation.description": "Descripción", |
| 21 | "deviation.downloads": "Descargas", | |
| 16 | 22 | "deviation.license": "Licencia", |
| 17 | 23 | "deviation.nsfw": "NSFW", |
| 18 | 24 | "deviation.open": "abrir/descargar imagen", |
| 19 | 25 | "deviation.original": "Ir al original", |
| 20 | 26 | "deviation.published": "Publicado: ", |
| 21 | 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 | 34 | "index.logo": "Logo de SkunkyArt", |
| 24 | 35 | "index.source": "Código fuente", |
| 36 | "list.text": "TEXTO", | |
| 25 | 37 | "nav.about": "Acerca de", |
| 26 | 38 | "nav.daily": "Desviaciones del día", |
| 27 | 39 | "nav.dd": "DD", |
| 28 | 40 | "nav.favourites": "Favoritos", |
| 29 | 41 | "nav.gallery": "Galería", |
| 30 | 42 | "nav.home": "INICIO", |
| 43 | "nav.next": "Siguiente", | |
| 44 | "nav.prev": "Anterior", | |
| 31 | 45 | "nav.rss": "RSS", |
| 32 | 46 | "search.all": "Todo", |
| 33 | 47 | "search.groups": "Grupos", |
| 48 | "search.none": "Sin resultados :(", | |
| 34 | 49 | "search.placeholder": "Buscar ...", |
| 50 | "search.results": "Resultados para", | |
| 35 | 51 | "search.submit": "¡Buscar!", |
| 36 | 52 | "search.tag": "Etiqueta", |
| 53 | "user.about": "Sobre mí", | |
| 54 | "user.admins": "Administradores del grupo", | |
| 37 | 55 | "user.comments": "Comentarios", |
| 38 | 56 | "user.deviations": "Desviaciones", |
| 39 | 57 | "user.friends": "Amigos", |
| 58 | "user.interests": "Intereses", | |
| 40 | 59 | "user.pageviews": "Visitas", |
| 41 | 60 | "user.registered": "Fecha de registro", |
| 61 | "user.social": "Enlaces sociales", | |
| 42 | 62 | "user.statistics": "Estadísticas", |
| 43 | 63 | "user.tag": "Etiqueta del usuario", |
| 44 | 64 | "user.uid": "UID", |