feat: add translation config ui

Signed-off-by: WindSpiritSR <simon343riley@gmail.com>
This commit is contained in:
WindSpiritSR 2021-06-20 01:31:34 +08:00
parent 7a50aa5ee2
commit bee1726dc0
14 changed files with 1125 additions and 123 deletions

View File

@ -25,5 +25,4 @@ casdoorEndpoint = http://localhost:8000
clientId = 014ae4bd048734ca2dea
clientSecret = xxx
jwtSecret = CasdoorSecret
casdoorOrganization = "casbin-forum"
googleTranslationKey = xxx
casdoorOrganization = "casbin-forum"

View File

@ -17,11 +17,6 @@ package controllers
import (
"encoding/base64"
"encoding/json"
beego "github.com/beego/beego/v2/adapter"
"io/ioutil"
"net/http"
"net/url"
"regexp"
"strconv"
"strings"
"time"
@ -651,82 +646,17 @@ func (c *ApiController) TranslateTopic() {
topicId := util.ParseInt(topicIdStr)
var result TopicTranslateData
translateData := &object.TranslateData{}
topic := object.GetTopic(topicId)
if topic == nil || topic.Deleted {
result.ErrMsg = "Invalid TopicId"
c.Data["json"] = result
translateData.ErrMsg = "Invalid TopicId"
c.Data["json"] = translateData
c.ServeJSON()
return
}
contentStr := topic.Content
replaceStr := "<code>RplaceWithCasnodeTranslator<code/>"
contentReg := regexp.MustCompile(`(?s)\x60{1,3}[^\x60](.*?)\x60{1,3}`)
translateReg := regexp.MustCompile(replaceStr)
codeBlocks := contentReg.FindAllString(contentStr, -1)
var cbList []string
if codeBlocks != nil {
for _, cbItem := range codeBlocks {
cbList = append(cbList, cbItem)
}
}
contentStr = contentReg.ReplaceAllString(contentStr, replaceStr)
params := url.Values{
"target": {targetLang},
"format": {"text"},
"key": {beego.AppConfig.String("googleTranslationKey")},
"q": {contentStr},
}
resp, _ := http.PostForm("https://translation.googleapis.com/language/translate/v2", params)
defer resp.Body.Close()
respByte, _ := ioutil.ReadAll(resp.Body)
var translateResp GoogleTranslationResult
translateResp.Error.Code = 0
err := json.Unmarshal(respByte, &translateResp)
if err != nil {
panic(err)
}
translateStr := translateResp.Data.Translations[0].TranslatedText
detectSrcLang := translateResp.Data.Translations[0].DetectedSourceLanguage
replacedCb := translateReg.FindAllString(translateStr, -1)
var replacedCbList []string
if replacedCb != nil {
for _, replacedCbItem := range replacedCb {
replacedCbList = append(replacedCbList, replacedCbItem)
}
}
if len(replacedCbList) != len(codeBlocks) {
result.ErrMsg = "Translate Failed"
c.Data["json"] = result
c.ServeJSON()
return
}
replaceIndex := 0
translateStr = translateReg.ReplaceAllStringFunc(translateStr, func(src string) string {
replaceIndex = replaceIndex + 1
return cbList[replaceIndex-1]
})
if translateResp.Error.Code != 0 {
result.ErrMsg = translateResp.Error.Message
} else {
result.SrcLang = detectSrcLang
result.Target = translateStr
}
c.Data["json"] = result
c.Data["json"] = object.StrTranslate(topic.Content, targetLang)
c.ServeJSON()
return
}

100
controllers/translator.go Normal file
View File

@ -0,0 +1,100 @@
package controllers
import (
"encoding/json"
"github.com/casbin/casnode/object"
)
func (c *ApiController) UpdateTranslator() {
var resp Response
if !object.CheckModIdentity(c.GetSessionUsername()) {
resp = Response{Status: "fail", Msg: "Unauthorized."}
c.Data["json"] = resp
c.ServeJSON()
return
}
var translator object.Translator
err := json.Unmarshal(c.Ctx.Input.RequestBody, &translator)
if err != nil {
panic(err)
}
object.UpdateTranslator(translator)
c.Data["json"] = Response{Status: "ok", Msg: "success"}
c.ServeJSON()
}
func (c *ApiController) AddTranslator() {
var resp Response
if !object.CheckModIdentity(c.GetSessionUsername()) {
resp = Response{Status: "fail", Msg: "Unauthorized."}
c.Data["json"] = resp
c.ServeJSON()
return
}
var translator object.Translator
err := json.Unmarshal(c.Ctx.Input.RequestBody, &translator)
if err != nil {
panic(err)
}
res := object.GetTranslator(translator.Id)
if len(*res) > 0 {
resp = Response{Status: "fail", Msg: "Translator ID existed"}
c.Data["json"] = resp
c.ServeJSON()
return
}
if object.AddTranslator(translator) {
resp = Response{Status: "ok", Msg: "Success"}
c.Data["json"] = resp
c.ServeJSON()
return
} else {
resp = Response{Status: "fail", Msg: "Add translator failed"}
c.Data["json"] = resp
c.ServeJSON()
return
}
}
func (c *ApiController) GetTranslator() {
id := c.Input().Get("id")
res := object.GetTranslator(id)
c.Data["json"] = res
c.ServeJSON()
}
func (c *ApiController) GetEnableTranslator() {
res := object.GetEnableTranslator()
c.Data["json"] = res
c.ServeJSON()
}
func (c *ApiController) VisibleTranslator() {
resp := Response{Status: "ok", Data: false}
res := object.GetEnableTranslator()
if res != nil {
if res.Visible {
resp = Response{Status: "ok", Data: true}
}
}
c.Data["json"] = resp
c.ServeJSON()
}
func (c *ApiController) DelTranslator() {
id := c.Input().Get("id")
resp := Response{Status: "fail", Msg: "Delete translator failed"}
if object.DelTranslator(id) {
resp = Response{Status: "ok", Msg: "Success"}
}
c.Data["json"] = resp
c.ServeJSON()
}

View File

@ -84,27 +84,3 @@ type adminNodeInfo struct {
TopicNum int `json:"topicNum"`
FavoritesNum int `json:"favoritesNum"`
}
type TopicTranslateData struct {
SrcLang string `json:"srcLang"`
Target string `json:"target"`
ErrMsg string `json:"err_msg"`
}
type GoogleTranslationResult struct {
Data struct {
Translations []struct {
TranslatedText string `json:"translatedText"`
DetectedSourceLanguage string `json:"detectedSourceLanguage"`
} `json:"translations"`
} `json:"data"`
Error struct {
Code int `json:"code"`
Message string `json:"message"`
Errors []struct {
Message string `json:"message"`
Domain string `json:"domain"`
Reason string `json:"reason"`
} `json:"errors"`
} `json:"error"`
}

View File

@ -120,6 +120,11 @@ func (a *Adapter) createTable() {
panic(err)
}
err = a.Engine.Sync2(new(Translator))
if err != nil {
panic(err)
}
err = a.Engine.Sync2(new(Node))
if err != nil {
panic(err)

174
object/translator.go Normal file
View File

@ -0,0 +1,174 @@
package object
import (
"encoding/json"
"io/ioutil"
"net/http"
"net/url"
"regexp"
)
type Translator struct {
Id string `xorm:"varchar(50) notnull pk" json:"id"`
Name string `xorm:"varchar(50)" json:"name"`
Translator string `xorm:"varchar(50)" json:"translator"`
Key string `xorm:"varchar(200)" json:"key"`
Enable bool `xorm:"bool" json:"enable"`
Visible bool `xorm:"bool" json:"visible"`
}
type TranslateData struct {
SrcLang string `json:"srcLang"`
Target string `json:"target"`
ErrMsg string `json:"err_msg"`
}
type GoogleTranslationResult struct {
Data struct {
Translations []struct {
TranslatedText string `json:"translatedText"`
DetectedSourceLanguage string `json:"detectedSourceLanguage"`
} `json:"translations"`
} `json:"data"`
Error struct {
Code int `json:"code"`
Message string `json:"message"`
Errors []struct {
Message string `json:"message"`
Domain string `json:"domain"`
Reason string `json:"reason"`
} `json:"errors"`
} `json:"error"`
}
func StrTranslate(srcStr, targetLang string) *TranslateData {
replaceStr := "<code>RplaceWithCasnodeTranslator<code/>"
contentReg := regexp.MustCompile(`(?s)\x60{1,3}[^\x60](.*?)\x60{1,3}`)
translateReg := regexp.MustCompile(replaceStr)
translateData := &TranslateData{}
translator := GetEnableTranslator()
if translator == nil || !translator.Visible {
translateData.ErrMsg = "Translate Failed"
return translateData
}
codeBlocks := contentReg.FindAllString(srcStr, -1)
var cbList []string
if codeBlocks != nil {
for _, cbItem := range codeBlocks {
cbList = append(cbList, cbItem)
}
}
srcStr = contentReg.ReplaceAllString(srcStr, replaceStr)
params := url.Values{
"target": {targetLang},
"format": {"text"},
"key": {translator.Key},
"q": {srcStr},
}
resp, _ := http.PostForm("https://translation.googleapis.com/language/translate/v2", params)
defer resp.Body.Close()
respByte, _ := ioutil.ReadAll(resp.Body)
var translateResp GoogleTranslationResult
translateResp.Error.Code = 0
err := json.Unmarshal(respByte, &translateResp)
if err != nil {
panic(err)
}
translateStr := translateResp.Data.Translations[0].TranslatedText
detectSrcLang := translateResp.Data.Translations[0].DetectedSourceLanguage
replacedCb := translateReg.FindAllString(translateStr, -1)
var replacedCbList []string
if replacedCb != nil {
for _, replacedCbItem := range replacedCb {
replacedCbList = append(replacedCbList, replacedCbItem)
}
}
if len(replacedCbList) != len(codeBlocks) {
translateData.ErrMsg = "Translate Failed"
return translateData
}
replaceIndex := 0
translateStr = translateReg.ReplaceAllStringFunc(translateStr, func(src string) string {
replaceIndex = replaceIndex + 1
return cbList[replaceIndex-1]
})
if translateResp.Error.Code != 0 {
translateData.ErrMsg = translateResp.Error.Message
} else {
translateData.SrcLang = detectSrcLang
translateData.Target = translateStr
}
return translateData
}
func AddTranslator(translator Translator) bool {
affected, err := adapter.Engine.Insert(translator)
if err != nil {
panic(err)
}
return affected != 0
}
func GetTranslator(id string) *[]Translator {
translators := []Translator{}
var err error
if id != "" {
err = adapter.Engine.Where("id = ?", id).Find(&translators)
} else {
err = adapter.Engine.Find(&translators)
}
if err != nil {
panic(err)
}
return &translators
}
func GetEnableTranslator() *Translator {
var translator Translator
resultNum, err := adapter.Engine.Where("enable = ?", true).Get(&translator)
if err != nil {
panic(err)
}
if resultNum {
return &translator
}
return nil
}
func UpdateTranslator(translator Translator) bool {
_, err := adapter.Engine.Where("enable = ?", true).Cols("enable").Update(Translator{Enable: false})
if err != nil {
return false
}
affected, err := adapter.Engine.Id(translator.Id).AllCols().Update(translator)
if err != nil {
panic(err)
}
return affected != 0
}
func DelTranslator(id string) bool {
affected, err := adapter.Engine.Where("id = ?", id).Delete(&Translator{})
if err != nil {
panic(err)
}
return affected != 0
}

View File

@ -100,6 +100,12 @@ func initAPI() {
beego.Router("/api/update-poster", &controllers.ApiController{}, "POST:UpdatePoster") //Update poster api just for admin.
beego.Router("/api/read-poster", &controllers.ApiController{}, "GET:ReadPoster")
beego.Router("/api/update-translator", &controllers.ApiController{}, "POST:UpdateTranslator") //Update translator api just for admin.
beego.Router("/api/add-translator", &controllers.ApiController{}, "POST:AddTranslator") //Add translator api just for admin.
beego.Router("/api/del-translator", &controllers.ApiController{}, "POST:DelTranslator") //Delete translator api just for admin.
beego.Router("/api/get-translator", &controllers.ApiController{}, "GET:GetTranslator")
beego.Router("/api/visible-translator", &controllers.ApiController{}, "GET:VisibleTranslator")
beego.Router("/api/get-nodes", &controllers.ApiController{}, "GET:GetNodes")
beego.Router("/api/get-node", &controllers.ApiController{}, "GET:GetNode")
beego.Router("/api/update-node", &controllers.ApiController{}, "POST:UpdateNode") // Update node api just for admin.

View File

@ -63,6 +63,7 @@ import AdminPlane from "./admin/AdminPlane";
import AdminPoster from "./admin/AdminPoster";
import AdminTopic from "./admin/AdminTopic";
import AdminSensitive from "./admin/AdminSensitive";
import AdminTranslation from "./admin/AdminTranslation";
import i18next from "i18next";
import "./node.css";
import "./i18n";
@ -406,6 +407,12 @@ class App extends Component {
<AdminPoster />
</div>
</Route>
<Route exact path="/admin/translation">
<div id={pcBrowser ? "Main" : ""}>
{pcBrowser ? <div className="sep20" /> : null}
<AdminTranslation />
</div>
</Route>
<Route exact path="/admin/member">
<div id={pcBrowser ? "Main" : ""}>
{pcBrowser ? <div className="sep20" /> : null}

View File

@ -59,6 +59,11 @@ class AdminHomepage extends React.Component {
value: "sensitive",
image: Setting.getStatic("/static/img/settings.png"),
},
{
label: i18next.t("admin:Translation management"),
value: "translation",
image: Setting.getStatic("/static/img/settings.png"),
},
],
message: "",
};

View File

@ -0,0 +1,671 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import { withRouter, Link } from "react-router-dom";
import * as TranslatorBackend from "../backend/TranslatorBackend";
import * as Setting from "../Setting";
import i18next from "i18next";
import Select2 from "react-select2-wrapper";
import i18n from "i18next";
class AdminTranslation extends React.Component {
constructor(props) {
super(props);
this.state = {
message: "",
errMessage: "",
translators: [],
apis: ["Google"],
form: {
id: "",
name: "",
translator: "",
key: "",
enable: false,
visible: false,
},
event: "config",
Management_LIST: [
{ label: "Config Translator", value: "config" },
{ label: "Create", value: "create" },
{ label: "Manage", value: "manage" },
],
color: "",
backgroundColor: "",
displayColorPicker: false,
displayBackgroundColorPicker: false,
};
this.state.url = "/admin/translation";
}
getAll() {
TranslatorBackend.getTranslator().then((res) => {
this.setState(
{
translators: res,
},
() => {
this.getEnableFromList(this.state.translators);
}
);
});
}
getEnableFromList(translatorArray) {
if (translatorArray) {
translatorArray.forEach((item) => {
if (item.enable) {
this.setState({
form: item,
});
}
});
}
}
renderSelectPlatform() {
if (this.state.translators === null) return;
return (
<Select2
value={this.getIndexFromTranslatorId(this.state.form.id)}
style={{
width: Setting.PcBrowser ? "300px" : "200px",
fontSize: "14px",
}}
data={this.state.translators.map((translatorItem, index) => {
return {
text: `${translatorItem.name} / ${translatorItem.translator}`,
id: index,
};
})}
onSelect={(event) => {
const selected = event.target.value;
if (selected === null) {
return;
}
const selectedIndex = parseInt(selected);
this.setState({
form: this.state.translators[selectedIndex],
});
}}
options={{
placeholder: i18next.t(
"translator:Please select a translator platform"
),
}}
/>
);
}
renderSelectAPI() {
if (this.state.apis === null) return;
return (
<Select2
value={this.state.form.translator}
style={{
width: Setting.PcBrowser ? "300px" : "200px",
fontSize: "14px",
}}
data={this.state.apis.map((apiItem) => {
return { text: apiItem, id: apiItem };
})}
onSelect={(event) => {
const selected = event.target.value;
if (selected === null) {
return;
}
this.setState((prevState) => {
prevState.form.translator = selected;
return prevState;
});
}}
options={{
placeholder: i18next.t("translator:Please select a translator api"),
}}
/>
);
}
getIndexFromTranslatorId(translatorId) {
for (let i = 0; i < this.state.translators.length; i++) {
if (this.state.translators[i].id === translatorId) {
return i;
}
}
return -1;
}
updateTranslator() {
this.clearMessage();
this.clearErrorMessage();
if (this.state.form.id === "") {
this.setState({
errMessage: i18next.t("translator:Please select a translator platform"),
});
return;
}
if (this.state.form.key === "") {
this.setState({ errMessage: i18next.t("translator:Please input key") });
return;
}
this.setState(
(prevState) => {
prevState.form.enable = true;
return prevState;
},
() => {
TranslatorBackend.updateTranslator(this.state.form).then((res) => {
if (res.status === "ok") {
this.setState({
message: i18next.t(
"translator:Update translator information success"
),
});
} else {
this.setState({
errMessage: res?.msg,
});
}
});
}
);
}
addTranslator() {
this.clearMessage();
this.clearErrorMessage();
this.setState((prevState) => {
prevState.form.enable = false;
return prevState;
});
if (this.state.form.id === "") {
this.setState({ errMessage: i18next.t("translator:Please input id") });
return;
}
if (this.state.form.name === "") {
this.setState({ errMessage: i18next.t("translator:Please input name") });
return;
}
if (this.state.form.translator === "") {
this.setState({
errMessage: i18next.t("translator:Please select a translator api"),
});
return;
}
if (this.state.form.key === "") {
this.setState({ errMessage: i18next.t("translator:Please input key") });
return;
}
TranslatorBackend.addTranslator(this.state.form).then((res) => {
if (res.status === "ok") {
this.setState({
message: i18next.t("translator:Add translator information success"),
});
} else {
this.setState({
errMessage: i18next.t(`error:${res?.msg}`),
});
}
});
}
deleteTranslator(translator) {
if (
window.confirm(
`${i18next.t(`translator:Are you sure to delete translator`)} ${
translator.name
} ?`
)
) {
if (translator.enable) {
alert(i18next.t("translator:Please disable it first"));
return;
}
TranslatorBackend.delTranslator(translator.id).then((res) => {
if (res.status === "ok") {
this.setState({
message: i18next.t("translator:Delete translator success"),
});
this.setState({
translators: this.state.translators.reduce((total, current) => {
current.id !== translator.id && total.push(current);
return total;
}, []),
});
} else {
this.setState({
errMessage: i18next.t(`error:${res?.msg}`),
});
}
});
}
}
clearMessage() {
this.setState({
message: "",
});
}
clearErrorMessage() {
this.setState({
errMessage: "",
});
}
changeEvent(event) {
switch (event) {
case "config":
this.getAll();
break;
case "create":
this.setState({
form: {
id: "",
name: "",
translator: "",
key: "",
enable: false,
visible: false,
},
});
break;
case "manage":
this.getAll();
break;
}
this.setState({
event: event,
message: "",
errMessage: "",
});
}
renderProblem() {
let problems = [];
if (this.state.errMessage !== "") {
problems.push(this.state.errMessage);
}
if (problems.length === 0) {
return null;
}
return (
<div className="problem" onClick={() => this.clearErrorMessage()}>
{i18next.t(
"error:Please resolve the following issues before submitting"
)}
<ul>
{problems.map((problem) => {
return <li>{problem}</li>;
})}
</ul>
</div>
);
}
componentDidMount() {
this.getAll();
}
renderManagementList(item) {
return (
<a
href="javascript:void(0);"
className={this.state.event === item.value ? "tab_current" : "tab"}
onClick={() => this.changeEvent(item.value)}
>
{i18next.t(`translator:${item.label}`)}
</a>
);
}
renderTranslators(translator) {
const pcBrowser = Setting.PcBrowser;
return (
<div className="cell">
<table cellPadding="0" cellSpacing="0" border="0" width="100%">
<tbody>
<tr>
<td width={pcBrowser ? "60" : "auto"} align="left">
<span>{translator?.id}</span>
</td>
<td width={pcBrowser ? "200" : "auto"} align="center">
<span>{translator?.name}</span>
</td>
<td width={pcBrowser ? "200" : "auto"} align="center">
<span>{translator?.translator}</span>
</td>
<td width={pcBrowser ? "200" : "auto"} align="center">
{translator?.enable ? (
<span className="positive">
{i18next.t("translator:Enable")}
</span>
) : (
<span className="gray">
{i18next.t("translator:Disable")}
</span>
)}
</td>
<td>
<a onClick={() => this.deleteTranslator(translator)}>
{i18next.t("translator:Delete")}
</a>
</td>
</tr>
</tbody>
</table>
</div>
);
}
renderFormBox(form) {
return (
<div>
<div className="box">
<div className="header">
<Link to="/">{Setting.getForumName()}</Link>{" "}
<span className="chevron">&nbsp;&nbsp;</span>
<Link to="/admin">{i18next.t("admin:Backstage management")}</Link>
<span className="chevron">&nbsp;&nbsp;</span>{" "}
{i18next.t("translator:Translator management")}
</div>
<div className="cell">
{this.state.Management_LIST.map((item) => {
return this.renderManagementList(item);
})}
</div>
{form}
</div>
</div>
);
}
render() {
switch (this.state.event) {
case "config":
return this.renderFormBox(
<div className="box">
{this.renderProblem()}
{this.state.message !== "" ? (
<div className="message" onClick={() => this.clearMessage()}>
<li className="fa fa-exclamation-triangle" />
&nbsp; {this.state.message}
</div>
) : null}
<div className="inner">
<table cellPadding="5" cellSpacing="0" border="0" width="100%">
<tbody>
<tr>
<td width={Setting.PcBrowser ? "120" : "90"} align="right">
{i18next.t("translator:Translation Platform")}
</td>
<td width="auto" align="left">
{this.renderSelectPlatform()}
</td>
</tr>
<tr>
<td width={Setting.PcBrowser ? "120" : "90"} align="right">
{i18next.t("translator:Key")}
</td>
<td width="auto" align="left">
<input
onChange={(event) => {
let targetVal = event.target.value;
this.setState((prevState) => {
prevState.form.key = targetVal;
return prevState;
});
}}
value={this.state.form?.key}
style={{ width: 300 }}
/>
</td>
</tr>
<tr>
<td width={Setting.PcBrowser ? "120" : "90"} align="right">
{i18next.t("translator:Visible")}
</td>
<td width="auto" align="left">
<input
type="radio"
onClick={() => {
this.setState((prevState) => {
prevState.form.visible = true;
return prevState;
});
}}
checked={this.state.form?.visible}
name="visible"
/>
{i18next.t("tab:show")}{" "}
<input
type="radio"
onClick={() => {
this.setState((prevState) => {
prevState.form.visible = false;
return prevState;
});
}}
checked={!this.state.form?.visible}
name="visible"
/>
{i18next.t("tab:hidden")}
</td>
</tr>
<tr>
<td
width={Setting.PcBrowser ? "120" : "90"}
align="right"
></td>
<td width="auto" align="left">
<input
type="submit"
className="super normal button"
value={i18next.t("translator:Save")}
onClick={() => this.updateTranslator()}
/>
</td>
</tr>
</tbody>
</table>
</div>
</div>
);
break;
case "create":
return this.renderFormBox(
<div className="box">
{this.renderProblem()}
{this.state.message !== "" ? (
<div className="message" onClick={() => this.clearMessage()}>
<li className="fa fa-exclamation-triangle" />
&nbsp; {this.state.message}
</div>
) : null}
<div className="inner">
<table cellPadding="5" cellSpacing="0" border="0" width="100%">
<tbody>
<tr>
<td width={Setting.PcBrowser ? "120" : "90"} align="right">
{i18next.t("translator:Translation Platform ID")}
</td>
<td width="auto" align="left">
<input
onChange={(event) => {
let targetVal = event.target.value;
this.setState((prevState) => {
prevState.form.id = targetVal;
return prevState;
});
}}
value={this.state.form.id}
style={{ width: 300 }}
/>
</td>
</tr>
<tr>
<td width={Setting.PcBrowser ? "120" : "90"} align="right">
{i18next.t("translator:Translation Platform Name")}
</td>
<td width="auto" align="left">
<input
onChange={(event) => {
let targetVal = event.target.value;
this.setState((prevState) => {
prevState.form.name = targetVal;
return prevState;
});
}}
value={this.state.form.name}
style={{ width: 300 }}
/>
</td>
</tr>
<tr>
<td width={Setting.PcBrowser ? "120" : "90"} align="right">
{i18next.t("translator:Translation API")}
</td>
<td width="auto" align="left">
{this.renderSelectAPI()}
</td>
</tr>
<tr>
<td width={Setting.PcBrowser ? "120" : "90"} align="right">
{i18next.t("translator:Key")}
</td>
<td width="auto" align="left">
<input
onChange={(event) => {
let targetVal = event.target.value;
this.setState((prevState) => {
prevState.form.key = targetVal;
return prevState;
});
}}
value={this.state.form.key}
style={{ width: 300 }}
/>
</td>
</tr>
<tr>
<td width={Setting.PcBrowser ? "120" : "90"} align="right">
{i18next.t("translator:Visible")}
</td>
<td width="auto" align="left">
<input
type="radio"
onClick={() => {
this.setState((prevState) => {
prevState.form.visible = true;
return prevState;
});
}}
checked={this.state.form.visible}
name="visible"
/>
{i18next.t("tab:show")}{" "}
<input
type="radio"
onClick={() => {
this.setState((prevState) => {
prevState.form.visible = false;
return prevState;
});
}}
checked={!this.state.form.visible}
name="visible"
/>
{i18next.t("tab:hidden")}
</td>
</tr>
<tr>
<td
width={Setting.PcBrowser ? "120" : "90"}
align="right"
></td>
<td width="auto" align="left">
<input
type="submit"
className="super normal button"
value={i18next.t("translator:Save")}
onClick={() => this.addTranslator()}
/>
</td>
</tr>
</tbody>
</table>
</div>
</div>
);
break;
case "manage":
return this.renderFormBox(
<div className="box">
{this.renderProblem()}
{this.state.message !== "" ? (
<div className="message" onClick={() => this.clearMessage()}>
<li className="fa fa-exclamation-triangle" />
&nbsp; {this.state.message}
</div>
) : null}
<div id="all-translators">
{this.state.translators !== null &&
this.state.translators.length !== 0 ? (
this.state.translators.map((translator) =>
this.renderTranslators(translator)
)
) : (
<div
className="cell"
style={{
textAlign: "center",
height: "100px",
lineHeight: "100px",
}}
>
{this.state.translators === null
? i18next.t("loading:Data is loading...")
: i18next.t("translator:No translator yet")}
</div>
)}
</div>
</div>
);
break;
}
}
}
export default withRouter(AdminTranslation);

View File

@ -0,0 +1,52 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import * as Setting from "../Setting";
export function updateTranslator(translator) {
return fetch(`${Setting.ServerUrl}/api/update-translator`, {
method: "POST",
credentials: "include",
body: JSON.stringify(translator),
}).then((res) => res.json());
}
export function getTranslator() {
return fetch(`${Setting.ServerUrl}/api/get-translator`, {
method: "GET",
credentials: "include",
}).then((res) => res.json());
}
export function addTranslator(translator) {
return fetch(`${Setting.ServerUrl}/api/add-translator`, {
method: "POST",
credentials: "include",
body: JSON.stringify(translator),
}).then((res) => res.json());
}
export function delTranslator(id) {
return fetch(`${Setting.ServerUrl}/api/del-translator?id=${id}`, {
method: "POST",
credentials: "include",
}).then((res) => res.json());
}
export function visibleTranslator() {
return fetch(`${Setting.ServerUrl}/api/visible-translator`, {
method: "GET",
credentials: "include",
}).then((res) => res.json());
}

View File

@ -159,8 +159,6 @@
"Favor": "Favor",
"hits": "hits",
"Ignore": "Ignore",
"Translate": "Translate",
"Translated from {lang} by": "Translated from {lang} by",
"Thank": "Thank",
"Thanked": "Thanked",
"Total Topics": "Total Topics",
@ -444,6 +442,34 @@
"Save": "Save",
"Update poster information success": "Update poster information success"
},
"translator": {
"Translator management": "Translator management",
"Translation Platform ID": "Platform ID",
"Translation Platform Name": "Platform Name",
"Translation Platform": "Translation Platform",
"Translation API": "Translation API",
"Key": "Key",
"Visible": "Visible",
"Save": "Save",
"Update translator information success": "Update translator information success",
"Add translator information success": "Add translator information success",
"Delete translator success": "Delete translator success",
"Please select a translator platform": "Please select a translator platform",
"Please input id": "Please input id",
"Please input name": "Please input name",
"Please input key": "Please input key",
"Please select a translator api": "Please select a translator api",
"No translator yet": "No translator yet",
"Config Translator": "Config Translator",
"Are you sure to delete translator": "Are you sure to delete translator",
"Please disable it first": "Please disable it first",
"Delete": "Delete",
"Enable": "Enable",
"Disable": "Disable",
"Create": "Create",
"Success": "Success",
"Manage": "Manage"
},
"member": {
"Watch": "Watch",
"member": "member",
@ -669,6 +695,7 @@
"Poster management": "Poster management",
"Member management": "Member management",
"Sensitive management": "Sensitive management",
"Translation management": "Translation management",
"Tab management": "Tab management",
"Plane management": "Plane management",
"Get search result success": "Get search result success",
@ -763,7 +790,10 @@
"Please sign out before sign up": "Please sign out before sign up",
"Please sign out before sign in": "Please sign out before sign in",
"Adding image failed": "Adding image failed",
"Add file failed": "Add file failed"
"Add file failed": "Add file failed",
"Translator ID existed": "Translator ID existed",
"Add translator failed": "Add translator failed",
"Delete translator failed": "Delete translator failed"
},
"search": {
"loading...": "loading...",

View File

@ -159,8 +159,6 @@
"Favor": "加入收藏",
"hits": "次点击",
"Ignore": "忽略主题",
"Translate": "Translate",
"Translated from {lang} by": "翻译自 {lang} 通过",
"Thank": "感谢",
"Thanked": "已感谢",
"Total Topics": "主题总数",
@ -514,6 +512,34 @@
"Save": "保存",
"Update poster information success": "更新广告信息成功"
},
"translator": {
"Translator management": "翻译管理",
"Translation Platform ID": "平台 ID",
"Translation Platform Name": "平台名称",
"Translation Platform": "翻译平台",
"Translation API": "翻译 API",
"Key": "Key",
"Visible": "可见性",
"Save": "保存",
"Update translator information success": "更新翻译信息成功",
"Add translator information success": "添加翻译信息成功",
"Delete translator success": "删除翻译平台成功",
"Please select a translator platform": "请选择翻译平台",
"Please input id": "请输入 ID",
"Please input name": "请输入平台名称",
"Please input key": "请输入 key",
"Please select a translator api": "请选择翻译 API",
"No translator yet": "暂无平台",
"Config Translator": "配置翻译",
"Are you sure to delete translator": "确定删除翻译平台",
"Please disable it first": "请先禁用该平台",
"Delete": "删除",
"Enable": "启用",
"Disable": "禁用",
"Create": "新建",
"Success": "成功",
"Manage": "管理"
},
"sensitive": {
"sensitive management": "敏感词管理",
"new sensitive": "增加敏感词"
@ -678,6 +704,7 @@
"Tab management": "类别管理",
"Plane management": "位面管理",
"Sensitive management": "敏感词管理",
"Translation management": "翻译管理",
"Get search result success": "获取搜索结果成功",
"Sorter": "排序",
"Show": "显示",
@ -763,7 +790,10 @@
"Please sign out before sign up": "注册前请先登出",
"Please sign out before sign in": "登录前请先登出",
"Adding image failed": "添加图片文件失败",
"Add file failed": "添加文件失败"
"Add file failed": "添加文件失败",
"Translator ID existed": "翻译平台 ID 已存在",
"Add translator failed": "添加翻译平台失败",
"Delete translator failed": "删除翻译平台失败"
},
"search": {
"loading...": "加载中...",

View File

@ -21,6 +21,7 @@ import Avatar from "../Avatar";
import ReplyBox from "./ReplyBox";
import * as FavoritesBackend from "../backend/FavoritesBackend";
import * as BalanceBackend from "../backend/BalanceBackend";
import * as TranslatorBackend from "../backend/TranslatorBackend";
import "../node.css";
import Zmage from "react-zmage";
import { Link } from "react-router-dom";
@ -44,6 +45,7 @@ class TopicBox extends React.Component {
favoritesStatus: false,
defaultTopTopicTime: 10,
from: "/",
showTranslateBtn: false,
translation: {
translated: false,
from: "",
@ -64,6 +66,7 @@ class TopicBox extends React.Component {
this.getTopic();
this.getFavoriteStatus();
TopicBackend.addTopicBrowseCount(this.state.topicId);
this.renderTranslateButton();
}
componentWillUnmount() {
@ -409,6 +412,18 @@ class TopicBox extends React.Component {
);
}
renderTranslateButton() {
TranslatorBackend.visibleTranslator().then((res) => {
let translateBtn = false;
if (res?.data) {
translateBtn = true;
}
this.setState({
showTranslateBtn: translateBtn,
});
});
}
renderDesktopButtons() {
return (
<div className="topic_buttons">
@ -884,23 +899,25 @@ class TopicBox extends React.Component {
)}
escapeHtml={false}
/>
<a href="#;" onClick={() => this.translateTopic()}>
<p style={{ margin: 15 }}>
{this.state.translation.translated ? (
<span>
{i18next
.t("topic:Translated from {lang} by")
.replace("{lang}", this.state.translation.from)}
<img
height={18}
src="https://cdn.casbin.org/img/logo_google.svg"
></img>
</span>
) : (
<span>{i18next.t("topic:Translate")}</span>
)}
</p>
</a>
{this.state.showTranslateBtn ? (
<a href="#;" onClick={() => this.translateTopic()}>
<p style={{ margin: 15 }}>
{this.state.translation.translated ? (
<span>
{`Translate from ${this.state.translation.from} by `}
<img
height={18}
src="https://cdn.casbin.org/img/logo_google.svg"
></img>
</span>
) : (
<span>Translate</span>
)}
</p>
</a>
) : (
""
)}
{this.state.translation.translated ? (
<ReactMarkdown
renderers={{