Index: unk/Dockerfile
===================================================================
--- /trunk/Dockerfile	(revision 59)
+++ 	(revision )
@@ -1,7 +1,0 @@
-FROM golang:1.20-alpine
-
-COPY . .
-RUN go mod download
-RUN go build -o mai
-EXPOSE 5000
-CMD [ "./mai" ]
Index: /trunk/README.md
===================================================================
--- /trunk/README.md	(revision 59)
+++ /trunk/README.md	(revision 60)
@@ -33,10 +33,10 @@
 
 ### Legal notice
-Mai does not host any content. All content shown on any Mai/Mozhi/SimplyTranslate instances is from [Google Translate](https://translate.google.com) and [Reverso](https://www.reverso.net/). 
+Mai does not host any content. All content shown on any Mai instances is from [Google Translate](https://translate.google.com), [Reverso](https://www.reverso.net/), [iCIBA](https://www.iciba.net) and [LibreTranslate](https://libretranslate.com)
 
-Mai is not affiliated with Google Translate nor Reverso, which this program relays.
+Mai is not affiliated with none of the above, which this program relays.
 
 Trademarks belong to their respective owners. 
-Google Translate is a trademark of [Google LLC](https://www.google.com). Reverso is a trademark of Reverso.
+Google Translate is a trademark of [Google LLC](https://www.google.com). Reverso is a trademark of Reverso, et cetera.
 
 The creators and maintainers of this repository assume no liability for the accuracy and timeliness of any information provided above. Trademark owner information was researched to the best of the author's knowledge at the time of curation and may be outdated or incorrect.
Index: /trunk/cmd/mai/main.go
===================================================================
--- /trunk/cmd/mai/main.go	(revision 59)
+++ /trunk/cmd/mai/main.go	(revision 60)
@@ -39,6 +39,4 @@
 	})
 
-	api := app.Group("/api")
-
 	app.All("/", func(c *fiber.Ctx) error {
 		engine := c.Cookies("engine")
@@ -140,5 +138,5 @@
 	})
 
-	api.All("/api/translate", func(c *fiber.Ctx) error {
+	app.All("/api/translate", func(c *fiber.Ctx) error {
 		from := ""
 		to := ""
@@ -171,5 +169,5 @@
 	})
 
-	api.Get("/api/source_languages", func(c *fiber.Ctx) error {
+	app.Get("/api/source_languages", func(c *fiber.Ctx) error {
 		engine := c.Query("engine")
 		if _, ok := engines.Engines[engine]; !ok || engine == "" {
@@ -183,5 +181,5 @@
 	})
 
-	api.Get("/api/target_languages", func(c *fiber.Ctx) error {
+	app.Get("/api/target_languages", func(c *fiber.Ctx) error {
 		engine := c.Query("engine")
 		if _, ok := engines.Engines[engine]; !ok || engine == "" {
@@ -195,5 +193,5 @@
 	})
 
-	api.Get("/api/tts", func(c *fiber.Ctx) error {
+	app.Get("/api/tts", func(c *fiber.Ctx) error {
 		engine := c.Query("engine")
 		if _, ok := engines.Engines[engine]; !ok || engine == "" {
@@ -222,5 +220,5 @@
 		}
 	})
-	api.Get("/version", func(c *fiber.Ctx) error {
+	app.Get("/version", func(c *fiber.Ctx) error {
 		return c.JSON(fiber.Map{
 			"fiberversion": fiber.Version,
Index: unk/docker-compose.yml
===================================================================
--- /trunk/docker-compose.yml	(revision 59)
+++ 	(revision )
@@ -1,11 +1,0 @@
-version: "3.6"
-
-services:
-  mai:
-    container_name: mai
-    build: 
-      context: .
-      dockerfile: Dockerfile
-    restart: unless-stopped
-    ports:
-      - 5000:5000
Index: /trunk/engines/engine.go
===================================================================
--- /trunk/engines/engine.go	(revision 59)
+++ /trunk/engines/engine.go	(revision 60)
@@ -1,3 +1,7 @@
 package engines
+
+import (
+	"os"
+)
 
 type TranslationResult struct {
@@ -21,3 +25,8 @@
 	"google": &GoogleTranslate{},
 	"reverso": &Reverso{},
+	"iciba": &ICIBA{},
+	"libretranslate": &LibreTranslate{
+		InstanceURL: os.Getenv("MAI_LIBRETRANSLATE_INSTANCE"),
+		APIKey: os.Getenv("MAI_LIBRETRANSLATE_API"),
+	},
 }
Index: /trunk/engines/iciba.go
===================================================================
--- /trunk/engines/iciba.go	(revision 60)
+++ /trunk/engines/iciba.go	(revision 60)
@@ -0,0 +1,283 @@
+package engines
+
+import (
+	"crypto/md5"
+	"encoding/hex"
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"net/url"
+)
+
+// iCIBA is an engine that fetches data from https://www.iciba.com.
+type ICIBA struct{}
+
+func (_ *ICIBA) DisplayName() string { return "iCIBA" }
+
+var icibaLanguages = Language{
+	// ICIBA does have an API, but they return Chinese names.
+	// For languages already present in Google translate, the English
+	// names in that engine file are used; Otherwise official names
+	// as researched on Wikipedia are used. They're validated against
+	// the Chinese names to the best of my ability.
+	// Missing "cni", "kbh", "tmh"
+	// due to conflict between ISO-639 table and Chinese label
+	// one "//" means on iciba but not on google
+	"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) {
+	icibaLanguagesCopy := make(Language)
+	for k, v := range icibaLanguages {
+		icibaLanguagesCopy[k] = v
+	}
+
+	icibaLanguagesCopy["auto"] = "Detect Language"
+	return icibaLanguagesCopy, nil
+}
+
+func (_ *ICIBA) TargetLanguages() (Language, error) { return icibaLanguages, nil }
+
+func (_ *ICIBA) Translate(text string, from, to string) (TranslationResult, error) {
+	requestURL, _ := url.Parse("https://ifanyi.iciba.com/index.php")
+
+	query := url.Values{}
+	query.Add("c", "trans")
+	query.Add("m", "fy")
+	query.Add("client", "6")
+	query.Add("auth_user", "key_web_fanyi")
+
+	sum := md5.Sum([]byte(("6key_web_fanyiifanyiweb8hc9s98e" + text)))
+	query.Add("sign", hex.EncodeToString(sum[:])[:16])
+
+	requestURL.RawQuery = query.Encode()
+
+	formData := url.Values{}
+	formData.Add("from", from)
+	formData.Add("to", to)
+	formData.Add("q", text)
+
+	response, err := http.PostForm(requestURL.String(), formData)
+	if err != nil {
+		return TranslationResult{}, err
+	}
+
+	defer response.Body.Close()
+
+	if response.StatusCode != 200 {
+		return TranslationResult{}, fmt.Errorf("got status code %d from iCIBA", response.StatusCode)
+	}
+
+	var responseJSON struct {
+		Content struct {
+			From string `json:"from"`
+			Out  string `json:"out"`
+		} `json:"content"`
+	}
+
+	if err := json.NewDecoder(response.Body).Decode(&responseJSON); err != nil {
+		return TranslationResult{}, err
+	}
+
+	for code := range icibaLanguages {
+		if code == responseJSON.Content.From {
+			from = code
+			break
+		}
+	}
+
+	if from == "" {
+		return TranslationResult{TranslatedText: responseJSON.Content.Out},
+			fmt.Errorf("language code \"%s\" is not in iCIBA's language list", responseJSON.Content.From)
+	}
+
+	return TranslationResult{
+		SourceLanguage: from,
+		TranslatedText: responseJSON.Content.Out,
+	}, nil
+}
+
+func (_ *ICIBA) Tts(text, lang string) (string, error) { return "", nil }
Index: /trunk/engines/libretranslate.go
===================================================================
--- /trunk/engines/libretranslate.go	(revision 60)
+++ /trunk/engines/libretranslate.go	(revision 60)
@@ -0,0 +1,169 @@
+package engines
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"net/http"
+)
+
+// LibreTranslate is an engine that interfaces with any
+// [LibreTranslate](https://github.com/LibreTranslate/LibreTranslate) instance.
+type LibreTranslate struct {
+	// InstanceURL is the URL to a LibreTranslate instance, for example
+	// "https://libretranslate.com".
+	InstanceURL string
+	// APIKey is the API key for the given instance. If empty, then no API
+	// key will be sent along with requests to the instance.
+	//
+	// Some instances issue API keys to users so that they can have a
+	// higher rate limit. See
+	// https://github.com/LibreTranslate/LibreTranslate#manage-api-keys for
+	// more information.
+	APIKey string
+}
+
+func (_ *LibreTranslate) DisplayName() string { return "LibreTranslate" }
+
+func (e *LibreTranslate) getLangs() (Language, error) {
+	response, err := http.Get(e.InstanceURL + "/languages")
+
+	if err != nil {
+		return nil, err
+	}
+
+	defer response.Body.Close()
+
+	if response.StatusCode != 200 {
+		return nil, fmt.Errorf("got status code %d from LibreTranslate API", response.StatusCode)
+	}
+
+	var langsResponse []struct {
+		Name string `json:"name"`
+		Code string `json:"code"`
+	}
+
+	if err := json.NewDecoder(response.Body).Decode(&langsResponse); err != nil {
+		return nil, err
+	}
+
+	langs := Language{}
+
+	for _, lang := range langsResponse {
+		langs[lang.Code] = lang.Name
+	}
+
+	return langs, nil
+}
+
+func (e *LibreTranslate) SourceLanguages() (Language, error) { return e.getLangs() }
+
+func (e *LibreTranslate) TargetLanguages() (Language, error) { return e.getLangs() }
+
+func (e *LibreTranslate) Tts(text, lang string) (string, error) { return "", nil }
+
+type libreDetectResponse []struct {
+	Confidence   float64 `json:"confidence"`
+	LanguageCode string  `json:"language"`
+}
+
+func (e *LibreTranslate) detectLanguage(text string) (string, error) {
+	formData := map[string]string{"q": text}
+
+	if e.APIKey != "" {
+		formData["api_key"] = e.APIKey
+	}
+
+	formDataJSON, err := json.Marshal(formData)
+
+	if err != nil {
+		return "", err
+	}
+
+	response, err := http.Post(e.InstanceURL+"/detect", "application/json", bytes.NewBuffer(formDataJSON))
+
+	if err != nil {
+		return "", err
+	}
+
+	defer response.Body.Close()
+
+	if response.StatusCode != 200 {
+		return "", fmt.Errorf("got status code %d from LibreTranslate API", response.StatusCode)
+	}
+
+	var langs libreDetectResponse
+
+	if err := json.NewDecoder(response.Body).Decode(&langs); err != nil {
+		return "", err
+	}
+
+	maxConfidenceLang := langs[0]
+
+	for _, lang := range langs[1:] {
+		if lang.Confidence > maxConfidenceLang.Confidence {
+			maxConfidenceLang = lang
+		}
+	}
+
+	engineLangs, err := e.getLangs()
+
+	if err != nil {
+		return "", err
+	}
+
+	for code := range engineLangs {
+		if code == maxConfidenceLang.LanguageCode {
+			return code, nil
+		}
+	}
+
+	return "", fmt.Errorf("language code \"%s\" is not in the instance's language list", maxConfidenceLang.LanguageCode)
+}
+
+func (e *LibreTranslate) Translate(text string, from, to string) (TranslationResult, error) {
+	formData := map[string]string{
+		"q":      text,
+		"source": from,
+		"target": to,
+	}
+
+	if e.APIKey != "" {
+		formData["api_key"] = e.APIKey
+	}
+
+	formDataJSON, err := json.Marshal(formData)
+
+	if err != nil {
+		return TranslationResult{}, err
+	}
+
+	response, err := http.Post(e.InstanceURL+"/translate", "application/json", bytes.NewBuffer(formDataJSON))
+
+	if err != nil {
+		return TranslationResult{}, err
+	}
+
+	defer response.Body.Close()
+
+	if response.StatusCode != 200 {
+		return TranslationResult{}, fmt.Errorf("got status code %d from LibreTranslate API", response.StatusCode)
+	}
+
+	var responseJSON struct {
+		TranslatedText string `json:"translatedText"`
+	}
+
+	if err := json.NewDecoder(response.Body).Decode(&responseJSON); err != nil {
+		return TranslationResult{}, err
+	}
+
+	if r, err := e.detectLanguage(text); err == nil {
+		from = r
+	}
+
+	return TranslationResult{
+		TranslatedText: responseJSON.TranslatedText,
+		SourceLanguage: from,
+	}, nil
+}
Index: /trunk/mai.1
===================================================================
--- /trunk/mai.1	(revision 60)
+++ /trunk/mai.1	(revision 60)
@@ -0,0 +1,22 @@
+.Dd $Mdocdate$
+.Dt MAI 1
+.Os
+.Sh NAME
+.Nm mai
+.Nd An usable frontend for popular translation engines
+.Sh SYNOPSIS
+.Nm
+.Op Fl f Ar config-file
+.Sh DESCRIPTION
+This project has been forked from the remains of the
+rewrite of the SimplyTranslate project, which is currently
+maintained by ManeraKai, but as time passed, this fork
+diverged wildly, and was renamed.
+Much to anyone's dismay, this was named after
+.Lk https://en.touhouwiki.net/wiki/Mai Mai from Mystic Square
+.Sh AUTHORS
+.An fattalion
+.An metalune
+.An ManeraKai
+.Sh MAINTAINERS
+.An Izuru Yakumo Aq Mt yakumo.izuru@chaotic.ninja
Index: /trunk/mai.ini.5
===================================================================
--- /trunk/mai.ini.5	(revision 60)
+++ /trunk/mai.ini.5	(revision 60)
@@ -0,0 +1,37 @@
+.Dd $Mdocdate$
+.Dt MAI.INI 5
+.Os
+.Sh NAME
+.Nm mai.ini
+.Nd Configuration file for Mai
+.Sh OPTIONS
+.Bl -tag -width 11n
+.It listen
+HTTP port for the server to listen.
+Default is "localhost:5000"
+.It rootdir
+Directory where the static resources are located.
+Default is "./static"
+.It tmplpath
+Directory where the templates are located.
+Default is "./views"
+.El
+.Sh ENVIRONMENT
+.Bl -tag -width 11n
+.It Ev MAI_LIBRETRANSLATE_INSTANCE
+LibreTranslate instance to use.
+Default is ""
+.It Ev MAI_LIBRETRANSLATE_API
+LibreTranslate API key for the above setting.
+Default is ""
+.El
+.Sh FILES
+.Pa /usr/local/etc/mai/mai.ini
+.Sh SEE ALSO
+.Xr mai 1
+.Sh AUTHORS
+.An fattalion
+.An metalune
+.An ManeraKai
+.Sh MAINTAINERS
+.An Izuru Yakumo Aq Mt yakumo.izuru@chaotic.ninja
