【问题标题】:PHP web app internationalizationPHP Web 应用程序国际化
【发布时间】:2011-07-21 06:58:31
【问题描述】:

我正在构建一个需要国际化的 php web 应用程序。我决定使用 get-text 获取系统相关的字符串,也许使用一些数据库表来获取用户生成的内容。

例如,用户可能能够发布博客文章。他应该能够以不同的语言发布该帖子的不同版本。我可以通过将所有帖子存储在带有表示语言的额外列的帖子表中来实现这一点。

困难的一点是试图国际化存储在数据库中的系统字符串。

例如,我有一个存储权限的表。每个权限都应该有一个字符串来描述这个权限的作用。

目前,它存储在这样的表中:

app_privileges

  • 身份证
  • 特权
  • 其他一些列
  • 说明

我打算使用像 PoEdit 这样的应用程序来生成 gettext 文件。它能够搜索所有 php 文件以获取字符串。但是在字符串存储在数据库中的这种情况下,提取字符串以进行转换可能需要相当多的工作。有什么技巧和解决方案来处理这个问题?

最后,假设我有一些用户可以在应用程序中创建和定义的数据类型和表单。例如,为购物车定义“产品类型”。这意味着产品将拥有自己独特的一组属性和描述。这些属性需要连同描述一起翻译。

表单也是如此。用户可以创建一个可能存储在一组表中的表单。然后需要翻译这些表格。

我可以使用哪些数据库模型来存储表单和产品类型的翻译?

干杯:)

【问题讨论】:

  • 为什么投反对票?感觉是一个非常好的问题?

标签: php database-design internationalization gettext


【解决方案1】:

对于更“系统”导向的字符串,例如:

  • 按钮
  • 功能名称
  • 特权

Gettext 不错。

例如,权限更面向“系统”(用户不创建权限,而是管理员将权限授予用户但不创建新类型的权限)。因此,您的权限表可以有一个“privilege_name”列,它永远不会显示并且只包含 gettext 键,例如:“Privilege : User can edit posts in the specified forum”。

应用程序中的字符串也不应该是用户将看到的文本,而是更详细的内容,例如“菜单:编辑首选项”。

这些字符串通过 gettext(即使是英语或网站的“母语”)并被翻译成正确的用户可见字符串。

您还应该使用带编号的 sprintf 样式参数,即不是“%s 的价格是 %s”,而是“%(1)s 的价格是 %(2)s”。

这有几个优点:

  • gettext 提供的上下文非常少,并且会合并相同的字符串。看到“按钮:编辑此帖子”的翻译人员将比看到“编辑”的翻译人员获得更多帮助。在某些语言中,一个简单的文本“编辑”可能会翻译成不同的词,或同一动词的语法形式,具体取决于“编辑”的内容。
  • 您可以随意更改英文文本,而不会破坏其他语言的 gettext 键
  • 编号参数处理具有不同主语/动词/等顺序的语言

如果您在图像中有文字(例如某些按钮),您也需要注意这一点。 Gettext 也可以翻译文件名(images/buttons/en/submit.png => images/buttons/fr/valider.png 虽然一个简单的正则表达式也很好)并且不要忘记使用屏幕阅读器的盲人。

对于多语言用户生成的内容(存储在表格中),通常的关系方法更好。

表格帖子(post_id ...) 表posts_translated(post_id外键、language_id外键、title、text等)

这将允许您将 SQL 用于所有内容,例如显示帖子可用的语言列表、允许使用默认语言、显示未翻译的帖子、全文搜索等。

【讨论】:

    【解决方案2】:

    棘手的问题。

    根据我的经验,翻译过程确实很难管理 - 您不想因为“创建帖子”权限的乌兹别克语翻译尚未获得批准而推迟发布应用程序。

    我从未使用过 gettext,但 .Net 和 Java 等价物要求您将带有翻译的文件放在 Web 服务器上;这通常被视为部署,至少必须通过版本控制例程 - 这也可能有点痛苦......

    如果您能侥幸成功,我会同意为您的 gettext 键使用 TableName_ColumnName 约定,并将所有用户可见的系统消息存储在 gettext 文件中。

    我认为系统消息的本地化是前端问题,不应该在数据库中。您的业​​务实体(帖子、购物项目等)可以在哪里本地化,这是一个域问题,应该反映在您的数据库架构中。

    【讨论】:

      【解决方案3】:

      我认为gettext 更适合用于消息、按钮、标题等的翻译。而不是动态内容。为此,您可以根据项目要求采用几种不同的方式。首先,你应该决定:

      1. 翻译机制应该有多灵活。您是否需要经常添加新的翻译表或更新现有表结构?您是否需要经常添加新语言?
      2. 您是否需要跟踪原始内容的更改并在更改原始内容时停用所有翻译。
      3. 您是否需要后端来对翻译进行预审(如果有多个人参与翻译)

      对于具有最大灵活性和可扩展性的解决方案,您可以按照以下方式进行。原始表保持原样,没有变化。对于翻译,您可以添加一个带有列的表 translations

      • id主键
      • object_table翻译后的表名
      • object_id 对翻译对象的引用
      • languagelanguage_id 如果您需要动态管理语言
      • field翻译字段名称
      • original_md5原始字段值的哈希(如果您需要跟踪更改)
      • translation
      • author_idpublisheddate 以及审核所需的其他字段(如果需要)。

      然后,在另一个表或配置文件中描述要翻译的表和字段。您还可以描述字段的类型,例如texttextareahtmlfile等等。

      例如:

      $translatedFields = array(
        // here key stands for translated table name
        'posts' => array(
          // here key stands for translated field
          // and value for field's type
          'title' => 'text',
          'body' => 'html',
        ),
      );
      

      然后,在您的数据访问层中,您确定当前语言并将所有 SELECT 查询替换为要翻译的表。一点正则表达式的魔力和查询的严格语法可以在这里为您提供帮助。查询SELECT title, body FROM posts WHERE posts.id='1'变成了

      SELECT
        IF(posts_title_translation.translation IS NULL,
          posts.title,
          posts_title_translation.translation) AS title,
        IF(posts_body_translation.translation IS NULL,
          posts.body,
          posts_body_translation.translation) AS body
      FROM posts
      
      LEFT JOIN translations AS posts_title_translation
      ON posts_title_translation.object_id = posts.id
        AND posts_title_translation.object_table = 'posts'
        AND posts_title_translation.language = '$language'
        AND posts_title_translation.field = 'title'
      ---- And if you need premoderation, then filter off unpublished translations
      --AND posts_title_translation.published
      
      LEFT JOIN translations AS posts_body_translation
      ON posts_body_translation.object_id = posts.id
        AND posts_body_translation.object_table = 'posts'
        AND posts_body_translation.language = '$language'
        AND posts_body_translation.field = 'body'
        --AND posts_body_translation.published
      
      WHERE posts.id = '1'
      

      (SELECT 部分中的 IF 表达式允许您在没有翻译完成或发布时选择原始字段。

      这就是您可以拥有灵活的 i18n 系统的方式。为每个单独的字段进行翻译,并在选择时自动在数据访问层中替换。

      这有点棘手,而且必须说,部分受到 Joomla! 的 Joom!Fish 扩展的影响,但我就是这样做的。

      我将为更简单的解决方案添加另一个答案,因为这已经太大了。

      【讨论】:

      • -1 的解决方案会占用大量性能并破坏关系完整性、外键、全文搜索等。
      • 性能可以通过缓存来保存。但是全文搜索和相关的东西会被我的解决方案打破。是的,有点没想到。感谢批评,@peufeu!
      • 我在使用这个(通用翻译表)还是 peufeu 在他的答案中发布的内容(每张表 1 个翻译表)之间犹豫不决?在性能方面,为什么一般表会慢一些?
      • 因为不是简单的查询SELECT * FROM table,您将获得带有 N 个左连接到翻译表的查询(其中 N 是可翻译字段的数量)。但是如果查询结果被缓存,那么性能不会有太大差异,因为这些复杂的查询只会执行一次——生成缓存,之后创建缓存的方式没有区别。如果您的内容更新太频繁,那么是的,这将是一个问题。
      【解决方案4】:

      对于更简单的解决方案,但无需更新表的结构,您可以在每个已翻译字段中存储带有翻译的序列化数组。例如:

      id    title
      1     a:2:{s:7:"english";s:12:"Hello World!";s:7:"spanish";s:14:"¡Hola, mundo!";}
      

      然后,当通过 id 加载记录时,您只需调用 unserialize 反序列化内容并选择所需的翻译。您还可以使用json_encodejson_decode 进行序列化和反序列化。它可以在您的模型或基础模型类中完成。

      但当您必须支持多种语言时,这可能会成为问题,因为所有字段的大小都应乘以多种语言。

      编辑但是,正如peufeu 好心指出的那样,此解决方案破坏了全文搜索,并且不允许您使用简单的 SQL 查询检查现有翻译。因此,如果您使用全文搜索或需要通过“已翻译”标志选择内容,不要使用它

      【讨论】:

      • -1 用于性能消耗(将数据库网络流量乘以语言数量),破坏全文搜索和完整性,无法通过简单的 SQL 查询发现哪些字符串未翻译,等等。
      • @peufeu 谢谢,但我在回答中警告了性能。当语言数量较少时应使用它。无论如何,良好的缓存可以处理这个问题,IMO。但同样,索引和完整性是我没有想到的东西。我已经用更多警告更新了我的答案。
      猜你喜欢
      • 1970-01-01
      • 2012-08-12
      • 2017-09-02
      • 2015-07-07
      • 2011-04-12
      • 1970-01-01
      • 2010-09-14
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多