Index: trunk/engines/engine.go
===================================================================
--- trunk/engines/engine.go	(revision 14)
+++ trunk/engines/engine.go	(revision 15)
@@ -2,6 +2,6 @@
 
 type TranslationResult struct {
-	SourceLanguage Language
-	TranslatedText string
+	SourceLanguage string `json:"source_language"`
+	TranslatedText string `json:"translated_text"`
 }
 
@@ -9,8 +9,16 @@
 	InternalName() string
 	DisplayName() string
-	SourceLanguages() ([]Language, error)
-	TargetLanguages() ([]Language, error)
-	Translate(text string, from, to Language) (TranslationResult, error)
-	SupportsAutodetect() bool
-	DetectLanguage(text string) (Language, error)
+	SourceLanguages() (Language, error)
+	TargetLanguages() (Language, error)
+	Translate(text string, from, to string) (TranslationResult, error)
+	DetectLanguage(text string) (string, error)
 }
+
+type Language map[string]string
+
+var Engines = map[string]Engine{
+	"google":   &GoogleTranslate{},
+	"icibia":   &ICIBA{},
+	"libre":    &LibreTranslate{},
+	"reverseo": &Reverso{},
+}
Index: trunk/engines/google.go
===================================================================
--- trunk/engines/google.go	(revision 14)
+++ trunk/engines/google.go	(revision 15)
@@ -15,5 +15,5 @@
 func (_ *GoogleTranslate) DisplayName() string { return "Google" }
 
-func (_ *GoogleTranslate) getLangs(type_ string) ([]Language, error) {
+func (_ *GoogleTranslate) getLangs(type_ string) (Language, error) {
 	var langsType string
 	switch type_ {
@@ -28,10 +28,5 @@
 	}
 
-	requestURL, err := url.Parse("https://translate.google.com/m")
-
-	if err != nil {
-		// The URL is constant, so it should never fail.
-		panic(err)
-	}
+	requestURL, _ := url.Parse("https://translate.google.com/m")
 
 	query := url.Values{}
@@ -54,5 +49,5 @@
 	}
 
-	var langs []Language
+	var langs Language
 
 	doc.Find(".language-item").Each(func(_ int, s *goquery.Selection) {
@@ -78,5 +73,5 @@
 		}
 
-		langs = append(langs, Language{Name: a.Text(), Code: langCode})
+		langs[langCode] = a.Text()
 	})
 
@@ -84,28 +79,25 @@
 }
 
-func (e *GoogleTranslate) SourceLanguages() ([]Language, error) {
+func (e *GoogleTranslate) SourceLanguages() (Language, error) {
 	return e.getLangs("source")
 }
 
-func (e *GoogleTranslate) TargetLanguages() ([]Language, error) {
+func (e *GoogleTranslate) TargetLanguages() (Language, error) {
 	return e.getLangs("target")
 }
 
-func (_ *GoogleTranslate) SupportsAutodetect() bool { return true }
+func (_ *GoogleTranslate) DetectLanguage(text string) (string, error) { return "", nil }
 
-func (_ *GoogleTranslate) DetectLanguage(text string) (Language, error) { return Language{}, nil }
+func (_ *GoogleTranslate) Translate(text string, from, to string) (TranslationResult, error) {
+	requestURL, _ := url.Parse("https://translate.google.com/m")
 
-func (_ *GoogleTranslate) Translate(text string, from, to Language) (TranslationResult, error) {
-	requestURL, err := url.Parse("https://translate.google.com/m")
-
-	if err != nil {
-		// The URL is constant, so it should never fail.
-		panic(err)
+	if from == "" {
+		from = "auto"
 	}
 
 	query := url.Values{}
-	query.Add("sl", from.Code)
-	query.Add("tl", to.Code)
-	query.Add("hl", to.Code)
+	query.Add("sl", from)
+	query.Add("tl", to)
+	query.Add("hl", to)
 	query.Add("q", text)
 	requestURL.RawQuery = query.Encode()
Index: trunk/engines/iciba.go
===================================================================
--- trunk/engines/iciba.go	(revision 14)
+++ trunk/engines/iciba.go	(revision 15)
@@ -17,5 +17,5 @@
 func (_ *ICIBA) DisplayName() string { return "iCIBA" }
 
-var icibaLanguages = []Language{
+var icibaLanguages = Language{
 	// ICIBA does have an API, but they return Chinese names.
 	// For languages already present in Google translate, the English
@@ -26,197 +26,195 @@
 	// due to conflict between ISO-639 table and Chinese label
 	// one "//" means on iciba but not on google
-	{Name: "Achinese", Code: "ace"},       //
-	{Name: "Achuar-Shiwiar", Code: "acu"}, //
-	{Name: "Afrikaans", Code: "af"},
-	{Name: "Aguaruna", Code: "agr"}, //
-	{Name: "Akawaio", Code: "ake"},  //
-	{Name: "Albanian", Code: "sq"},
-	{Name: "Amharic", Code: "am"},
-	{Name: "Arabic", Code: "ar"},
-	{Name: "Armenian", Code: "hy"},
-	{Name: "Azerbaijani", Code: "az"},
-	{Name: "Barasana-Eduria", Code: "bsn"}, //
-	{Name: "Bashkir", Code: "ba"},          //
-	{Name: "Basque", Code: "eu"},
-	{Name: "Belarusian", Code: "be"},
-	{Name: "Bemba", Code: "bem"}, //
-	{Name: "Bengali", Code: "bn"},
-	{Name: "Berber", Code: "ber"}, //
-	{Name: "Bislama", Code: "bi"}, //
-	{Name: "Bosnian", Code: "bs"},
-	{Name: "Breton", Code: "br"}, //
-	{Name: "Bulgarian", Code: "bg"},
-	{Name: "Cabécar", Code: "cjp"}, //
-	{Name: "Cantonese", Code: "yue"},
-	{Name: "Catalan", Code: "ca"},
-	{Name: "Cebuano", Code: "ceb"},
-	{Name: "Chamorro", Code: "cha"}, //
-	{Name: "Cherokee", Code: "chr"}, //
-	{Name: "Chichewa", Code: "ny"},
-	{Name: "Chinese (Simplified)", Code: "zh"},   // "zh-cn" on Google
-	{Name: "Chinese (Traditional)", Code: "cht"}, // "zh-tw" on Google
-	{Name: "Chuvash", Code: "cv"},
-	{Name: "Coptic", Code: "cop"}, //
-	{Name: "Corsican", Code: "co"},
-	{Name: "Croatian", Code: "hr"},
-	{Name: "Czech", Code: "cs"},
-	{Name: "Danish", Code: "da"},
-	{Name: "Dhivehi", Code: "dv"}, //
-	{Name: "Dinka", Code: "dik"},  //
-	{Name: "Dutch", Code: "nl"},
-	{Name: "Dzongkha", Code: "dz"}, //
-	{Name: "English", Code: "en"},
-	{Name: "Esperanto", Code: "eo"},
-	{Name: "Estonian", Code: "et"},
-	{Name: "Ewe", Code: "ee"},       //
-	{Name: "Faroese", Code: "fo"},   //
-	{Name: "Fijian", Code: "fj"},    //
-	{Name: "Filipino", Code: "fil"}, // "tl" on Google
-	{Name: "Finnish", Code: "fi"},
-	{Name: "French", Code: "fr"},
-	{Name: "Frisian", Code: "fy"},
-	{Name: "Galela", Code: "gbi"}, //
-	{Name: "Galician", Code: "gl"},
-	{Name: "Ganda", Code: "lg"},    //
-	{Name: "Georgian", Code: "jy"}, // "ka" on Google
-	{Name: "German", Code: "de"},
-	{Name: "Greek", Code: "el"},
-	{Name: "Guerrero Amuzgo", Code: "amu"}, //
-	{Name: "Gujarati", Code: "gu"},
-	{Name: "Haitian Creole", Code: "ht"},
-	{Name: "Hausa", Code: "ha"},
-	{Name: "Hawaiian", Code: "haw"},
-	{Name: "Hebrew", Code: "he"}, // "iw" on Google
-	{Name: "Hindi", Code: "hi"},
-	{Name: "Hmong Daw", Code: "mww"}, //
-	{Name: "Hmong", Code: "hmn"},     // not in iciba
-	{Name: "Hungarian", Code: "hu"},
-	{Name: "Icelandic", Code: "is"},
-	{Name: "Igbo", Code: "ig"},
-	{Name: "Indonesian", Code: "id"},
-	{Name: "Irish", Code: "ga"},
-	{Name: "Italian", Code: "it"},
-	{Name: "Jacalteco", Code: "jac"}, //
-	{Name: "Japanese", Code: "ja"},
-	{Name: "Javanese", Code: "jv"}, // "jw" on Google
-	{Name: "Kabyle", Code: "kab"},  //
-	{Name: "Kannada", Code: "kn"},
-	{Name: "Kaqchikel", Code: "cak"},        //
-	{Name: "Kazakh", Code: "ka"},            // Google only has "kk"
-	{Name: "Kazakh (Cyrillic)", Code: "kk"}, // Google has it as just "Kazakh"
-	{Name: "Kekchí", Code: "kek"},           //
-	{Name: "Khmer", Code: "km"},
-	{Name: "Kinyarwanda", Code: "rw"},
-	{Name: "Kongo", Code: "kg"}, //
-	{Name: "Korean", Code: "ko"},
-	{Name: "Kurdish (Kurmanji)", Code: "ku"},
-	{Name: "Kyrgyz", Code: "ky"},
-	{Name: "Lao", Code: "lo"},
-	{Name: "Latin", Code: "la"},
-	{Name: "Latvian", Code: "lv"},
-	{Name: "Lingala", Code: "ln"}, //
-	{Name: "Lithuanian", Code: "lt"},
-	{Name: "Lukpa", Code: "dop"}, //
-	{Name: "Luxembourgish", Code: "lb"},
-	{Name: "Macedonian", Code: "mk"},
-	{Name: "Malagasy", Code: "mg"},
-	{Name: "Malay", Code: "ms"},
-	{Name: "Malayalam", Code: "ml"},
-	{Name: "Maltese", Code: "mt"},
-	{Name: "Mam", Code: "mam"}, //
-	{Name: "Manx", Code: "gv"}, //
-	{Name: "Maori", Code: "mi"},
-	{Name: "Marathi", Code: "mr"},
-	{Name: "Mari (Eastern)", Code: "mhr"}, //
-	{Name: "Mari (Western)", Code: "mrj"}, //
-	{Name: "Mongolian", Code: "mn"},
-	{Name: "Montenegrin", Code: "me"}, //
-	{Name: "Myanmar (Burmese)", Code: "my"},
-	{Name: "Nahuatl", Code: "nhg"}, //
-	{Name: "Ndyuka", Code: "djk"},  //
-	{Name: "Nepali", Code: "ne"},
-	{Name: "Norwegian", Code: "no"},
-	{Name: "Odia (Oriya)", Code: "or"},
-	{Name: "Ojibwa", Code: "ojb"},
-	{Name: "Oromo", Code: "om"},       //
-	{Name: "Ossetian", Code: "os"},    //
-	{Name: "Paite", Code: "pck"},      //
-	{Name: "Papiamento", Code: "pap"}, //
-	{Name: "Pashto", Code: "ps"},
-	{Name: "Persian", Code: "fa"},
-	{Name: "Polish", Code: "pl"},
-	{Name: "Portuguese", Code: "pt"},
-	{Name: "Potawatomi", Code: "pot"}, //
-	{Name: "Punjabi", Code: "pa"},
-	{Name: "Querétaro Otomi", Code: "otq"},     //
-	{Name: "Quiché", Code: "quc"},              //
-	{Name: "Quichua", Code: "quw"},             //
-	{Name: "Quiotepec Chinantec", Code: "chq"}, //
-	{Name: "Romani", Code: "rmn"},              //
-	{Name: "Romanian", Code: "ro"},
-	{Name: "Rundi", Code: "rn"}, //
-	{Name: "Russian", Code: "ru"},
-	{Name: "Samoan", Code: "sm"},
-	{Name: "Sango", Code: "sg"}, //
-	{Name: "Scots Gaelic", Code: "gd"},
-	{Name: "Serbian", Code: "sr"},
-	{Name: "Seselwa Creole French", Code: "crs"}, //
-	{Name: "Sesotho", Code: "st"},
-	{Name: "Shona", Code: "sn"},
-	{Name: "Shuar", Code: "jiv"}, //
-	{Name: "Sindhi", Code: "sd"},
-	{Name: "Sinhala", Code: "si"},
-	{Name: "Slovak", Code: "sk"},
-	{Name: "Slovenian", Code: "sl"},
-	{Name: "Somali", Code: "so"},
-	{Name: "Spanish", Code: "es"},
-	{Name: "Sundanese", Code: "su"},
-	{Name: "Swahili", Code: "sw"},
-	{Name: "Swedish", Code: "sv"},
-	{Name: "Syriac", Code: "syc"},    // considered "extinct" but is somehow supported
-	{Name: "Tachelhit", Code: "shi"}, //
-	{Name: "Tahitian", Code: "ty"},   //
-	{Name: "Tajik", Code: "tg"},
-	{Name: "Tamil", Code: "ta"},
-	{Name: "Tatar", Code: "tt"},
-	{Name: "Telugu", Code: "te"},
-	{Name: "Tetum", Code: "tet"}, //
-	{Name: "Thai", Code: "th"},
-	{Name: "Tigre", Code: "ti"},      //
-	{Name: "Tiwi", Code: "tw"},       //
-	{Name: "Tok Pisin", Code: "tpi"}, //
-	{Name: "Tonga", Code: "to"},      //
-	{Name: "Tsonga", Code: "ts"},
-	{Name: "Tswana", Code: "tn"}, //
-	{Name: "Turkish", Code: "tr"},
-	{Name: "Turkmen", Code: "tk"},
-	{Name: "Udmurt", Code: "udm"}, //
-	{Name: "Ukrainian", Code: "uk"},
-	{Name: "Uma", Code: "ppk"}, //
-	{Name: "Urdu", Code: "ur"},
-	{Name: "Uspanteco", Code: "usp"}, //
-	{Name: "Uyghur", Code: "uy"},     // "ug" on Google
-	{Name: "Uzbek", Code: "uz"},
-	{Name: "Venda", Code: "ve"}, //
-	{Name: "Vietnamese", Code: "vi"},
-	{Name: "Waray", Code: "war"}, //
-	{Name: "Welsh", Code: "cy"},
-	{Name: "Wolaitta", Code: "wal"}, //
-	{Name: "Wolof", Code: "wol"},
-	{Name: "Xhosa", Code: "xh"},
-	{Name: "Yiddish", Code: "yi"},
-	{Name: "Yoruba", Code: "yo"},
-	{Name: "Yucatán Maya", Code: "yua"}, //
-	{Name: "Zarma", Code: "dje"},        //
-	{Name: "Zulu", Code: "zu"},
+	"ace": "Achinese",       //
+	"acu": "Achuar-Shiwiar", //
+	"af":  "Afrikaans",
+	"agr": "Aguaruna", //
+	"ake": "Akawaio",  //
+	"sq":  "Albanian",
+	"am":  "Amharic",
+	"ar":  "Arabic",
+	"hy":  "Armenian",
+	"az":  "Azerbaijani",
+	"bsn": "Barasana-Eduria", //
+	"ba":  "Bashkir",         //
+	"eu":  "Basque",
+	"be":  "Belarusian",
+	"bem": "Bemba", //
+	"bn":  "Bengali",
+	"ber": "Berber",  //
+	"bi":  "Bislama", //
+	"bs":  "Bosnian",
+	"br":  "Breton", //
+	"bg":  "Bulgarian",
+	"cjp": "Cabécar", //
+	"yue": "Cantonese",
+	"ca":  "Catalan",
+	"ceb": "Cebuano",
+	"cha": "Chamorro", //
+	"chr": "Cherokee", //
+	"ny":  "Chichewa",
+	"zh":  "Chinese (Simplified)",  // "zh-cn" on Google
+	"cht": "Chinese (Traditional)", // "zh-tw" on Google
+	"cv":  "Chuvash",
+	"cop": "Coptic", //
+	"co":  "Corsican",
+	"hr":  "Croatian",
+	"cs":  "Czech",
+	"da":  "Danish",
+	"dv":  "Dhivehi", //
+	"dik": "Dinka",   //
+	"nl":  "Dutch",
+	"dz":  "Dzongkha", //
+	"en":  "English",
+	"eo":  "Esperanto",
+	"et":  "Estonian",
+	"ee":  "Ewe",      //
+	"fo":  "Faroese",  //
+	"fj":  "Fijian",   //
+	"fil": "Filipino", // "tl" on Google
+	"fi":  "Finnish",
+	"fr":  "French",
+	"fy":  "Frisian",
+	"gbi": "Galela", //
+	"gl":  "Galician",
+	"lg":  "Ganda",    //
+	"jy":  "Georgian", // "ka" on Google
+	"de":  "German",
+	"el":  "Greek",
+	"amu": "Guerrero Amuzgo", //
+	"gu":  "Gujarati",
+	"ht":  "Haitian Creole",
+	"ha":  "Hausa",
+	"haw": "Hawaiian",
+	"he":  "Hebrew", // "iw" on Google
+	"hi":  "Hindi",
+	"mww": "Hmong Daw", //
+	"hmn": "Hmong",     // not in iciba
+	"hu":  "Hungarian",
+	"is":  "Icelandic",
+	"ig":  "Igbo",
+	"id":  "Indonesian",
+	"ga":  "Irish",
+	"it":  "Italian",
+	"jac": "Jacalteco", //
+	"ja":  "Japanese",
+	"jv":  "Javanese", // "jw" on Google
+	"kab": "Kabyle",   //
+	"kn":  "Kannada",
+	"cak": "Kaqchikel",         //
+	"ka":  "Kazakh",            // Google only has "kk"
+	"kk":  "Kazakh (Cyrillic)", // Google has it as just "Kazakh"
+	"kek": "Kekchí",            //
+	"km":  "Khmer",
+	"rw":  "Kinyarwanda",
+	"kg":  "Kongo", //
+	"ko":  "Korean",
+	"ku":  "Kurdish (Kurmanji)",
+	"ky":  "Kyrgyz",
+	"lo":  "Lao",
+	"la":  "Latin",
+	"lv":  "Latvian",
+	"ln":  "Lingala", //
+	"lt":  "Lithuanian",
+	"dop": "Lukpa", //
+	"lb":  "Luxembourgish",
+	"mk":  "Macedonian",
+	"mg":  "Malagasy",
+	"ms":  "Malay",
+	"ml":  "Malayalam",
+	"mt":  "Maltese",
+	"mam": "Mam",  //
+	"gv":  "Manx", //
+	"mi":  "Maori",
+	"mr":  "Marathi",
+	"mhr": "Mari (Eastern)", //
+	"mrj": "Mari (Western)", //
+	"mn":  "Mongolian",
+	"me":  "Montenegrin", //
+	"my":  "Myanmar (Burmese)",
+	"nhg": "Nahuatl", //
+	"djk": "Ndyuka",  //
+	"ne":  "Nepali",
+	"no":  "Norwegian",
+	"or":  "Odia (Oriya)",
+	"ojb": "Ojibwa",
+	"om":  "Oromo",      //
+	"os":  "Ossetian",   //
+	"pck": "Paite",      //
+	"pap": "Papiamento", //
+	"ps":  "Pashto",
+	"fa":  "Persian",
+	"pl":  "Polish",
+	"pt":  "Portuguese",
+	"pot": "Potawatomi", //
+	"pa":  "Punjabi",
+	"otq": "Querétaro Otomi",     //
+	"quc": "Quiché",              //
+	"quw": "Quichua",             //
+	"chq": "Quiotepec Chinantec", //
+	"rmn": "Romani",              //
+	"ro":  "Romanian",
+	"rn":  "Rundi", //
+	"ru":  "Russian",
+	"sm":  "Samoan",
+	"sg":  "Sango", //
+	"gd":  "Scots Gaelic",
+	"sr":  "Serbian",
+	"crs": "Seselwa Creole French", //
+	"st":  "Sesotho",
+	"sn":  "Shona",
+	"jiv": "Shuar", //
+	"sd":  "Sindhi",
+	"si":  "Sinhala",
+	"sk":  "Slovak",
+	"sl":  "Slovenian",
+	"so":  "Somali",
+	"es":  "Spanish",
+	"su":  "Sundanese",
+	"sw":  "Swahili",
+	"sv":  "Swedish",
+	"syc": "Syriac",    // considered "extinct" but is somehow supported
+	"shi": "Tachelhit", //
+	"ty":  "Tahitian",  //
+	"tg":  "Tajik",
+	"ta":  "Tamil",
+	"tt":  "Tatar",
+	"te":  "Telugu",
+	"tet": "Tetum", //
+	"th":  "Thai",
+	"ti":  "Tigre",     //
+	"tw":  "Tiwi",      //
+	"tpi": "Tok Pisin", //
+	"to":  "Tonga",     //
+	"ts":  "Tsonga",
+	"tn":  "Tswana", //
+	"tr":  "Turkish",
+	"tk":  "Turkmen",
+	"udm": "Udmurt", //
+	"uk":  "Ukrainian",
+	"ppk": "Uma", //
+	"ur":  "Urdu",
+	"usp": "Uspanteco", //
+	"uy":  "Uyghur",    // "ug" on Google
+	"uz":  "Uzbek",
+	"ve":  "Venda", //
+	"vi":  "Vietnamese",
+	"war": "Waray", //
+	"cy":  "Welsh",
+	"wal": "Wolaitta", //
+	"wol": "Wolof",
+	"xh":  "Xhosa",
+	"yi":  "Yiddish",
+	"yo":  "Yoruba",
+	"yua": "Yucatán Maya", //
+	"dje": "Zarma",        //
+	"zu":  "Zulu",
 }
 
-func (_ *ICIBA) SourceLanguages() ([]Language, error) { return icibaLanguages, nil }
-
-func (_ *ICIBA) TargetLanguages() ([]Language, error) { return icibaLanguages, nil }
-
-func (_ *ICIBA) SupportsAutodetect() bool { return true }
-
-func (_ *ICIBA) DetectLanguage(text string) (Language, error) { return Language{}, nil }
+func (_ *ICIBA) SourceLanguages() (Language, error) { return icibaLanguages, nil }
+
+func (_ *ICIBA) TargetLanguages() (Language, error) { return icibaLanguages, nil }
+
+func (_ *ICIBA) DetectLanguage(text string) (string, error) { return "", nil }
 
 type icibaTranslateResponse struct {
@@ -227,5 +225,5 @@
 }
 
-func (_ *ICIBA) Translate(text string, from, to Language) (TranslationResult, error) {
+func (_ *ICIBA) Translate(text string, from, to string) (TranslationResult, error) {
 	requestURL, err := url.Parse("https://ifanyi.iciba.com/index.php")
 
@@ -248,6 +246,6 @@
 
 	formData := url.Values{}
-	formData.Add("from", from.Code)
-	formData.Add("to", to.Code)
+	formData.Add("from", from)
+	formData.Add("to", to)
 	formData.Add("q", text)
 
@@ -270,15 +268,15 @@
 	}
 
-	var sourceLanguage Language
-
-	for _, lang := range icibaLanguages {
-		if lang.Code == responseJSON.Content.From {
-			sourceLanguage = lang
+	var sourceLanguage string
+
+	for code := range icibaLanguages {
+		if code == responseJSON.Content.From {
+			sourceLanguage = code
 			break
 		}
 	}
 
-	if sourceLanguage == (Language{}) {
-		return TranslationResult{SourceLanguage: from, TranslatedText: responseJSON.Content.Out},
+	if sourceLanguage == "" {
+		return TranslationResult{TranslatedText: responseJSON.Content.Out},
 			fmt.Errorf("language code \"%s\" is not in iCIBA's language list", responseJSON.Content.From)
 	}
Index: trunk/engines/language.go
===================================================================
--- trunk/engines/language.go	(revision 14)
+++ 	(revision )
@@ -1,5 +1,0 @@
-package engines
-
-type Language struct {
-	Name, Code string
-}
Index: trunk/engines/libretranslate.go
===================================================================
--- trunk/engines/libretranslate.go	(revision 14)
+++ trunk/engines/libretranslate.go	(revision 15)
@@ -28,10 +28,5 @@
 func (_ *LibreTranslate) DisplayName() string { return "LibreTranslate" }
 
-type libreLanguagesResponse []struct {
-	Name string `json:"name"`
-	Code string `json:"code"`
-}
-
-func (e *LibreTranslate) getLangs() ([]Language, error) {
+func (e *LibreTranslate) getLangs() (Language, error) {
 	response, err := http.Get(e.InstanceURL + "/languages")
 
@@ -46,5 +41,8 @@
 	}
 
-	var langsResponse libreLanguagesResponse
+	var langsResponse []struct {
+		Name string `json:"name"`
+		Code string `json:"code"`
+	}
 
 	if err := json.NewDecoder(response.Body).Decode(&langsResponse); err != nil {
@@ -52,8 +50,8 @@
 	}
 
-	langs := make([]Language, len(langsResponse))
+	langs := Language{}
 
-	for i, lang := range langsResponse {
-		langs[i] = Language{Name: lang.Name, Code: lang.Code}
+	for _, lang := range langsResponse {
+		langs[lang.Code] = lang.Name
 	}
 
@@ -62,9 +60,7 @@
 }
 
-func (e *LibreTranslate) SourceLanguages() ([]Language, error) { return e.getLangs() }
+func (e *LibreTranslate) SourceLanguages() (Language, error) { return e.getLangs() }
 
-func (e *LibreTranslate) TargetLanguages() ([]Language, error) { return e.getLangs() }
-
-func (_ *LibreTranslate) SupportsAutodetect() bool { return true }
+func (e *LibreTranslate) TargetLanguages() (Language, error) { return e.getLangs() }
 
 type libreDetectResponse []struct {
@@ -73,5 +69,5 @@
 }
 
-func (e *LibreTranslate) DetectLanguage(text string) (Language, error) {
+func (e *LibreTranslate) DetectLanguage(text string) (string, error) {
 	formData := map[string]string{"q": text}
 
@@ -83,5 +79,5 @@
 
 	if err != nil {
-		return Language{}, err
+		return "", err
 	}
 
@@ -89,5 +85,5 @@
 
 	if err != nil {
-		return Language{}, err
+		return "", err
 	}
 
@@ -95,5 +91,5 @@
 
 	if response.StatusCode != 200 {
-		return Language{}, fmt.Errorf("got status code %d from LibreTranslate API", response.StatusCode)
+		return "", fmt.Errorf("got status code %d from LibreTranslate API", response.StatusCode)
 	}
 
@@ -101,5 +97,5 @@
 
 	if err := json.NewDecoder(response.Body).Decode(&langs); err != nil {
-		return Language{}, err
+		return "", err
 	}
 
@@ -115,14 +111,14 @@
 
 	if err != nil {
-		return Language{}, err
+		return "", err
 	}
 
-	for _, lang := range engineLangs {
-		if lang.Code == maxConfidenceLang.LanguageCode {
-			return lang, nil
+	for code := range engineLangs {
+		if code == maxConfidenceLang.LanguageCode {
+			return code, nil
 		}
 	}
 
-	return Language{}, fmt.Errorf("language code \"%s\" is not in the instance's language list", maxConfidenceLang.LanguageCode)
+	return "", fmt.Errorf("language code \"%s\" is not in the instance's language list", maxConfidenceLang.LanguageCode)
 }
 
@@ -131,9 +127,9 @@
 }
 
-func (e *LibreTranslate) Translate(text string, from, to Language) (TranslationResult, error) {
+func (e *LibreTranslate) Translate(text string, from, to string) (TranslationResult, error) {
 	formData := map[string]string{
 		"q":      text,
-		"source": from.Code,
-		"target": to.Code,
+		"source": from,
+		"target": to,
 	}
 
@@ -166,4 +162,4 @@
 	}
 
-	return TranslationResult{SourceLanguage: from, TranslatedText: responseJSON.TranslatedText}, nil
+	return TranslationResult{TranslatedText: responseJSON.TranslatedText}, nil
 }
Index: trunk/engines/reverso.go
===================================================================
--- trunk/engines/reverso.go	(revision 14)
+++ trunk/engines/reverso.go	(revision 15)
@@ -15,37 +15,35 @@
 func (_ *Reverso) DisplayName() string { return "Reverso" }
 
-var reversoLangs = []Language{
-	{Name: "Arabic", Code: "ara"},
-	{Name: "Chinese (Simplified)", Code: "chi"}, // marketed as just "Chinese"
-	{Name: "Czech", Code: "cze"},
-	{Name: "Danish", Code: "dan"},
-	{Name: "Dutch", Code: "dut"},
-	{Name: "English", Code: "eng"},
-	{Name: "French", Code: "fra"},
-	{Name: "German", Code: "ger"},
-	{Name: "Hebrew", Code: "heb"},
-	{Name: "Hindi", Code: "hin"},
-	{Name: "Hungarian", Code: "hun"},
-	{Name: "Italian", Code: "ita"},
-	{Name: "Japanese", Code: "jpn"},
-	{Name: "Korean", Code: "kor"},
-	{Name: "Persian", Code: "per"},
-	{Name: "Polish", Code: "pol"},
-	{Name: "Portuguese", Code: "por"},
-	{Name: "Romanian", Code: "rum"},
-	{Name: "Russian", Code: "rus"},
-	{Name: "Slovak", Code: "slo"},
-	{Name: "Spanish", Code: "spa"},
-	{Name: "Swedish", Code: "swe"},
-	{Name: "Thai", Code: "tha"},
-	{Name: "Turkish", Code: "tur"},
-	{Name: "Ukrainian", Code: "ukr"},
+var reversoLangs = Language{
+	"ara": "Arabic",
+	"chi": "Chinese (Simplified)", // marketed as just "Chinese"
+	"cze": "Czech",
+	"dan": "Danish",
+	"dut": "Dutch",
+	"eng": "English",
+	"fra": "French",
+	"ger": "German",
+	"heb": "Hebrew",
+	"hin": "Hindi",
+	"hun": "Hungarian",
+	"ita": "Italian",
+	"jpn": "Japanese",
+	"kor": "Korean",
+	"per": "Persian",
+	"pol": "Polish",
+	"por": "Portuguese",
+	"rum": "Romanian",
+	"rus": "Russian",
+	"slo": "Slovak",
+	"spa": "Spanish",
+	"swe": "Swedish",
+	"tha": "Thai",
+	"tur": "Turkish",
+	"ukr": "Ukrainian",
 }
 
-func (_ *Reverso) SourceLanguages() ([]Language, error) { return reversoLangs, nil }
+func (_ *Reverso) SourceLanguages() (Language, error) { return reversoLangs, nil }
 
-func (_ *Reverso) TargetLanguages() ([]Language, error) { return reversoLangs, nil }
-
-func (_ *Reverso) SupportsAutodetect() bool { return true }
+func (_ *Reverso) TargetLanguages() (Language, error) { return reversoLangs, nil }
 
 type reversoAPIResponse struct {
@@ -56,10 +54,10 @@
 }
 
-func (e *Reverso) callAPI(text string, from, to Language) (reversoAPIResponse, error) {
+func (e *Reverso) callAPI(text string, from, to string) (reversoAPIResponse, error) {
 	// `contextResults` must be false for language detection
 	formData := map[string]interface{}{
 		"format": "text",
-		"from":   from.Code,
-		"to":     to.Code,
+		"from":   from,
+		"to":     to,
 		"input":  text,
 		"options": map[string]interface{}{
@@ -110,25 +108,25 @@
 }
 
-func (e *Reverso) DetectLanguage(text string) (Language, error) {
+func (e *Reverso) DetectLanguage(text string) (string, error) {
 	// Any language pair works here, does not affect result
-	r, err := e.callAPI(text, reversoLangs[0], reversoLangs[1])
+	r, err := e.callAPI(text, "ara", "chi")
 
 	if err != nil {
-		return Language{}, err
+		return "", err
 	}
 
 	langCode := r.LanguageDetection.DetectedLanguage
 
-	for _, lang := range reversoLangs {
-		if lang.Code == langCode {
-			return lang, nil
+	for code := range reversoLangs {
+		if code == langCode {
+			return code, nil
 		}
 	}
 
-	return Language{}, fmt.Errorf("language code \"%s\" is not in Reverso's language list", langCode)
+	return "", fmt.Errorf("language code \"%s\" is not in Reverso's language list", langCode)
 }
 
-func (e *Reverso) Translate(text string, from, to Language) (TranslationResult, error) {
-	if from.Code == "auto" {
+func (e *Reverso) Translate(text string, from, to string) (TranslationResult, error) {
+	if from == "auto" {
 		from_, err := e.DetectLanguage(text)
 
@@ -155,5 +153,4 @@
 
 	return TranslationResult{
-		SourceLanguage: from,
 		TranslatedText: translation,
 	}, nil
Index: trunk/web/go.mod
===================================================================
--- trunk/web/go.mod	(revision 14)
+++ trunk/web/go.mod	(revision 15)
@@ -3,5 +3,8 @@
 go 1.16
 
-require codeberg.org/SimpleWeb/SimplyTranslate/engines v0.0.0
+require (
+	codeberg.org/SimpleWeb/SimplyTranslate/engines v0.0.0
+	github.com/gofiber/fiber/v2 v2.49.0 // indirect
+)
 
 replace codeberg.org/SimpleWeb/SimplyTranslate/engines v0.0.0 => ../engines
Index: trunk/web/go.sum
===================================================================
--- trunk/web/go.sum	(revision 14)
+++ trunk/web/go.sum	(revision 15)
@@ -1,10 +1,38 @@
 github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
 github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
+github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
+github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
 github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c=
 github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
+github.com/gofiber/fiber/v2 v2.49.0 h1:xBVG2c66GDcWfww56xHvMn52Q0XX7UrSvjj6MD8/5EE=
+github.com/gofiber/fiber/v2 v2.49.0/go.mod h1:oxpt7wQaEYgdDmq7nMxCGhilYicBLFnZ+jQSJcQDlSE=
+github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
+github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/klauspost/compress v1.16.3/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
+github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
+github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
+github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
+github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
+github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
+github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
+github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0=
+github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
+github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw=
+github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
+github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
+github.com/valyala/fasthttp v1.48.0 h1:oJWvHb9BIZToTQS3MuQ2R3bJZiNSa2KiNdeI8A+79Tc=
+github.com/valyala/fasthttp v1.48.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA=
+github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
+github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -12,4 +40,5 @@
 golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
 golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
@@ -25,8 +54,13 @@
 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
+golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
 golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
@@ -35,4 +69,5 @@
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
@@ -40,4 +75,5 @@
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
 golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
Index: trunk/web/main.go
===================================================================
--- trunk/web/main.go	(revision 14)
+++ trunk/web/main.go	(revision 15)
@@ -3,10 +3,41 @@
 import (
 	"codeberg.org/SimpleWeb/SimplyTranslate/engines"
+	"github.com/gofiber/fiber/v2"
 )
 
-// TODO: port web frontend to Go.
+func main() {
+	app := fiber.New()
 
-func main() {
-	engine := &engines.GoogleTranslate{}
-	print(engine.DisplayName())
+	app.All("/api/translate", func(c *fiber.Ctx) error {
+		from := ""
+		to := ""
+		engine := ""
+		text := ""
+		if c.Method() == "GET" {
+			engine = c.Query("engine")
+			text = c.Query("text")
+			from = c.Query("from")
+			to = c.Query("to")
+		} else if c.Method() == "POST" {
+			engine = c.FormValue("engine")
+			text = c.FormValue("text")
+			from = c.FormValue("from")
+			to = c.FormValue("to")
+		} else {
+			return c.SendStatus(400)
+		}
+		if engine == "" {
+			engine = "google"
+		}
+		if to == "" {
+			return c.SendStatus(400)
+		}
+		if result, err := engines.Engines[engine].Translate(text, from, to); err != nil {
+			return c.SendStatus(500)
+		} else {
+			return c.JSON(result)
+		}
+	})
+
+	app.Listen(":3000")
 }
