Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Enable Multi-Language Support for Application Installation Forms #7717

Merged
merged 1 commit into from
Jan 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/app/api/v1/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ func (b *BaseApi) GetApp(c *gin.Context) {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
appDTO, err := appService.GetApp(appKey)
appDTO, err := appService.GetApp(c, appKey)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
Expand Down
12 changes: 5 additions & 7 deletions backend/app/dto/app.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
package dto

import (
"github.com/1Panel-dev/1Panel/backend/app/model"
)

type AppDatabase struct {
ServiceName string `json:"PANEL_DB_HOST"`
DbName string `json:"PANEL_DB_NAME"`
Expand Down Expand Up @@ -62,7 +58,7 @@ type AppDefine struct {
}

type LocalAppAppDefine struct {
AppProperty model.App `json:"additionalProperties" yaml:"additionalProperties"`
AppProperty AppProperty `json:"additionalProperties" yaml:"additionalProperties"`
}

type LocalAppParam struct {
Expand All @@ -84,6 +80,7 @@ type AppProperty struct {
Tags []string `json:"tags"`
ShortDescZh string `json:"shortDescZh"`
ShortDescEn string `json:"shortDescEn"`
Description Locale `json:"description"`
Key string `json:"key"`
Required []string `json:"Required"`
CrossVersionUpdate bool `json:"crossVersionUpdate"`
Expand Down Expand Up @@ -114,9 +111,9 @@ type Locale struct {
En string `json:"en"`
Ja string `json:"ja"`
Ms string `json:"ms"`
PtBr string `json:"pt-br"`
PtBr string `json:"pt-br" yaml:"pt-br"`
Ru string `json:"ru"`
ZhHant string `json:"zh-hant"`
ZhHant string `json:"zh-hant" yaml:"zh-hant"`
Zh string `json:"zh"`
}

Expand All @@ -129,6 +126,7 @@ type AppFormFields struct {
Type string `json:"type"`
LabelZh string `json:"labelZh"`
LabelEn string `json:"labelEn"`
Label Locale `json:"label"`
Required bool `json:"required"`
Default interface{} `json:"default"`
EnvKey string `json:"envKey"`
Expand Down
3 changes: 1 addition & 2 deletions backend/app/dto/response/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,7 @@ type AppItem struct {
Name string `json:"name"`
Key string `json:"key"`
ID uint `json:"id"`
ShortDescZh string `json:"shortDescZh"`
ShortDescEn string `json:"shortDescEn"`
Description string `json:"description"`
Icon string `json:"icon"`
Type string `json:"type"`
Status string `json:"status"`
Expand Down
18 changes: 17 additions & 1 deletion backend/app/model/app.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package model

import (
"encoding/json"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/utils/common"
"github.com/gin-gonic/gin"
"path/filepath"
"strings"
)
Expand All @@ -12,6 +15,7 @@ type App struct {
Key string `json:"key" gorm:"type:varchar(64);not null;"`
ShortDescZh string `json:"shortDescZh" yaml:"shortDescZh" gorm:"type:longtext;"`
ShortDescEn string `json:"shortDescEn" yaml:"shortDescEn" gorm:"type:longtext;"`
Description string `json:"description"`
Icon string `json:"icon" gorm:"type:longtext;"`
Type string `json:"type" gorm:"type:varchar(64);not null"`
Status string `json:"status" gorm:"type:varchar(64);not null"`
Expand All @@ -36,8 +40,20 @@ func (i *App) IsLocalApp() bool {
}
func (i *App) GetAppResourcePath() string {
if i.IsLocalApp() {
//这里要去掉本地应用的local前缀
return filepath.Join(constant.LocalAppResourceDir, strings.TrimPrefix(i.Key, "local"))
}
return filepath.Join(constant.RemoteAppResourceDir, i.Key)
}

func (i *App) GetDescription(ctx *gin.Context) string {
var translations = make(map[string]string)
_ = json.Unmarshal([]byte(i.Description), &translations)
lang := strings.ToLower(common.GetLang(ctx))
if desc, ok := translations[lang]; ok {
return desc
}
if lang == "zh" {
return i.ShortDescZh
}
return i.ShortDescEn
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The differences between the provided code patches are primarily related to adding functionality for handling and translating app descriptions using JSON and Go's built-in JSON library.

Here are some key points:

  1. JSON Unmarshalling: The patch includes json.Unmarshal calls within GetDescription, which allows mapping of strings like "en" or "zh" from a translation map back into their respective language fields (ShortDescEn or ShortDescZh). This is useful for internationalizing applications where descriptions can vary by locale.

  2. Language Selection: After unmarhsalling the descriptions, it selects the appropriate description based on the user's current language setting (common.GetLang(ctx)).

  3. Default Behavior: If no match exists in the translation map, the function defaults to returning the translated short description if possible, falling back to English or Chinese as last resort.

  4. Simplicity with Default Values: While this approach enhances localization capabilities, keep in mind that defaulting to English or Chinese might not be ideal unless they are supported at all languages. Adjustments may needed depending on specific use cases and cultural considerations.

Optimization Suggestions:

  • Performance Considerations: Ensure that accessing and parsing translations efficiently, especially when dealing with large datasets, would also benefit performance.

  • Error Handling: Currently, error handling around Unmarshal is absent, but it should be added to account for malformed JSON data gracefully.

Overall, the changes align with modern software development practices aimed at improving internationalization and localization features within an application.

24 changes: 12 additions & 12 deletions backend/app/service/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ type AppService struct {
type IAppService interface {
PageApp(ctx *gin.Context, req request.AppSearch) (interface{}, error)
GetAppTags(ctx *gin.Context) ([]response.TagDTO, error)
GetApp(key string) (*response.AppDTO, error)
GetApp(ctx *gin.Context, key string) (*response.AppDTO, error)
GetAppDetail(appId uint, version, appType string) (response.AppDetailDTO, error)
Install(ctx context.Context, req request.AppInstallCreate) (*model.AppInstall, error)
SyncAppListFromRemote() error
Expand Down Expand Up @@ -94,16 +94,15 @@ func (a AppService) PageApp(ctx *gin.Context, req request.AppSearch) (interface{
lang := strings.ToLower(common.GetLang(ctx))
for _, ap := range apps {
appDTO := &response.AppItem{
ID: ap.ID,
Name: ap.Name,
Key: ap.Key,
Type: ap.Type,
Icon: ap.Icon,
ShortDescZh: ap.ShortDescZh,
ShortDescEn: ap.ShortDescEn,
Resource: ap.Resource,
Limit: ap.Limit,
}
ID: ap.ID,
Name: ap.Name,
Key: ap.Key,
Type: ap.Type,
Icon: ap.Icon,
Resource: ap.Resource,
Limit: ap.Limit,
}
appDTO.Description = ap.GetDescription(ctx)
appDTOs = append(appDTOs, appDTO)
appTags, err := appTagRepo.GetByAppId(ap.ID)
if err != nil {
Expand Down Expand Up @@ -166,7 +165,7 @@ func (a AppService) GetAppTags(ctx *gin.Context) ([]response.TagDTO, error) {
return res, nil
}

func (a AppService) GetApp(key string) (*response.AppDTO, error) {
func (a AppService) GetApp(ctx *gin.Context, key string) (*response.AppDTO, error) {
var appDTO response.AppDTO
if key == "postgres" {
key = "postgresql"
Expand All @@ -176,6 +175,7 @@ func (a AppService) GetApp(key string) (*response.AppDTO, error) {
return nil, err
}
appDTO.App = app
appDTO.App.Description = app.GetDescription(ctx)
details, err := appDetailRepo.GetBy(appDetailRepo.WithAppId(app.ID))
if err != nil {
return nil, err
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No significant issues identified in the provided code changes. Here are a few minor adjustments for clarity:

  1. Corrected indentation of the if statement that checks if key == "postgres".

These improvements ensure proper readability and maintainability of the code.

Expand Down
30 changes: 25 additions & 5 deletions backend/app/service/app_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -1051,6 +1051,8 @@ func getApps(oldApps []model.App, items []dto.AppDefine) map[string]model.App {
app.Key = key
app.ShortDescZh = config.ShortDescZh
app.ShortDescEn = config.ShortDescEn
description, _ := json.Marshal(config.Description)
app.Description = string(description)
app.Website = config.Website
app.Document = config.Document
app.Github = config.Github
Expand Down Expand Up @@ -1150,14 +1152,32 @@ func handleLocalApp(appDir string) (app *model.App, err error) {
err = buserr.WithMap(constant.ErrFileParseApp, map[string]interface{}{"name": "data.yml", "err": err.Error()}, err)
return
}
app = &localAppDefine.AppProperty
appDefine := localAppDefine.AppProperty
app = &model.App{}
app.Name = appDefine.Name
app.TagsKey = append(appDefine.Tags, "Local")
app.Type = appDefine.Type
app.CrossVersionUpdate = appDefine.CrossVersionUpdate
app.Limit = appDefine.Limit
app.Recommend = appDefine.Recommend
app.Website = appDefine.Website
app.Github = appDefine.Github
app.Document = appDefine.Document

if appDefine.ShortDescZh != "" {
appDefine.Description.Zh = appDefine.ShortDescZh
}
if appDefine.ShortDescEn != "" {
appDefine.Description.En = appDefine.ShortDescEn
}
desc, _ := json.Marshal(appDefine.Description)
app.Description = string(desc)

app.Key = "local" + appDefine.Key
app.Resource = constant.AppResourceLocal
app.Status = constant.AppNormal
app.Recommend = 9999
app.TagsKey = append(app.TagsKey, "Local")
app.Key = "local" + app.Key
readMePath := path.Join(appDir, "README.md")
readMeByte, err := fileOp.GetContent(readMePath)
readMeByte, err := fileOp.GetContent(path.Join(appDir, "README.md"))
if err == nil {
app.ReadMe = string(readMeByte)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code has two main issues:

  1. Handling JSON Marshal Errors: The json.Marshal function call to convert the description into bytes may fail if there are issues with converting a nested interface. It's best practice to handle this explicitly by checking the returned error.

  2. Initialization of App Structure: There were missing initializations when copying from localAppDefine.AppProperty to new instance of model.App. This can lead to uninitialized fields which might cause undefined behavior.

Here is an updated version that addresses these issues:

@@ -1051,6 +1051,8 @@ func getApps(oldApps []model.App, items []dto.AppDefine) map[string]model.App {
         return oldApps
     }
 
+    // Assuming constant.json defines Description struct
+    type Description struct { Zh string; En string }
+
     for _, item := range items {
         key := item.GetKey()
-        var app model.App
 
+        // Create a new model.App instance and populate its fields
         app := model.App{
             Key:          key,
             ShortDescZh:   config.ShortDescZh,
@@ -1159,20 +1159,36 @@ func handleLocalApp(appDir string) (app *model.App, err error) {
             readMeByte, err = fileOp.GetContent(path.Join(appDir, "README.md"))
         }
         if err == nil {
-            app.ReadMe = string(readMeByte)
+            // Decode the Markdown README into raw HTML for further processing.
+            // You should implement a markdown-to-html conversion utility like go-md2html here.
+            //
+            // For now, assuming it already returns raw HTML content:
+            app.ReadMe = string(markdownToHtml(readMeByte))
         }

         apps[key] = app
     }

+    return apps, nil

Replace the line handling the README reading with appropriate parsing logic based on your project’s requirements. Alternatively, you could use an external library like go-md2html to parse the Markdown directly into raw HTML format.

Expand Down
1 change: 1 addition & 0 deletions backend/init/migration/migrate.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ func Init() {
migrations.AddApiKeyValidityTime,

migrations.UpdateAppTag,
migrations.UpdateApp,
})
if err := m.Migrate(); err != nil {
global.LOG.Error(err)
Expand Down
12 changes: 11 additions & 1 deletion backend/init/migration/migrations/v_1_10.go
Original file line number Diff line number Diff line change
Expand Up @@ -362,11 +362,21 @@ var AddApiKeyValidityTime = &gormigrate.Migration{
}

var UpdateAppTag = &gormigrate.Migration{
ID: "20241226-update-app-tag",
ID: "20250114-update-app-tag",
Migrate: func(tx *gorm.DB) error {
if err := tx.AutoMigrate(&model.Tag{}); err != nil {
return err
}
return nil
},
}

var UpdateApp = &gormigrate.Migration{
ID: "20250114-update-app",
Migrate: func(tx *gorm.DB) error {
if err := tx.AutoMigrate(&model.App{}); err != nil {
return err
}
return nil
},
}
12 changes: 12 additions & 0 deletions frontend/src/api/interface/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export namespace App {
tags: Tag[];
shortDescZh: string;
shortDescEn: string;
description: string;
author: string;
source: string;
type: string;
Expand Down Expand Up @@ -58,10 +59,21 @@ export namespace App {
formFields: FromField[];
}

interface Locale {
zh: string;
en: string;
'zh-Hant': string;
ja: string;
ms: string;
'pt-br': string;
ru: string;
}

export interface FromField {
type: string;
labelZh: string;
labelEn: string;
label: Locale;
required: boolean;
default: any;
envKey: string;
Expand Down
11 changes: 1 addition & 10 deletions frontend/src/views/app-store/apps/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,7 @@
</div>
<div class="app-desc">
<span class="desc">
{{
language == 'zh' || language == 'tw'
? app.shortDescZh
: app.shortDescEn
}}
{{ app.description }}
</span>
</div>
<div class="app-tag">
Expand Down Expand Up @@ -177,16 +173,11 @@ import Detail from '../detail/index.vue';
import Install from '../detail/install/index.vue';
import router from '@/routers';
import { GlobalStore } from '@/store';
import { getLanguage } from '@/utils/util';

const globalStore = GlobalStore();

const mobile = computed(() => {
return globalStore.isMobile();
});

const language = getLanguage();

const paginationConfig = reactive({
cacheSizeKey: 'app-page-size',
currentPage: 1,
Expand Down
5 changes: 1 addition & 4 deletions frontend/src/views/app-store/detail/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
</div>
<div class="description mb-4">
<span>
{{ language == 'zh' || language == 'tw' ? app.shortDescZh : app.shortDescEn }}
{{ app.description }}
</span>
</div>
<br />
Expand Down Expand Up @@ -84,13 +84,10 @@ import { ref } from 'vue';
import Install from './install/index.vue';
import router from '@/routers';
import { GlobalStore } from '@/store';
import { getLanguage } from '@/utils/util';
import { storeToRefs } from 'pinia';
const globalStore = GlobalStore();
const { isDarkTheme } = storeToRefs(globalStore);

const language = getLanguage();

const app = ref<any>({});
const appDetail = ref<any>({});
const version = ref('');
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/views/app-store/detail/params/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,10 @@ const changeService = (value: string, services: App.AppService[]) => {

const getLabel = (row: ParamObj): string => {
const language = localStorage.getItem('lang') || 'zh';
let lang = language == 'tw' ? 'zh-Hant' : language;
if (row.label && row.label[lang] != '') {
return row.label[lang];
}
if (language == 'zh' || language == 'tw') {
return row.labelZh;
} else {
Expand Down
4 changes: 1 addition & 3 deletions frontend/src/views/home/app/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<span class="h-app-title">{{ app.name }}</span>
<div class="h-app-desc">
<span>
{{ language == 'zh' || language == 'tw' ? app.shortDescZh : app.shortDescEn }}
{{ app.description }}
</span>
</div>
</div>
Expand All @@ -37,11 +37,9 @@
<script lang="ts" setup>
import { App } from '@/api/interface/app';
import { SearchApp } from '@/api/modules/app';
import { getLanguage } from '@/utils/util';
import { reactive, ref } from 'vue';
import { useRouter } from 'vue-router';
const router = useRouter();
const language = getLanguage();

let req = reactive({
name: '',
Expand Down
Loading