GET HTTP リクエストが機能しない問題を修正し、iCIBA および LibreTranslate エンジンを再追加し、マニュアル ページを作成し、Docker に関連するものを削除しました。

Signed-off-by: Izuru Yakumo <yakumo.izuru@chaotic.ninja>

git-svn-id: file:///srv/svn/repo/mai/trunk@60 e410bdd4-646f-c54f-a7ce-fffcc4f439ae
This commit is contained in:
yakumo.izuru 2024-01-20 23:01:02 +00:00
parent 0c83958125
commit 21388e220d
9 changed files with 528 additions and 28 deletions

View File

@ -1,7 +0,0 @@
FROM golang:1.20-alpine
COPY . .
RUN go mod download
RUN go build -o mai
EXPOSE 5000
CMD [ "./mai" ]

View File

@ -32,12 +32,12 @@ location / {
```
### 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.

View File

@ -38,8 +38,6 @@ func main() {
Views: engine,
})
api := app.Group("/api")
app.All("/", func(c *fiber.Ctx) error {
engine := c.Cookies("engine")
if c.Query("engine") != "" {
@ -139,7 +137,7 @@ func main() {
})
})
api.All("/api/translate", func(c *fiber.Ctx) error {
app.All("/api/translate", func(c *fiber.Ctx) error {
from := ""
to := ""
engine := ""
@ -170,7 +168,7 @@ func main() {
}
})
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 == "" {
engine = "google"
@ -182,7 +180,7 @@ func main() {
}
})
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 == "" {
engine = "google"
@ -194,7 +192,7 @@ func main() {
}
})
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 == "" {
engine = "google"
@ -221,7 +219,7 @@ func main() {
}
}
})
api.Get("/version", func(c *fiber.Ctx) error {
app.Get("/version", func(c *fiber.Ctx) error {
return c.JSON(fiber.Map{
"fiberversion": fiber.Version,
"goversion": runtime.Version(),

View File

@ -1,11 +0,0 @@
version: "3.6"
services:
mai:
container_name: mai
build:
context: .
dockerfile: Dockerfile
restart: unless-stopped
ports:
- 5000:5000

View File

@ -1,5 +1,9 @@
package engines
import (
"os"
)
type TranslationResult struct {
SourceLanguage string `json:"source_language"`
Definitions interface{} `json:"definitions"`
@ -20,4 +24,9 @@ type Language map[string]string
var Engines = map[string]Engine{
"google": &GoogleTranslate{},
"reverso": &Reverso{},
"iciba": &ICIBA{},
"libretranslate": &LibreTranslate{
InstanceURL: os.Getenv("MAI_LIBRETRANSLATE_INSTANCE"),
APIKey: os.Getenv("MAI_LIBRETRANSLATE_API"),
},
}

283
engines/iciba.go Normal file
View File

@ -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 }

169
engines/libretranslate.go Normal file
View File

@ -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
}

22
mai.1 Normal file
View File

@ -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

37
mai.ini.5 Normal file
View File

@ -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