多语言网站的设计的探索——安企CMS多语言功能的实现记录

对于一个内容管理系统(CMS)来说,网站需要为不同语言的用户提供本地化的体验,支持多语言已经成为必备功能之一。本文将通过安企CMS的多语言实现为例,深入探讨多语言网站的设计与实现策略。

需求背景

安企CMS自推出以来,已经逐步扩展到多站点功能,用户群体也在不断扩大。在这个过程中,用户们对于多语言功能的呼声越来越高。然而,当前版本的安企CMS虽然支持每个站点设置不同语言,但由于无法实现内容之间的语言切换,这种多语言功能显得不够完善。尤其是在国际化环境中,内容的多语言切换和自动翻译的需求变得尤为迫切。因此,作为开发者,我们决定改进多语言功能,以满足用户需求,并让安企CMS更好地服务于全球市场。

多语言网站的功能调研

在深入研究多语言网站的实现后,我们总结出几种常见的处理方式。每种方式都有其独特的优势和适用场景,以下是几种主要的多语言处理方法:

  1. URL路径分段
    通过在URL中添加语言代码路径来区分语言。例如:

    • 英文版:example.com/en
    • 中文版:example.com/zh

    这种方式直观且有助于SEO优化,因为不同语言的页面通过不同的URL被区分。它还方便用户直接通过URL分享特定语言的页面。

  2. 子域名分离
    为不同语言版本创建独立的子域名。例如:

    • 英文版:en.example.com
    • 中文版:zh.example.com

    使用子域名的好处在于可以清晰区分不同的语言版本,也有利于SEO优化,但这需要在服务器和DNS配置上做更多工作。

  3. 查询参数 + Cookie 持久化
    通过查询参数(如 ?lang=en)来选择语言,同时结合 Cookie 实现用户的语言偏好持久化。例如:

    • 英文版:example.com?lang=en
    • 中文版:example.com?lang=zh

    这种方法可以根据用户的浏览器语言自动匹配,并允许用户手动切换语言。不过,URL中包含查询参数的方式在SEO上不如路径分段或子域名方式友好。

  4. 完全独立的多语言站点
    为每个语言版本创建完全独立的网站。例如:

    • 英文版:example.com
    • 中文版:example.cn

    这种方式适用于不同市场的独立运营,尤其适合需要针对不同地区和语言定制内容的场景。不过,维护多个独立站点的成本较高,每个站点都需要单独进行SEO优化和服务器配置。

目前,安企CMS的多语言功能实际上类似于第四种方式,即通过创建独立站点来实现不同语言的切换。这种方式在小规模使用中效果尚可,但随着内容增长和用户多样化,这种方式的局限性开始显现。

多语言站点处理方式的选择

为了让安企CMS更加灵活,满足不同用户的需求,不能局限于单一的多语言实现方式。我们决定在安企CMS中同时支持上述多种多语言处理方式,并让用户根据自己的实际需求自由选择。这不仅提升了用户体验,也为开发者提供了更广泛的定制化空间。

在设计过程中,我们特别关注以下几个关键点:

  • 灵活性:支持多种实现方式,让用户根据需求选择最适合的方案。
  • 易用性:通过后台简单的配置操作,即可实现多语言功能的开启与管理。
  • SEO 友好性:无论用户选择何种语言处理方式,系统都要确保其对搜索引擎友好,支持 hreflang 标签的正确使用。
  • 高效性:在处理多语言内容时,系统应尽可能减少冗余操作,并通过缓存等技术提升性能。

多语言站点的实现方案

基于安企CMS现有的多站点架构,我们的多语言功能在此基础上进行了进一步扩展。以下将详细介绍后台、前台以及模板的实现方案,并通过代码示例展示如何在安企CMS中实现多语言功能。

1. 后台多语言的实现

后台通过 ant-design 提供多语言站点的配置选项,让用户轻松配置不同站点的语言,并允许语言间的关联管理。我们可以在数据库中为每个站点记录其对应的语言信息。

数据库设计示例

CREATE TABLE websites (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100),
    language VARCHAR(10)  -- 站点语言,如 'en', 'zh', 'fr'
);

在后台接口中,使用 iris 框架处理语言配置的 API:

type Website struct {
    ID       int    `json:"id"`
    Name     string `json:"name"`
    Language string `json:"language"`  // 站点语言
}

// 更新站点语言的API
func UpdateSiteLanguage(ctx iris.Context) {
    var site Website
    if err := ctx.ReadJSON(&site); err != nil {
        ctx.StatusCode(iris.StatusBadRequest)
        ctx.JSON(iris.Map{"error": "Invalid input"})
        return
    }

    // 更新数据库
    _, err := db.Exec("UPDATE site SET language = ? WHERE id = ?", site.Language, site.ID)
    if err != nil {
        ctx.StatusCode(iris.StatusInternalServerError)
        ctx.JSON(iris.Map{"error": "Failed to update site language"})
        return
    }

    ctx.JSON(iris.Map{"success": true})
}

通过前端 ant-design 表单组件,管理员可以在后台选择或更新站点语言:

// 语言选择表单
import { Form, Select, Button } from 'antd';

const { Option } = Select;

const LanguageForm = () => {
  const onFinish = (values) => {
    // 调用API更新语言设置
    fetch('/api/site/update-language', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(values),
    });
  };

  return (
    <Form onFinish={onFinish}>
      <Form.Item name="language" label="选择语言">
        <Select>
          <Option value="en">English</Option>
          <Option value="zh">中文</Option>
          <Option value="fr">Français</Option>
        </Select>
      </Form.Item>
      <Button type="primary" htmlType="submit">保存</Button>
    </Form>
  );
};

2. 前台多语言的实现

在前台,通过 iris 框架实现用户语言的切换与记忆。在用户选择语言时,我们将语言存储在 Cookie 中,以便后续访问时加载相应的语言版本。

中间件:多语言选择

func LanguageMiddleware(ctx iris.Context) {
    // 从 Cookie 获取语言信息
    lang := ctx.GetCookie("lang")
    if lang == "" {
        // 默认语言设置为 'en'
        lang = "en"
    }

    // 将语言信息存入上下文,供后续处理使用
    ctx.Values().Set("lang", lang)
    ctx.Next()
}

用户切换语言时,可以通过 URL 或表单请求触发语言更新:

func SetLanguage(ctx iris.Context) {
    lang := ctx.URLParam("lang")
    if lang == "" {
        lang = "en"  // 默认语言
    }

    // 将语言写入 Cookie,有效期为 7 天
    ctx.SetCookie(&http.Cookie{
        Name:  "lang",
        Value: lang,
        Path:  "/",
        MaxAge: 60 * 60 * 24 * 7,
    })

    // 重定向到主页
    ctx.Redirect("/")
}

3. 模板多语言的实现

在前端页面中,安企CMS使用 pongo2 作为模板引擎。模板文件夹中带了 locales 文件夹用于存放不同的语言。

语言包文件示例(以 en.yml 为例):

 "welcome": "Welcome"
 "contact_us": "Contact Us"

解析模板语言

func(s *DjangoEngine) LoadTplLocales(site *Website) {
   // 检查模板是否有多语言
   var mapLocales = map[string]struct{}{}
   sfs := getFS(site.GetTemplateDir())
   rootDirName := getRootDirName(sfs)
   err = walk(sfs, "", func(path string, info os.FileInfo, err error) error {
      if err != nil {
         return nil
      }

      if info == nil || info.IsDir() {
         return nil
      }
      // 判断是否有多语言
      if strings.HasPrefix(path, "locales") {
         pathSplit := strings.Split(path, "/")
         if len(pathSplit) > 2 {
            mapLocales[pathSplit[1]] = struct{}{}
         }
      }
      return nil
   })
   if len(mapLocales) > 0 {
      var locales = make([]string, 0, len(mapLocales))
      for k := range mapLocales {
         locales = append(locales, k)
      }
      tplI18n := i18n.New()
      err = tplI18n.LoadFS(sfs, "./locales/*/*.yml", locales...)
      if err == nil {
         site.TplI18n = tplI18n
      }
   }
}

制作模板的多语言翻译标签:

package tags

import (
    "github.com/flosch/pongo2/v6"
    "kandaoni.com/anqicms/provider"
)

// tagTrNode 翻译
type tagTrNode struct {
    args []pongo2.IEvaluator
    key  string
}

func (node *tagTrNode) Execute(ctx *pongo2.ExecutionContext, writer pongo2.TemplateWriter) *pongo2.Error {
    var args []interface{}
    for _, value := range node.args {
        val, err := value.Evaluate(ctx)
        if err != nil {
            return err
        }
        args = append(args, val.Interface())
    }

    currentSite, _ := ctx.Public["website"].(*provider.Website)
    if currentSite == nil || currentSite.DB == nil {
        writer.WriteString(node.key)
        return nil
    }

    writer.WriteString(currentSite.TplTr(node.key, args...))

    return nil
}

func TagTrParser(doc *pongo2.Parser, start *pongo2.Token, arguments *pongo2.Parser) (pongo2.INodeTag, *pongo2.Error) {
    tagNode := &tagTrNode{
        args: []pongo2.IEvaluator{},
    }

    if arguments.Remaining() > 0 {
        arg := arguments.Current()
        arguments.Consume()
        tagNode.key = arg.Val
    }

    var args []pongo2.IEvaluator
    for arguments.Remaining() > 0 {
        valueExpr, err := arguments.ParseExpression()
        if err != nil {
            return nil, arguments.Error("Can not parse with args.", nil)
        }
        args = append(args, valueExpr)
    }

    tagNode.args = args

    return tagNode, nil
}

模板文件示例

<!DOCTYPE html>
<html>
<head>
    <title>{% tr 'welcome' %}</title>
</head>
<body>
    <h1>{% tr 'welcome' %}</h1>
    <a href="//www.greatytc.com/contact">{% tr 'contact_us' %}</a>
</body>
</html>

4. 文档自动同步与翻译

为了解决多语言内容维护繁琐的问题,安企CMS集成了文档自动翻译功能。当管理员更新某一语言版本的内容时,系统会自动将修改同步到其他语言版本,并使用第三方API进行自动翻译。

自动同步和翻译的功能可以大大减少管理员的手动工作量,提升管理效率。

总结

通过支持多种多语言实现方式,安企CMS为用户提供了灵活的多语言站点解决方案。无论是路径分段、子域名还是查询参数,用户都可以根据需求选择适合自己的实现方式。同时,文档自动同步与翻译功能的加入,让国际化内容的维护更加高效。未来,安企CMS将持续优化多语言功能,探索更多创新的多语言实现策略。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 210,914评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 89,935评论 2 383
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,531评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,309评论 1 282
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,381评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,730评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,882评论 3 404
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,643评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,095评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,448评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,566评论 1 339
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,253评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,829评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,715评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,945评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,248评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,440评论 2 348

推荐阅读更多精彩内容