add translations

This commit is contained in:
Cadey Ratio 2018-10-31 23:10:04 +00:00
parent b749388104
commit 24d63f4c98
15 changed files with 327 additions and 69 deletions

View File

@ -19,31 +19,41 @@ func logTemplateTime(name string, from time.Time) {
func (s *Site) renderTemplatePage(templateFname string, data interface{}) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer logTemplateTime(templateFname, time.Now())
s.tlock.RLock()
defer s.tlock.RUnlock()
var t *template.Template
getTranslation := func(group, key string, vals ...interface{}) string {
var lc locale
var ok bool
locale, err := GetPreferredLocale(r)
if err != nil {
ln.Error(r.Context(), err)
lc = s.t.locales["en"]
goto overwith
}
lc, ok = s.t.Get(locale.Lang)
if !ok {
ln.Log(r.Context(), ln.F{"msg": "unknown language requested", "lang": locale.Lang, "header": r.Header.Get("Accept-Language")})
lc = s.t.locales["en"]
goto overwith
}
overwith:
return lc.Value(group, key, vals...)
}
funcMap := template.FuncMap{
"trans": getTranslation,
}
var t *template.Template = template.New(templateFname).Funcs(funcMap)
var err error
if s.templates[templateFname] == nil {
t, err = template.ParseFiles("templates/base.html", "templates/"+templateFname)
t, err = t.ParseFiles("templates/base.html", "templates/"+templateFname)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
ln.Error(context.Background(), err, ln.F{"action": "renderTemplatePage", "page": templateFname})
fmt.Fprintf(w, "error: %v", err)
}
ln.Log(context.Background(), ln.F{"action": "loaded_new_template", "fname": templateFname})
s.tlock.RUnlock()
s.tlock.Lock()
s.templates[templateFname] = t
s.tlock.Unlock()
s.tlock.RLock()
} else {
t = s.templates[templateFname]
}
err = t.Execute(w, data)
if err != nil {
panic(err)

46
cmd/site/i18n.go Normal file
View File

@ -0,0 +1,46 @@
package main
import (
"encoding/json"
"fmt"
"io"
)
type locale map[string]map[string]string
func (l locale) Value(group, key string, args ...interface{}) string {
sg, ok := l[group]
if !ok {
return "no group " + group
}
result, ok := sg[key]
if !ok {
return fmt.Sprintf("in group %s, no key %s", group, key)
}
return fmt.Sprintf(result, args...)
}
type translations struct {
locales map[string]locale
}
func (t *translations) LoadLocale(name string, r io.Reader) error {
l := locale{}
d := json.NewDecoder(r)
err := d.Decode(&l)
if err == nil {
t.locales[name] = l
}
return err
}
func (t *translations) Get(name string) (locale, bool) {
l, ok := t.locales[name]
if !ok {
return nil, false
}
return l, ok
}

97
cmd/site/locale.go Normal file
View File

@ -0,0 +1,97 @@
package main
import (
"errors"
"net/http"
"strconv"
"strings"
)
// Locale is locale value from the Accept-Language header in request
type Locale struct {
Lang, Country string
Qual float64
}
// Name returns the locale value in 'lang' or 'lang_country' format
// eg: de_DE, en_US, gb
func (l *Locale) Name() string {
if len(l.Country) > 0 {
return l.Lang + "_" + l.Country
}
return l.Lang
}
// ParseLocale creates a Locale from a locale string
func ParseLocale(locale string) Locale {
locsplt := strings.Split(locale, "_")
resp := Locale{}
resp.Lang = locsplt[0]
if len(locsplt) > 1 {
resp.Country = locsplt[1]
}
return resp
}
const (
acceptLanguage = "Accept-Language"
)
func supportedLocales(alstr string) []Locale {
locales := make([]Locale, 0)
alstr = strings.Replace(alstr, " ", "", -1)
if alstr == "" {
return locales
}
al := strings.Split(alstr, ",")
for _, lstr := range al {
locales = append(locales, Locale{
Lang: parseLang(lstr),
Country: parseCountry(lstr),
Qual: parseQual(lstr),
})
}
return locales
}
// GetLocales returns supported locales for the given requet
func GetLocales(r *http.Request) []Locale {
return supportedLocales(r.Header.Get(acceptLanguage))
}
// GetPreferredLocale return preferred locale for the given reuqest
// returns error if there is no preferred locale
func GetPreferredLocale(r *http.Request) (*Locale, error) {
locales := GetLocales(r)
if len(locales) == 0 {
return &Locale{}, errors.New("No locale found")
}
return &locales[0], nil
}
func parseLang(val string) string {
locale := strings.Split(val, ";")[0]
lang := strings.Split(locale, "-")[0]
return lang
}
func parseCountry(val string) string {
locale := strings.Split(val, ";")[0]
spl := strings.Split(locale, "-")
if len(spl) > 1 {
return spl[1]
}
return ""
}
func parseQual(val string) float64 {
spl := strings.Split(val, ";")
if len(spl) > 1 {
qual, err := strconv.ParseFloat(strings.Split(spl[1], "=")[1], 64)
if err != nil {
return 1
}
return qual
}
return 1
}

View File

@ -9,7 +9,6 @@ import (
"path/filepath"
"sort"
"strings"
"sync"
"time"
"github.com/Xe/jsonfeed"
@ -46,10 +45,8 @@ type Site struct {
mux *http.ServeMux
templates map[string]*template.Template
tlock sync.RWMutex
segment analytics.Client
t *translations
}
func (s *Site) ServeHTTP(w http.ResponseWriter, r *http.Request) {
@ -78,31 +75,49 @@ func Build() (*Site, error) {
Date string
}
t := &translations{
locales: map[string]locale{},
}
for _, lang := range []string{"en", "tp"} {
fin, err := os.Open(filepath.Join("locales", lang+".json"))
if err != nil {
return nil, err
}
defer fin.Close()
err = t.LoadLocale(lang, fin)
if err != nil {
return nil, err
}
}
l := t.locales["en"]
s := &Site{
rssFeed: &feeds.Feed{
Title: "Christine Dodrill's Blog",
Title: l.Value("blog", "title"),
Link: &feeds.Link{Href: "https://christine.website/blog"},
Description: "My blog posts and rants about various technology things.",
Description: l.Value("blog", "description"),
Author: &feeds.Author{Name: "Christine Dodrill", Email: "me@christine.website"},
Created: bootTime,
Copyright: "This work is copyright Christine Dodrill. My viewpoints are my own and not the view of any employer past, current or future.",
Copyright: l.Value("meta", "rss_copyright"),
},
jsonFeed: &jsonfeed.Feed{
Version: jsonfeed.CurrentVersion,
Title: "Christine Dodrill's Blog",
Title: l.Value("blog", "title"),
HomePageURL: "https://christine.website",
FeedURL: "https://christine.website/blog.json",
Description: "My blog posts and rants about various technology things.",
UserComment: "This is a JSON feed of my blogposts. For more information read: https://jsonfeed.org/version/1",
Description: l.Value("blog", "description"),
UserComment: l.Value("meta", "json_feed"),
Icon: icon,
Favicon: icon,
Author: jsonfeed.Author{
Name: "Christine Dodrill",
Name: l.Value("header", "name"),
Avatar: icon,
},
},
mux: http.NewServeMux(),
templates: map[string]*template.Template{},
}
if wk := os.Getenv("SEGMENT_WRITE_KEY"); wk != "" {

7
go.mod
View File

@ -1,18 +1,17 @@
module github.com/Xe/site
require (
github.com/GeertJohan/go.rice v0.0.0-20170420135705-c02ca9a983da // indirect
github.com/Xe/gopreload v0.0.0-20170326043426-a00a8beb369c
github.com/Xe/jsonfeed v0.0.0-20170520170432-e21591505612
github.com/Xe/ln v0.0.0-20170921000907-466e05b2ef3e
github.com/daaku/go.zipexe v0.0.0-20150329023125-a5fe2436ffcb // indirect
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/google/gops v0.3.2
github.com/gorilla/feeds v1.0.0
github.com/jtolds/qod v0.0.0-20170925014538-3abb44dfc7ba // indirect
github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1 // indirect
github.com/kr/pretty v0.1.0 // indirect
github.com/magefile/mage v0.0.0-20171025021237-ab3ca2f6f855 // indirect
github.com/pkg/errors v0.8.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/russross/blackfriday v0.0.0-20170806171014-cadec560ec52
github.com/segmentio/backo-go v0.0.0-20160424052352-204274ad699c // indirect
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 // indirect

10
go.sum
View File

@ -1,17 +1,17 @@
github.com/GeertJohan/go.rice v0.0.0-20170420135705-c02ca9a983da/go.mod h1:DgrzXonpdQbfN3uYaGz1EG4Sbhyum/MMIn6Cphlh2bw=
github.com/Xe/gopreload v0.0.0-20170326043426-a00a8beb369c h1:lqTJqaoonxgJMvvfl1ukr/3qCEGWC0nQxzPezbJrhHs=
github.com/Xe/gopreload v0.0.0-20170326043426-a00a8beb369c/go.mod h1:0aSWHJguPNHo6zlU7A4Ktua1A/VUr5Jdr1QZ2amOkAQ=
github.com/Xe/jsonfeed v0.0.0-20170520170432-e21591505612 h1:5cPld6YTMozzm3lK9VCnOErgoFbADM2hZc4KDu0YNKs=
github.com/Xe/jsonfeed v0.0.0-20170520170432-e21591505612/go.mod h1:UKXAbYA/G9yE+cPbpfiP7FlT6Kr3N2mbEjQEJ9hT004=
github.com/Xe/ln v0.0.0-20170921000907-466e05b2ef3e h1:uMC/C/zBov+PItx2c6Vax4lt37X+2V5X7NWRcXsxuzE=
github.com/Xe/ln v0.0.0-20170921000907-466e05b2ef3e/go.mod h1:hDOCtu3XM9SVMB0UHBilUvRbZ0JKMI7IDbvBzwclIb0=
github.com/daaku/go.zipexe v0.0.0-20150329023125-a5fe2436ffcb/go.mod h1:U0vRfAucUOohvdCxt5MWLF+TePIL0xbCkbKIiV8TQCE=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/gops v0.3.2 h1:n9jMkrye8dh3WQ0IxG5dzLRIhQeZDZoGaj0D7T7x7hQ=
github.com/google/gops v0.3.2/go.mod h1:pMQgrscwEK/aUSW1IFSaBPbJX82FPHWaSoJw1axQfD0=
github.com/gorilla/feeds v1.0.0 h1:EbkEvaYf+PXhYNHS20heBG7Rl2X6Zy8l11ZBWAHkWqE=
github.com/gorilla/feeds v1.0.0/go.mod h1:Nk0jZrvPFZX1OBe5NPiddPw7CfwF6Q9eqzaBbaightA=
github.com/jtolds/qod v0.0.0-20170925014538-3abb44dfc7ba/go.mod h1:CC0XsLGDIrizqHTsUEJh89QHzv2u2mix/O9xl4+LqSw=
github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1 h1:PJPDf8OUfOK1bb/NeTKd4f1QXZItOX389VN3B6qC8ro=
github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
@ -19,10 +19,10 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/magefile/mage v0.0.0-20171025021237-ab3ca2f6f855/go.mod h1:IUDi13rsHje59lecXokTfGX0QIzO45uVPlXnJYsXepA=
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday v0.0.0-20170806171014-cadec560ec52 h1:xZ0R4UuR0EDx7m0OmvsqZaomfqCeYJ/PyLm94WLhnIM=
github.com/russross/blackfriday v0.0.0-20170806171014-cadec560ec52/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/segmentio/backo-go v0.0.0-20160424052352-204274ad699c h1:rsRTAcCR5CeNLkvgBVSjQoDGRRt6kggsE6XYBqCv2KQ=

43
locales/en.json Normal file
View File

@ -0,0 +1,43 @@
{
"header": {
"name": "Christine Dodrill",
"blog": "Blog",
"contact": "Contact",
"resume": "Resume"
},
"meta": {
"copyright": "Copyright %s Christine Dodrill. Any and all opinions listed here are my own, not my employer's.",
"rss_copyright": "This work is copyright Christine Dodrill. My viewpoints are my own and not the view of any employer past, current or future.",
"json_feed": "This is a JSON feed of my blogposts. For more information, read: https://jsonfeed.org/version/1"
},
"index": {
"contact_me": "Contact Me",
"title": "Web and Backend Services Devops Specialist",
"skills": "Skills",
"highlighted_projects": "Highlighted Projects"
},
"blog": {
"index_title": "Blogposts",
"blogroll": "Other Blogs I Find Interesting",
"description": "My blog posts and rants about various technology things.",
"title": "Christine Dodrill's Blog",
"posted_on": "Posted on %s",
"read_this": "Read Post",
"disclaimer": "Content posted on %s, opinions and preferences of the author may have changed since then."
},
"contact": {
"header": "Contact Information",
"email": "Email",
"social_media": "Social Media",
"other_info": "Other Information",
"donations": "To send me donations, my bitcoin address is: ",
"irc_comment": "I am on many IRC networks. On Freenode I am using the nick Xe but elsewhere I will use the nick Xena or Cadey."
},
"error": {
"title": "Error",
"post_not_found": "no such post found: %s"
},
"resume": {
"plaintext": "Plain-text version of this resume here"
}
}

43
locales/tp.json Normal file
View File

@ -0,0 +1,43 @@
{
"header": {
"name": "jan Wisi Totiwi",
"blog": "lipu sona",
"contact": "lipu toki",
"resume": "lipu pali mi"
},
"meta": {
"copyright": "lipu ni li pali e jan Wisi Totiwi pi suli %s. sina pali ala e ni. tenpo ale la nasin sona mi li nasin sona ala pi kulupu lawa mi.",
"rss_copyright": "lipu ni li pali e jan Wisi Totiwi. sina pali ala e ni. tenpo ale la nasin sona mi li nasin sona ala pi kulupu lawa mi.",
"json_feed": "lipu ni li lipu Sason Pe. sina wile sona la sina lukin e lipu: https://jsonfeed.org/version/1"
},
"index": {
"contact_me": "toki e mi",
"title": "jan pali e ilo suli",
"skills": "kepeken pali e mi",
"highlighted_projects": "pali musi e mi"
},
"blog": {
"index_title": "lipu sona",
"blogroll": "lipu sona pi jan ante",
"description": "lipu ni li jan Wisi Totiwi pi lipu sona. lipu sona ni li ilo pona li ilo sona.",
"title": "jan Wisi Totiwi pi lipu sona",
"posted_on": "tenpo pi %s",
"read_this": "lukin e ni",
"disclaimer": "tenpo %s la lipu ni li pali. tenpo ni la jan Wisi Totiwi li ken nasin sona ante."
},
"contact": {
"header": "lipu toki e mi",
"email": "ilo toki",
"social_media": "kalama ilo toki",
"other_info": "lipu ante",
"donations": "ken la sina pana mani e mi. sina wile kepeken lipu: ",
"irc_comment": "mi kepeken e kulupu mute pi ilo toki. nimi mi li 'Xe' anu 'Xena' anu 'Cadey'."
},
"error": {
"title": "pakala",
"post_not_found": "lipu %s li pona ala. ona li lukin ala."
},
"resume": {
"plaintext": "sina wile lipu pona la sina lukin e ni"
}
}

View File

@ -56,11 +56,11 @@
{{ template "scripts" . }}
<div class="container">
<header>
<p><a href="/">Christine Dodrill</a> - <a href="/blog">Blog</a> - <a href="/contact">Contact</a> - <a href="/resume">Resume</a></p>
<p><a href="/">{{ trans "header" "name" }}</a> - <a href="/blog">{{ trans "header" "blog" }}</a> - <a href="/contact">{{ trans "header" "contact" }}</a> - <a href="/resume">{{ trans "header" "resume" }}</a></p>
</header>
{{ template "content" . }}
<footer>
<blockquote>Copyright 2018 Christine Dodrill. Any and all opinions listed here are my own and not representative of my employer.</blockquote>
<blockquote>{{ trans "meta" "copyright" "2018" }}</blockquote>
</footer>
<script>
if (navigator.serviceWorker.controller) {

View File

@ -1,5 +1,5 @@
{{ define "title" }}
<title>Blog - Christine Dodrill</title>
<title>{{ trans "header" "blog" }} - {{ trans "header" "name" }}</title>
<style>
.blogpost-card {
@ -9,14 +9,14 @@
{{ end }}
{{ define "content" }}
<h1>Blogposts</h1>
<h1>{{ trans "blog" "index_title" }}</h1>
<div class="grid">
{{ range . }}
<div class="card cell -4of12 blogpost-card">
<header class="card-header">{{ .Title }}</header>
<div class="card-content">
<p>Posted on {{ .Date }} <br> <a href="{{ .Link }}">Read Post</a></p>
<p>{{ trans "blog" "posted_on" .Date }} <br> <a href="{{ .Link }}">{{ trans "blog" "read_this" }}</a></p>
</div>
</div>
{{ end }}
@ -24,7 +24,7 @@
<br />
<h2>Other Blogs I Find Interesting</h2>
<h2>{{ trans "blog" "blogroll" }}</h2>
<ul>
<li><a href="https://write.as/excerpts/">Excerpts</a></li>

View File

@ -1,5 +1,5 @@
{{ define "title" }}
<title>{{ .Title }} - Christine Dodrill</title>
<title>{{ .Title }} - {{ trans "header" "name" }}</title>
<!-- Twitter -->
<meta name="twitter:card" content="summary" />
@ -10,11 +10,11 @@
<!-- Facebook -->
<meta property="og:type" content="website" />
<meta property="og:title" content="{{ .Title }}" />
<meta property="og:site_name" content="Christine Dodrill's Blog" />
<meta property="og:site_name" content="{{ trans "blog" "title" }}" />
<!-- Description -->
<meta name="description" content="{{ .Title }} - Christine Dodrill's Blog" />
<meta name="author" content="Christine Dodrill">
<meta name="description" content="{{ .Title }} - {{ trans "blog" "title" }}" />
<meta name="author" content="{{ trans "header" "name" }}">
{{ end }}
{{ define "content" }}
@ -22,5 +22,5 @@
<hr />
<i>Content posted on {{ .Date }}, opinions and preferences of the author may have changed since then.</i>
<i>{{ trans "blog" "disclaimer" .Date }}</i>
{{ end }}

View File

@ -1,15 +1,13 @@
{{ define "title" }}<title>Contact - Christine Dodrill</title>{{ end }}
{{ define "title" }}<title>{{ trans "header" "contact" }} - {{ trans "header" "name" }}</title>{{ end }}
{{ define "content" }}
<h1>Contact Information</h1>
<h1>{{ trans "contact" "header" }}</h1>
<div class="grid">
<div class="cell -6of12">
<h3>Email</h3>
<h3>{{ trans "contact" "email" }}</h3>
<p>me@christine.website</p>
<p>My GPG fingerprint is <code>799F 9134 8118 1111</code>. If you get an email that appears to be from me and the signature does not match that fingerprint, it is not from me. You may download a copy of my public key <a href="/static/gpg.pub">here</a>.</p>
<h3>Social Media</h3>
<h3>{{ trans "contact" "social_media" }}</h3>
<ul>
<li><a href="https://github.com/Xe">Github</a></li>
<li><a href="https://twitter.com/theprincessxena">Twitter</a></li>
@ -20,11 +18,11 @@
</ul>
</div>
<div class="cell -6of12">
<h3>Other Information</h3>
<p>To send me donations, my bitcoin address is <code>1Gi2ZF2C9CU9QooH8bQMB2GJ2iL6shVnVe</code>.</p>
<h3>{{ trans "contact" "other_info" }}</h3>
<p>{{ trans "contact" "donations" }} <code>1Gi2ZF2C9CU9QooH8bQMB2GJ2iL6shVnVe</code>.</p>
<h4>IRC</h4>
<p>I am on many IRC networks. On Freenode I am using the nick Xe but elsewhere I will use the nick Xena or Cadey.</p>
<p>{{ trans "contact" "irc_comment" }}</p>
<h4>Telegram</h4>
<p><a href="https://t.me/miamorecadenza">@miamorecadenza</a></p>

View File

@ -1,5 +1,5 @@
{{ define "title" }}
<title>Error - Christine Dodrill</title>
<title>{{ trans "error" "title" }} - {{ trans "header" "name" }}</title>
{{ end }}
{{ define "content" }}

View File

@ -1,29 +1,36 @@
{{ define "title" }}<title>Christine Dodrill</title>{{ end }}
{{ define "title" }}<title>{{ trans "header" "name" }}</title>{{ end }}
{{ define "content" }}
<div class="grid">
<div class="cell -3of12 content">
<img src="/static/img/avatar.png">
<br />
<a href="/contact" class="justify-content-center">Contact Me</a>
<a href="/contact" class="justify-content-center">{{ trans "index" "contact_me" }}</a>
</div>
<div class="cell -9of12 content">
<h1>Christine Dodrill</h1>
<h4>Web and Backend Services Devops Specialist</h4>
<h5>Skills</h5>
<h1>{{ trans "header" "name" }}</h1>
<h4>{{ trans "index" "title" }}</h4>
<h5>{{ trans "index" "skills" }}</h5>
<ul>
<li>Go, Lua, Nim, Haskell, C, Python (3.x) and other languages</li>
<li>Docker (deployment, development & more)</li>
<li>Mashups of data</li>
<li>Package maintainer for Alpine Linux</li>
<li>Speaks English, toki pona, and la .lojban.</li>
</ul>
<h5>Highlighted Projects</h5>
<h5>{{ trans "index" "highlighted_projects" }}</h5>
<ul>
<li><a href="https://github.com/Xe/PonyAPI">PonyAPI</a> - My Little Pony: Friendship is Magic Episode information API</li>
<li><a href="https://github.com/PonyvilleFM/aura">Aura</a> - PonyvilleFM live DJ recording bot</li>
<li><a href="https://github.com/Elemental-IRCd/elemental-ircd">Elemental-IRCd</a> - IRC Server Software</li>
<li><a href="https://github.com/Xe/site">This website</a> - The backend and templates for this website</li>
<li><a href="https://github.com/Xe/olin">Olin</a> - WebAssembly on the server</li>
<li><a href="https://github.com/Xe/aports">aports</a> - Alpine Linux ports</li>
<li><a href="https://github.com/Xe/when-then-zen">when-then-zen</a> - Meditation instructions in Gherkin</li>
<li><a href="https://github.com/Xe/creators-code">Creator's Code</a> - Minimal code of conduct for communities</li>
<li><a href="https://github.com/Xe/printerfacts">printerfacts</a> - Informative facts about printers</li>
<li><a href="https://github.com/Xe/x">x</a> - Experiments and toys</li>
</ul>
</div>
</div>

View File

@ -1,9 +1,9 @@
{{ define "title" }}<title>Resume - Christine Dodrill</title>{{ end }}
{{ define "title" }}<title>{{ trans "header" "resume" }} - {{ trans "header" "name" }}</title>{{ end }}
{{ define "content" }}
{{ . }}
<hr />
<a href="/static/resume/resume.md">Plain-text version of this resume here</a>
<a href="/static/resume/resume.md">{{ trans "resume" "plaintext" }}</a>
{{ end }}