# Internationalization for GitLab

> 原文：[https://docs.gitlab.com/ee/development/i18n/externalization.html](https://docs.gitlab.com/ee/development/i18n/externalization.html)

*   [Setting up GitLab Development Kit (GDK)](#setting-up-gitlab-development-kit-gdk)
*   [Tools](#tools)
*   [Preparing a page for translation](#preparing-a-page-for-translation)
    *   [Ruby files](#ruby-files)
    *   [HAML files](#haml-files)
    *   [ERB files](#erb-files)
    *   [JavaScript files](#javascript-files)
    *   [Dynamic translations](#dynamic-translations)
*   [Working with special content](#working-with-special-content)
    *   [Interpolation](#interpolation)
    *   [Plurals](#plurals)
    *   [Namespaces](#namespaces)
    *   [Dates / times](#dates--times)
*   [Best practices](#best-practices)
    *   [Keep translations dynamic](#keep-translations-dynamic)
    *   [Splitting sentences](#splitting-sentences)
        *   [Avoid splitting sentences when adding links](#avoid-splitting-sentences-when-adding-links)
            *   [Vue components interpolation](#vue-components-interpolation)
*   [Updating the PO files with the new content](#updating-the-po-files-with-the-new-content)
    *   [Validating PO files](#validating-po-files)
*   [Adding a new language](#adding-a-new-language)

# Internationalization for GitLab[](#internationalization-for-gitlab "Permalink")

在 GitLab 9.2 中[引入](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/10669) .

为了使用国际化（i18n），使用了[GNU gettext](https://www.gnu.org/software/gettext/) ，因为它是该任务最常用的工具，并且有许多应用程序可以帮助我们使用它.

## Setting up GitLab Development Kit (GDK)[](#setting-up-gitlab-development-kit-gdk "Permalink")

为了能够在[GitLab 社区版](https://gitlab.com/gitlab-org/gitlab-foss)项目上工作，您必须通过[GDK](https://gitlab.com/gitlab-org/gitlab-development-kit/blob/master/doc/set-up-gdk.md)下载并配置它.

准备好 GitLab 项目后，就可以开始进行翻译了.

## Tools[](#tools "Permalink")

使用以下工具：

1.  [`gettext_i18n_rails`](https://github.com/grosser/gettext_i18n_rails) ：这个 gem 使我们可以转换模型，视图和控制器中的内容. 此外，它还使我们可以访问以下 Rake 任务：
    *   `rake gettext:find` ：解析 Rails 应用程序中的几乎所有文件，以查找已标记为要翻译的内容. 最后，它将使用找到的新内容更新 PO 文件.
    *   `rake gettext:pack` ：处理 PO 文件并生成二进制的 MO 文件，最终由应用程序使用.
2.  [`gettext_i18n_rails_js`](https://github.com/webhippie/gettext_i18n_rails_js) ：此 gem 对于使翻译在 JavaScript 中可用非常有用. 它提供以下 Rake 任务：
    *   `rake gettext:po_to_json` ：从 PO 文件中读取内容，并生成包含所有可用翻译的 JSON 文件.
3.  PO 编辑器：有多个应用程序可以帮助我们处理 PO 文件， [Poedit](https://poedit.net/download)是一个不错的选择，可用于 macOS，GNU / Linux 和 Windows.

## Preparing a page for translation[](#preparing-a-page-for-translation "Permalink")

我们基本上有 4 种类型的文件：

1.  Ruby 文件：基本上是模型和控制器.
2.  HAML 文件：这些是视图文件.
3.  ERB 文件：用于电子邮件模板.
4.  JavaScript 文件：我们主要需要使用 Vue 模板.

### Ruby files[](#ruby-files "Permalink")

例如，如果存在使用原始字符串的方法或变量，则：

```
def hello
  "Hello world!"
end 
```

Or:

```
hello = "Hello world!" 
```

您可以轻松地将该内容标记为要翻译的内容：

```
def hello
  _("Hello world!")
end 
```

Or:

```
hello = _("Hello world!") 
```

在类或模块级别转换字符串时要小心，因为在类加载时它们只会被评估一次.

例如：

```
validates :group_id, uniqueness: { scope: [:project_id], message: _("already shared with this group") } 
```

当加载该类时，这将被翻译，并导致错误消息始终位于默认语言环境中.

Active Record’s `:message` option accepts a `Proc`, so we can do this instead:

```
validates :group_id, uniqueness: { scope: [:project_id], message: -> (object, data) { _("already shared with this group") } } 
```

**注意：** API（ `lib/api/`或`app/graphql` ）中的消息无需外部化.

### HAML files[](#haml-files "Permalink")

考虑到 HAML 中的以下内容：

```
%h1 Hello world! 
```

您可以使用以下方式将该内容标记为要翻译的内容：

```
%h1= _("Hello world!") 
```

### ERB files[](#erb-files "Permalink")

考虑到 ERB 中的以下内容：

```
<h1>Hello world!</h1> 
```

您可以使用以下方式将该内容标记为要翻译的内容：

```
<h1><%= _("Hello world!") %></h1> 
```

### JavaScript files[](#javascript-files "Permalink")

在 JavaScript 中，我们添加了`__()` （双下划线括号）函数，您可以从`~/locale`文件中导入该函数. 例如：

```
import { __ } from '~/locale';
const label = __('Subscribe'); 
```

为了测试 JavaScript 翻译，您必须将 GitLab 本地化更改为英语以外的其他语言，并且必须使用`bin/rake gettext:po_to_json`或`bin/rake gettext:compile`生成 JSON 文件.

### Dynamic translations[](#dynamic-translations "Permalink")

有时，运行`bin/rake gettext:find`时，解析器无法找到一些动态转换. 对于这些情况，可以使用[`N_`方法](https://github.com/grosser/gettext_i18n_rails/blob/c09e38d481e0899ca7d3fc01786834fa8e7aab97/Readme.md#unfound-translations-with-rake-gettextfind) .

还有另一种方法可以[转换来自验证错误的消息](https://github.com/grosser/gettext_i18n_rails/blob/c09e38d481e0899ca7d3fc01786834fa8e7aab97/Readme.md#option-a) .

## Working with special content[](#working-with-special-content "Permalink")

### Interpolation[](#interpolation "Permalink")

翻译后的文字中的占位符应与相应源文件的代码样式匹配. 例如使用`%{created_at}`在 Ruby，但`%{createdAt}`在 JavaScript. [添加链接时，](#avoid-splitting-sentences-when-adding-links)请确保[避免拆分句子](#avoid-splitting-sentences-when-adding-links) .

*   在 Ruby / HAML 中：

    ```
    _("Hello %{name}") % { name: 'Joe' } => 'Hello Joe' 
    ```

*   在 Vue 中：

    请参见有关[Vue 分量插值](#vue-components-interpolation)的部分.

*   在 JavaScript 中（无法使用 Vue 时）：

    ```
    import { __, sprintf } from '~/locale';

    sprintf(__('Hello %{username}'), { username: 'Joe' }); // => 'Hello Joe' 
    ```

    如果要在翻译中使用标记并且正在使用 Vue，则**必须**使用[`gl-sprintf`](#vue-components-interpolation)组件. 如果由于某种原因您不能使用 Vue，请使用`sprintf`并通过将`false`用作第三个参数来阻止其转义占位符值. 您**必须**使用自己逃避任何插值动态值，例如`escape`从`lodash` .

    ```
    import { escape } from 'lodash';
    import { __, sprintf } from '~/locale';

    let someDynamicValue = '<script>alert("evil")</script>';

    // Dangerous:
    sprintf(__('This is %{value}'), { value: `<strong>${someDynamicValue}</strong>`, false);
    // => 'This is <strong><script>alert('evil')</script></strong>'

    // Incorrect:
    sprintf(__('This is %{value}'), { value: `<strong>${someDynamicValue}</strong>` });
    // => 'This is &lt;strong&gt;&lt;script&gt;alert(&#x27;evil&#x27;)&lt;/script&gt;&lt;/strong&gt;'

    // OK:
    sprintf(__('This is %{value}'), { value: `<strong>${escape(someDynamicValue)}</strong>`, false);
    // => 'This is <strong>&lt;script&gt;alert(&#x27;evil&#x27;)&lt;/script&gt;</strong>' 
    ```

### Plurals[](#plurals "Permalink")

*   在 Ruby / HAML 中：

    ```
    n_('Apple', 'Apples', 3)
    # => 'Apples' 
    ```

    使用插值：

    ```
    n_("There is a mouse.", "There are %d mice.", size) % size
    # => When size == 1: 'There is a mouse.'
    # => When size == 2: 'There are 2 mice.' 
    ```

    避免在单个字符串中使用`%d`或计数变量. 这样可以更自然地翻译某些语言.

*   在 JavaScript 中：

    ```
    n__('Apple', 'Apples', 3)
    // => 'Apples' 
    ```

    使用插值：

    ```
    n__('Last day', 'Last %d days', x)
    // => When x == 1: 'Last day'
    // => When x == 2: 'Last 2 days' 
    ```

`n_`方法仅应用于获取同一字符串的多个转换，而不能控制针对不同数量显示不同字符串的逻辑. 某些语言具有不同数量的目标复数形式-例如，中文（简体）在我们的翻译工具中仅具有一种目标复数形式. 这意味着翻译者将不得不选择只翻译一个字符串，而翻译在另一种情况下的表现将不符合预期.

例如，更喜欢使用：

```
if selected_projects.one?
  selected_projects.first.name
else
  n__("Project selected", "%d projects selected", selected_projects.count)
end 
```

而不是：

```
# incorrect usage example
n_("%{project_name}", "%d projects selected", count) % { project_name: 'GitLab' } 
```

### Namespaces[](#namespaces "Permalink")

命名空间是一种将属于在一起的翻译进行分组的方法. 它们通过在前缀后面加上条形符号（ `|` ）为我们的翻译人员提供上下文. 例如：

```
'Namespace|Translated string' 
```

命名空间具有以下优点：

*   它解决了词义上的歧义，例如： `Promotions|Promote` vs `Epic|Promote`
*   它使翻译人员可以专注于翻译属于相同产品区域而不是任意产品区域的外部化字符串.
*   它提供了语言环境以帮助翻译.

在某些情况下，例如，对于无处不在的 UI 单词和短语（如"取消"）或短语（如"保存更改"）而言，名称空间可能会适得其反.

命名空间应为 PascalCase.

*   在 Ruby / HAML 中：

    ```
    s_('OpenedNDaysAgo|Opened') 
    ```

    如果找不到翻译，它将返回`Opened` .

*   在 JavaScript 中：

    ```
    s__('OpenedNDaysAgo|Opened') 
    ```

注意：应从转换中删除名称空间. 有关[更多详细信息，](translation.html#namespaced-strings)请参见[翻译指南](translation.html#namespaced-strings) .

### Dates / times[](#dates--times "Permalink")

*   在 JavaScript 中：

```
import { createDateTimeFormat } from '~/locale';

const dateFormat = createDateTimeFormat({ year: 'numeric', month: 'long', day: 'numeric' });
console.log(dateFormat.format(new Date('2063-04-05'))) // April 5, 2063 
```

这利用了[`Intl.DateTimeFormat`](https://s0developer0mozilla0org.icopy.site/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat) .

*   在 Ruby / HAML 中，我们有两种向日期和时间添加格式的方法：

    1.  **通过`l`帮助器** ，即`l(active_session.created_at, format: :short)` . 我们有一些预定义的[日期](https://gitlab.com/gitlab-org/gitlab/blob/4ab54c2233e91f60a80e5b6fa2181e6899fdcc3e/config/locales/en.yml#L54)和[时间](https://gitlab.com/gitlab-org/gitlab/blob/4ab54c2233e91f60a80e5b6fa2181e6899fdcc3e/config/locales/en.yml#L262)格式. 如果您需要添加新格式，因为代码的其他部分可能会从中受益，则需要将其添加到[en.yml](https://gitlab.com/gitlab-org/gitlab/blob/master/config/locales/en.yml)文件中.
    2.  **通过`strftime`** ，即`milestone.start_date.strftime('%b %-d')` . 如果在[en.yml 上](https://gitlab.com/gitlab-org/gitlab/blob/master/config/locales/en.yml)定义的格式[均不](https://gitlab.com/gitlab-org/gitlab/blob/master/config/locales/en.yml)符合我们所需的日期/时间规范，并且由于非常特殊而无需将其添加为新格式的情况（例如，仅在单个视图中使用），则我们将使用`strftime` . .

## Best practices[](#best-practices "Permalink")

### Keep translations dynamic[](#keep-translations-dynamic "Permalink")

在某些情况下，将翻译内容保持在数组或哈希中是有意义的.

Examples:

*   下拉列表的映射
*   错误讯息

要存储此类数据，使用常数似乎是最佳选择，但这不适用于翻译.

不好，请避免：

```
class MyPresenter
  MY_LIST = {
    key_1: _('item 1'),
    key_2: _('item 2'),
    key_3: _('item 3')
  }
end 
```

首次加载该类时，将调用翻译方法（ `_` ），并将文本翻译为默认语言环境. 无论用户的语言环境是什么，这些值都不会再次转换.

将类方法与备注一起使用时，也会发生类似的情况.

不好，请避免：

```
class MyModel
  def self.list
    @list ||= {
      key_1: _('item 1'),
      key_2: _('item 2'),
      key_3: _('item 3')
    }
  end
end 
```

此方法将使用用户的语言环境来记住翻译，该用户首先"调用"此方法.

为避免这些问题，请保持翻译动态.

Good:

```
class MyPresenter
  def self.my_list
    {
      key_1: _('item 1'),
      key_2: _('item 2'),
      key_3: _('item 3')
    }.freeze
  end
end 
```

### Splitting sentences[](#splitting-sentences "Permalink")

请不要拆分句子，因为这会假定句子的语法和结构在所有语言中都是相同的.

例如，以下内容：

```
{{ s__("mrWidget|Set by") }}
{{ author.name }}
{{ s__("mrWidget|to be merged automatically when the pipeline succeeds") }} 
```

应该外部化如下：

```
{{ sprintf(s__("mrWidget|Set by %{author} to be merged automatically when the pipeline succeeds"), { author: author.name }) }} 
```

#### Avoid splitting sentences when adding links[](#avoid-splitting-sentences-when-adding-links "Permalink")

当在翻译的句子之间使用链接时，这也适用，否则这些文本在某些语言中不可翻译.

*   在 Ruby / HAML 中，而不是：

    ```
    - zones_link = link_to(s_('ClusterIntegration|zones'), 'https://cloud.google.com/compute/docs/regions-zones/regions-zones', target: '_blank', rel: 'noopener noreferrer')
    = s_('ClusterIntegration|Learn more about %{zones_link}').html_safe % { zones_link: zones_link } 
    ```

    将链接的开始和结束 HTML 片段设置为变量，如下所示：

    ```
    - zones_link_url = 'https://cloud.google.com/compute/docs/regions-zones/regions-zones'
    - zones_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: zones_link_url }
    = s_('ClusterIntegration|Learn more about %{zones_link_start}zones%{zones_link_end}').html_safe % { zones_link_start: zones_link_start, zones_link_end: '</a>'.html_safe } 
    ```

*   在 Vue 中，而不是：

    ```
    <template>
      <div>
        <gl-sprintf :message="s__('ClusterIntegration|Learn more about %{link}')">
          <template #link>
            <gl-link
              href="https://cloud.google.com/compute/docs/regions-zones/regions-zones"
              target="_blank"
            >zones</gl-link>
          </template>
        </gl-sprintf>
      </div>
    </template> 
    ```

    将链接的开始和结束 HTML 片段设置为占位符，如下所示：

    ```
    <template>
      <div>
        <gl-sprintf :message="s__('ClusterIntegration|Learn more about %{linkStart}zones%{linkEnd}')">
          <template #link="{ content }">
            <gl-link
              href="https://cloud.google.com/compute/docs/regions-zones/regions-zones"
              target="_blank"
            >{{ content }}</gl-link>
          </template>
        </gl-sprintf>
      </div>
    </template> 
    ```

*   在 JavaScript 中（无法使用 Vue 时），而不是：

    ```
    {{
        sprintf(s__("ClusterIntegration|Learn more about %{link}"), {
            link: '<a href="https://cloud.google.com/compute/docs/regions-zones/regions-zones" target="_blank" rel="noopener noreferrer">zones</a>'
        })
    }} 
    ```

    将链接的开始和结束 HTML 片段设置为占位符，如下所示：

    ```
    {{
        sprintf(s__("ClusterIntegration|Learn more about %{linkStart}zones%{linkEnd}"), {
            linkStart: '<a href="https://cloud.google.com/compute/docs/regions-zones/regions-zones" target="_blank" rel="noopener noreferrer">',
            linkEnd: '</a>',
        })
    }} 
    ```

其背后的原因是，在某些语言中，单词会根据上下文而变化. 例如，日语将は添加到句子的主语中，将を添加到宾语的主语中. 如果我们从句子中提取单个单词，则无法正确翻译.

如有疑问，请尝试遵循此[Mozilla Developer 文档中](https://s0developer0mozilla0org.icopy.site/en-US/docs/Mozilla/Localization/Localization_content_best_practices)描述的最佳做法.

##### Vue components interpolation[](#vue-components-interpolation "Permalink")

在 Vue 组件中翻译 UI 文本时，您可能希望在转换字符串中包括子组件. 您不能使用仅 JavaScript 的解决方案来呈现翻译，因为 Vue 不会意识到子组件并将其呈现为纯文本.

对于此用例，应使用在**GitLab UI 中**维护的`gl-sprintf`组件.

`gl-sprintf`组件接受`message`属性，该属性是可翻译的字符串，并且为字符串中的每个占位符公开一个命名的插槽，这使您可以轻松地包含 Vue 组件.

假设您要打印`Pipeline %{pipelineId} triggered %{timeago} by %{author}`的可翻译字符串`Pipeline %{pipelineId} triggered %{timeago} by %{author}` . 要将`%{timeago}`和`%{author}`占位符替换为 Vue 组件，以下是使用`gl-sprintf` ：

```
<template>
  <div>
    <gl-sprintf :message="__('Pipeline %{pipelineId} triggered %{timeago} by %{author}')">
      <template #pipelineId>{{ pipeline.id }}</template>
      <template #timeago>
        <timeago :time="pipeline.triggerTime" />
      </template>
      <template #author>
        <gl-avatar-labeled
          :src="pipeline.triggeredBy.avatarPath"
          :label="pipeline.triggeredBy.name"
        />
      </template>
    </gl-sprintf>
  </div>
</template> 
```

有关更多信息，请参见[`gl-sprintf`](https://gitlab-org.gitlab.io/gitlab-ui/?path=/story/base-sprintf--default)文档.

## Updating the PO files with the new content[](#updating-the-po-files-with-the-new-content "Permalink")

现在，新内容已标记为要翻译，我们需要使用以下命令更新`locale/gitlab.pot`文件：

```
bin/rake gettext:regenerate 
```

该命令将使用新外部化的字符串更新`locale/gitlab.pot`文件，并删除不再使用的任何字符串. 您应该检入此文件.一旦将更改保存在主文件中，它们将由[CrowdIn 提取](https://translate.gitlab.com)并显示以进行翻译.

我们不需要签入对`locale/[language]/gitlab.po`文件的任何更改. 当[来自 CrowdIn 的翻译合并](merging_translations.html)时，它们会自动更新.

如果`gitlab.pot`文件中存在合并冲突，则可以删除该文件并使用同一命令重新生成它.

### Validating PO files[](#validating-po-files "Permalink")

为了确保我们的翻译文件保持最新， `static-analysis`工作中有一个运行在 CI 上的 lint.

要在本地`rake gettext:lint` PO 文件中的调整，可以运行`rake gettext:lint` .

短绒将考虑以下因素：

*   有效的 PO 文件语法
*   可变用法
    *   仅一个未命名（ `%d` ）变量，因为变量的顺序可能会以不同的语言更改
    *   消息 ID 中使用的所有变量都在转换中使用
    *   转换中不应使用消息 ID 中没有的变量
*   翻译时出错.

错误按文件和消息 ID 分组：

```
Errors in `locale/zh_HK/gitlab.po`:
  PO-syntax errors
    SimplePoParser::ParserErrorSyntax error in lines
    Syntax error in msgctxt
    Syntax error in msgid
    Syntax error in msgstr
    Syntax error in message_line
    There should be only whitespace until the end of line after the double quote character of a message text.
    Parsing result before error: '{:msgid=>["", "You are going to remove %{project_name_with_namespace}.\\n", "Removed project CANNOT be restored!\\n", "Are you ABSOLUTELY sure?"]}'
    SimplePoParser filtered backtrace: SimplePoParser::ParserError
Errors in `locale/zh_TW/gitlab.po`:
  1 pipeline
    <%d 條流水線> is using unknown variables: [%d]
    Failure translating to zh_TW with []: too few arguments 
```

在此输出中， `locale/zh_HK/gitlab.po`具有语法错误. `locale/zh_TW/gitlab.po`具有转换中使用的变量，这些变量不在 ID `1 pipeline`的消息中.

## Adding a new language[](#adding-a-new-language "Permalink")

假设您想添加一种新语言的翻译，例如法语.

1.  第一步是在`lib/gitlab/i18n.rb`注册新语言：

    ```
    ...
    AVAILABLE_LANGUAGES = {
      ...,
      'fr' => 'Français'
    }.freeze
    ... 
    ```

2.  接下来，您需要添加语言：

    ```
    bin/rake gettext:add_language[fr] 
    ```

    如果要为特定区域添加新语言，则命令类似，您只需要用下划线（ `_` ）分隔区域即可. 例如：

    ```
    bin/rake gettext:add_language[en_GB] 
    ```

    请注意，您需要在大写字母中指定地区部分.

3.  现在已经添加了该语言，已经在以下路径下创建了一个新目录： `locale/fr/` . 现在，您可以开始使用 PO 编辑器来编辑位于`locale/fr/gitlab.edit.po`的 PO 文件.

4.  更新完翻译后，您需要处理 PO 文件以生成二进制 MO 文件，最后更新包含翻译的 JSON 文件：

    ```
    bin/rake gettext:compile 
    ```

5.  为了查看翻译后的内容，我们需要更改首选语言，该语言可以在用户的**设置** （ `/profile` ）下找到.

6.  在确认更改没问题之后，您可以继续提交新文件. 例如：

    ```
    git add locale/fr/ app/assets/javascripts/locale/fr/
    git commit -m "Add French translations for Value Stream Analytics page" 
    ```