Skip to content

Commit

Permalink
feat: add language switch button (#31)
Browse files Browse the repository at this point in the history
* feat: add language switch button

* feat: add i18n for frontend

---------

Co-authored-by: HeartLinked <lifeiyang@zju.edu.cn>
  • Loading branch information
feiyang li and HeartLinked authored May 29, 2024
1 parent 1dca747 commit b8ec808
Show file tree
Hide file tree
Showing 17 changed files with 1,167 additions and 5 deletions.
97 changes: 97 additions & 0 deletions i18n/generate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Copyright 2021 The Casdoor 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.

package i18n

import (
"fmt"
"os"
"path/filepath"
"regexp"
"strings"

"github.com/casbin/caswaf/util"
)

type I18nData map[string]map[string]string

var reI18n *regexp.Regexp

func init() {
reI18n, _ = regexp.Compile("i18next.t\\(\"(.*?)\"\\)")
}

func getAllI18nStrings(fileContent string) []string {
res := []string{}

matches := reI18n.FindAllStringSubmatch(fileContent, -1)
if matches == nil {
return res
}

for _, match := range matches {
res = append(res, match[1])
}
return res
}

func getAllJsFilePaths() []string {
path := "../web/src"

res := []string{}
err := filepath.Walk(path,
func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}

if !strings.HasSuffix(info.Name(), ".js") {
return nil
}

res = append(res, path)
fmt.Println(path, info.Name())
return nil
})
if err != nil {
panic(err)
}

return res
}

func parseToData() *I18nData {
allWords := []string{}
paths := getAllJsFilePaths()
for _, path := range paths {
fileContent := util.ReadStringFromPath(path)
words := getAllI18nStrings(fileContent)
allWords = append(allWords, words...)
}
fmt.Printf("%v\n", allWords)

data := I18nData{}
for _, word := range allWords {
tokens := strings.Split(word, ":")
namespace := tokens[0]
key := tokens[1]

if _, ok := data[namespace]; !ok {
data[namespace] = map[string]string{}
}
data[namespace][key] = key
}

return &data
}
42 changes: 42 additions & 0 deletions i18n/generate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright 2021 The Casdoor 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.

//go:build !skipCi
// +build !skipCi

package i18n

import "testing"

func applyToOtherLanguage(dataEn *I18nData, lang string) {
dataOther := readI18nFile(lang)
println(dataOther)

applyData(dataEn, dataOther)
writeI18nFile(lang, dataEn)
}

func TestGenerateI18nStrings(t *testing.T) {
dataEn := parseToData()
writeI18nFile("en", dataEn)

applyToOtherLanguage(dataEn, "zh")
applyToOtherLanguage(dataEn, "fr")
applyToOtherLanguage(dataEn, "de")
applyToOtherLanguage(dataEn, "id")
applyToOtherLanguage(dataEn, "ja")
applyToOtherLanguage(dataEn, "ko")
applyToOtherLanguage(dataEn, "ru")
applyToOtherLanguage(dataEn, "es")
}
63 changes: 63 additions & 0 deletions i18n/util.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright 2021 The Casdoor 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.

package i18n

import (
"fmt"
"strings"

"github.com/casbin/caswaf/util"
)

func getI18nFilePath(language string) string {
return fmt.Sprintf("../web/src/locales/%s/data.json", language)
}

func readI18nFile(language string) *I18nData {
s := util.ReadStringFromPath(getI18nFilePath(language))

data := &I18nData{}
err := util.JsonToStruct(s, data)
if err != nil {
panic(err)
}
return data
}

func writeI18nFile(language string, data *I18nData) {
s := util.StructToJson(data)
s = strings.ReplaceAll(s, "\\\\\"", "\"")
println(s)

util.WriteStringToPath(s, getI18nFilePath(language))
}

func applyData(data1 *I18nData, data2 *I18nData) {
for namespace, pairs2 := range *data2 {
if _, ok := (*data1)[namespace]; !ok {
continue
}

pairs1 := (*data1)[namespace]

for key, value := range pairs2 {
if _, ok := pairs1[key]; !ok {
continue
}

pairs1[key] = value
}
}
}
11 changes: 9 additions & 2 deletions web/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import RecordListPage from "./RecordListPage";
import RecordEditPage from "./RecordEditPage";
import i18next from "i18next";
import DashboardPage from "./DashboardPage";
import LanguageSelect from "./LanguageSelect";
import {withTranslation} from "react-i18next";
// import SelectLanguageBox from "./SelectLanguageBox";

const {Header, Footer} = Layout;
Expand Down Expand Up @@ -212,7 +214,12 @@ class App extends Component {
</Menu.Item>
);
} else {
res.push(this.renderRightDropdown());
res.push(
<div style={{float: "right", display: "flex", alignItems: "center"}}>
<LanguageSelect style={{marginRight: "20px", marginTop: "10px"}} />
{this.renderRightDropdown()}
</div>
);
return (
<div style={{float: "right", margin: "0px", padding: "0px"}}>
{
Expand Down Expand Up @@ -372,4 +379,4 @@ class App extends Component {
}
}

export default withRouter(App);
export default withRouter(withTranslation()(App));
79 changes: 79 additions & 0 deletions web/src/LanguageSelect.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Copyright 2024 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 * as Setting from "./Setting";
import {Dropdown, Menu} from "antd";
import {GlobalOutlined} from "@ant-design/icons";

function flagIcon(country, alt) {
return <img src={`${Setting.StaticBaseUrl}/flag-icons/${country}.svg`} alt={alt} style={{marginRight: 8, width: 24}} />;
}

class LanguageSelect extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
languages: Setting.Countries.map(item => item.key),
};

// Preload flag icons
Setting.Countries.forEach((country) => {
new Image().src = `${Setting.StaticBaseUrl}/flag-icons/${country.country}.svg`;
});
}

items = Setting.Countries.map((country) => ({
key: country.key,
label: (
<span>
{flagIcon(country.country, country.alt)}
{country.label}
</span>
),
}));

getLanguages(languages) {
const select = [];
for (const language of languages) {
this.items.forEach((item) => item.key === language ? select.push(item) : null);
}
return select;
}

render() {
const languageItems = this.getLanguages(this.state.languages);

const menu = (
<Menu onClick={(e) => Setting.setLanguage(e.key)}>
{languageItems.map(item => (
<Menu.Item key={item.key}>
{item.label}
</Menu.Item>
))}
</Menu>
);

return (
<Dropdown overlay={menu}>
<div className="select-box" style={{display: languageItems.length === 0 ? "none" : null, ...this.props.style}}>
<GlobalOutlined style={{fontSize: "24px"}} />
</div>
</Dropdown>
);
}
}

export default LanguageSelect;
12 changes: 12 additions & 0 deletions web/src/Setting.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,18 @@ export const StaticBaseUrl = "https://cdn.casbin.org";

export let CasdoorSdk;

export const Countries = [
{label: "English", key: "en", country: "US", alt: "English"},
{label: "中文", key: "zh", country: "CN", alt: "中文"},
{label: "Español", key: "es", country: "ES", alt: "Español"},
{label: "Français", key: "fr", country: "FR", alt: "Français"},
{label: "Deutsch", key: "de", country: "DE", alt: "Deutsch"},
{label: "Indonesia", key: "id", country: "ID", alt: "Indonesia"},
{label: "日本語", key: "ja", country: "JP", alt: "日本語"},
{label: "한국어", key: "ko", country: "KR", alt: "한국어"},
{label: "Русский", key: "ru", country: "RU", alt: "Русский"},
];

export function initServerUrl() {
const hostname = window.location.hostname;
if (hostname === "localhost") {
Expand Down
2 changes: 1 addition & 1 deletion web/src/SiteListPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ class SiteListPage extends BaseListPage {
},
},
{
title: i18next.t("site: Enable WAF"),
title: i18next.t("site:Enable WAF"),
dataIndex: "enableWaf",
key: "enableWaf",
width: "120px",
Expand Down
36 changes: 36 additions & 0 deletions web/src/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,28 @@
import i18n from "i18next";
import zh from "./locales/zh/data.json";
import en from "./locales/en/data.json";
import de from "./locales/de/data.json";
import es from "./locales/es/data.json";
import fr from "./locales/fr/data.json";
import id from "./locales/id/data.json";
import ja from "./locales/ja/data.json";
import ko from "./locales/ko/data.json";
import ru from "./locales/ru/data.json";

import * as Conf from "./Conf";
import * as Setting from "./Setting";
import {initReactI18next} from "react-i18next";

const resources = {
en: en,
zh: zh,
de: de,
es: es,
fr: fr,
id: id,
ja: ja,
ko: ko,
ru: ru,
};

function initLanguage() {
Expand All @@ -44,6 +59,27 @@ function initLanguage() {
case "en-US":
language = "en";
break;
case "de":
language = "de";
break;
case "es":
language = "es";
break;
case "fr":
language = "fr";
break;
case "id":
language = "id";
break;
case "ja":
language = "ja";
break;
case "ko":
language = "ko";
break;
case "ru":
language = "ru";
break;
default:
language = Conf.DefaultLanguage;
}
Expand Down
Loading

0 comments on commit b8ec808

Please sign in to comment.