Index: trunk/libretranslate.go
===================================================================
--- trunk/libretranslate.go	(revision 5)
+++ trunk/libretranslate.go	(revision 5)
@@ -0,0 +1,169 @@
+package simplytranslate_engines
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"net/http"
+)
+
+// LibreTranslateEngine is an engine that interfaces with any
+// [LibreTranslate](https://github.com/LibreTranslate/LibreTranslate) instance.
+type LibreTranslateEngine struct {
+	// InstanceURL is the URL to a LibreTranslate instance, for instance
+	// "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 (_ *LibreTranslateEngine) InternalName() string { return "libre" }
+
+func (_ *LibreTranslateEngine) DisplayName() string { return "LibreTranslate" }
+
+type libreLanguagesResponse []struct {
+	Name string `json:"name"`
+	Code string `json:"code"`
+}
+
+func (e *LibreTranslateEngine) 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 libreLanguagesResponse
+
+	if err := json.NewDecoder(response.Body).Decode(&langsResponse); err != nil {
+		return nil, err
+	}
+
+	langs := make([]Language, len(langsResponse))
+
+	for i, lang := range langsResponse {
+		langs[i] = Language{Name: lang.Name, Code: lang.Code}
+	}
+
+	return langs, nil
+
+}
+
+func (e *LibreTranslateEngine) SourceLanguages() ([]Language, error) { return e.getLangs() }
+
+func (e *LibreTranslateEngine) TargetLanguages() ([]Language, error) { return e.getLangs() }
+
+func (_ *LibreTranslateEngine) SupportsAutodetect() bool { return true }
+
+type libreDetectResponse []struct {
+	Confidence   float64 `json:"confidence"`
+	LanguageCode string  `json:"language"`
+}
+
+func (e *LibreTranslateEngine) DetectLanguage(text string) (Language, error) {
+	formData := map[string]string{"q": text}
+
+	if e.APIKey != "" {
+		formData["api_key"] = e.APIKey
+	}
+
+	formDataJSON, err := json.Marshal(formData)
+
+	if err != nil {
+		return Language{}, err
+	}
+
+	response, err := http.Post(e.InstanceURL+"/detect", "application/json", bytes.NewBuffer(formDataJSON))
+
+	if err != nil {
+		return Language{}, err
+	}
+
+	defer response.Body.Close()
+
+	if response.StatusCode != 200 {
+		return Language{}, 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 Language{}, err
+	}
+
+	maxConfidenceLang := langs[0]
+
+	for _, lang := range langs[1:] {
+		if lang.Confidence > maxConfidenceLang.Confidence {
+			maxConfidenceLang = lang
+		}
+	}
+
+	engineLangs, err := e.getLangs()
+
+	if err != nil {
+		return Language{}, err
+	}
+
+	for _, lang := range engineLangs {
+		if lang.Code == maxConfidenceLang.LanguageCode {
+			return lang, nil
+		}
+	}
+
+	return Language{}, fmt.Errorf("language code \"%s\" is not in the instance's language list", maxConfidenceLang.LanguageCode)
+}
+
+type libreTranslateResponse struct {
+	TranslatedText string `json:"translatedText"`
+}
+
+func (e *LibreTranslateEngine) Translate(text string, from Language, to Language) (TranslationResult, error) {
+	formData := map[string]string{
+		"q":      text,
+		"source": from.Code,
+		"target": to.Code,
+	}
+
+	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 libreTranslateResponse
+
+	if err := json.NewDecoder(response.Body).Decode(&responseJSON); err != nil {
+		return TranslationResult{}, err
+	}
+
+	return TranslationResult{SourceLanguage: from, TranslatedText: responseJSON.TranslatedText}, nil
+}
