Help us learn about your current experience with the documentation. Take the survey.

GitLab 国际化

在处理国际化(i18n)工作时,我们使用 GNU gettext,因为它是该领域最常用的工具,且有许多应用可协助我们使用。

本页描述的所有 rake 命令必须在 GitLab 实例上运行。该实例通常是 GitLab Development Kit (GDK)。

设置 GitLab Development Kit (GDK)

要处理 GitLab Community Edition 项目,您必须通过 GDK 下载并配置它。

准备好 GitLab 项目后,即可开始处理翻译工作。

工具

使用以下工具:

  • 自定义工具辅助日常翻译开发工作:

    • tooling/bin/gettext_extractor locale/gitlab.pot:扫描所有源文件以查找待翻译的新内容
    • rake gettext:compile:读取 PO 文件内容并生成包含所有前端可用翻译的 JS 文件
    • rake gettext:lint验证 PO 文件
  • gettext_i18n_rails: 此 gem 允许我们翻译模型、视图和控制器中的内容。 其底层使用 fast_gettext

    它还提供以下 Rake 任务(日常开发中较少使用):

    • rake gettext:add_language[language]添加新语言
    • rake gettext:find:解析 Rails 应用中几乎所有文件,查找标记为翻译的内容,并更新 PO 文件
    • rake gettext:pack:处理 PO 文件并生成应用程序使用的二进制 MO 文件
  • PO 编辑器:有多个应用可协助处理 PO 文件。 推荐使用 Poedit, 支持 macOS、GNU/Linux 和 Windows 系统。

准备页面翻译

您必须使用以下可用辅助函数标记字符串为可翻译内容。请注意,字符串在工具中翻译时,其使用场景可能不明确。建议通过命名空间为领域特定字符串提供更多上下文给译者。

有四种文件类型:

  • Ruby 文件:模型和控制器
  • HAML 文件:视图文件
  • ERB 文件:用于邮件模板
  • JavaScript 文件:主要处理 Vue 模板

Ruby 文件

如果方法或变量处理原始字符串,例如:

def hello
  "Hello world!"
end

或:

hello = "Hello world!"

可通过以下方式标记内容为可翻译:

def hello
  _("Hello world!")
end

或:

hello = _("Hello world!")

注意在类或模块级别翻译字符串时,这些内容仅在类加载时评估一次。例如:

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

此内容在类加载时翻译,导致错误消息始终使用默认语言。Active Record 的 :message 选项支持 Proc,应改为:

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

API 中的消息(lib/api/app/graphql)无需外部化。

HAML 文件

给定 HAML 中的内容:

%h1 Hello world!

可通过以下方式标记为可翻译:

%h1= _("Hello world!")

ERB 文件

给定 ERB 中的内容:

<h1>Hello world!</h1>

可通过以下方式标记为可翻译:

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

JavaScript 文件

~/locale 模块导出以下外部化关键函数:

  • __() 标记内容为可翻译(双下划线括号)
  • s__() 标记命名空间内容为可翻译(s 双下划线括号)
  • n__() 标记复数内容为可翻译(n 双下划线括号)
import { __, s__, n__ } from '~/locale';

const defaultErrorMessage = s__('Branches|Create branch failed.');
const label = __('Subscribe');
const message =  n__('Apple', 'Apples', 3)

要测试 JavaScript 翻译,请了解如何从 UI 手动测试翻译

Vue 文件

在 Vue 文件中,我们通过 translate mixin 向 Vue 模板提供以下函数:

  • __()
  • s__()
  • n__()
  • sprintf

这意味着您可以在 Vue 模板中直接外部化字符串,无需从 ~/locale 文件导入函数:

<template>
  <h1>{{ s__('Branches|Create a new branch') }}</h1>
  <gl-button>{{ __('Create branch') }}</gl-button>
</template>

如果需要在 Vue 组件的 JavaScript 中翻译字符串,可按 JavaScript 文件 部分描述从 ~/locale 文件导入必要的函数。

要测试 Vue 翻译,请了解如何从 UI 手动测试翻译

测试文件 (RSpec)

对于 RSpec 测试,对已外部化内容的断言不应硬编码,因为我们可能需要使用非默认语言运行测试,硬编码内容的测试会失败。

这意味着任何对已外部化内容的断言都应调用相同的外部化方法以匹配翻译。

错误示例:

click_button 'Submit review'

expect(rendered).to have_content('Thank you for your feedback!')

正确示例:

click_button _('Submit review')

expect(rendered).to have_content(_('Thank you for your feedback!'))

测试文件 (Jest)

对于前端 Jest 测试,断言无需引用外部化方法。前端测试环境中已模拟外部化,因此断言在不同语言中具有确定性(相关 MR)。

示例:

// 错误。前端环境中无需此操作。
expect(findText()).toBe(__('Lorem ipsum dolor sit'));
// 正确。
expect(findText()).toBe('Lorem ipsum dolor sit');

建议

将翻译放置尽可能靠近使用位置。优先使用内联翻译而非包含翻译的变量。翻译的最佳描述是其键名。这提高了代码可读性,有助于减轻保存代码上下文的认知负担,也使重构更简单,因为我们无需额外维护变量。

// 错误。变量定义远离使用位置
const TITLE = __('Organisations');

function transform() {
  return TITLE;
}

// 正确。
function transform() {
  return __('Organisations');
}
共享翻译

有时翻译可在文件或模块的多个位置使用。此时可使用共享翻译的变量,但需注意以下事项:

  • 内联翻译具有更好的代码清晰度。不要仅因 DRY 原则就将翻译放入变量。
  • 插入或拼接翻译时需谨慎。更多信息请参见使用变量动态插入文本
  • 如果两个翻译共享相同的英文键,并不意味着其他语言中这两个位置的翻译相同。建议在适当位置使用命名空间

如果特定情况下优先使用包含翻译的变量,请遵循以下声明和放置指南:

在 JavaScript 文件中,声明包含翻译的常量:

const ORGANISATIONS_TITLE = __('Organisations');

在 Vue 单文件组件中,可在组件的 $options 对象中定义 i18n 属性:

<script>
  export default {
    i18n: {
      buttonLabel: s__('Plan|Button Label')
    }
  },
</script>

<template>
  <gl-button :aria-label="$options.i18n.buttonLabel">
    {{ $options.i18n.buttonLabel }}
  </gl-button>
</template>

在模块中,如果多个文件重复使用相同翻译,可将其添加到 constants.jsi18n.js 文件并在模块中导入这些翻译。但这会增加代码库的复杂度,应谨慎使用。

另一个应避免的做法是在规范中导出副本字符串。虽然这看起来像是更高效的测试(如果更改副本,测试仍会通过!),但它会产生额外问题:

  • 导入的值可能为 undefined,导致测试出现假阳性(特别是导入 i18n 对象时,请参见将常量导出为基本类型)。
  • 更难测试预期内容(应期望哪个副本)。
  • 由于不重写断言而是假设常量值正确,更容易遗漏拼写错误。
  • 此方法收益甚微。在组件中更新副本而不更新规范,不足以抵消潜在问题。

例如:

import { MSG_ALERT_SETTINGS_FORM_ERROR } from 'path/to/constants.js';

// 错误。`MSG_ALERT_SETTINGS_FORM_ERROR` 的实际文本是什么?如果 `wrapper.text()` 返回 undefined,测试仍可能通过错误值!
expect(wrapper.text()).toBe(MSG_ALERT_SETTINGS_FORM_ERROR);
// 极差。同上问题且通过 vm 属性!
expect(wrapper.text()).toBe(MyComponent.vm.i18n.buttonLabel);
// 正确。预期内容非常明确,不会有意外。
expect(wrapper.text()).toBe('There was an error: Please refresh and hope for the best!');

动态翻译

更多详情请参见我们如何保持翻译动态

修改已翻译字符串

如果您更改 GitLab 中的源字符串,必须在推送更改前更新 pot 文件。如果 pot 文件过时,预推送检查和 gettext 的流水线作业将失败。

处理特殊内容

插值

翻译文本中的占位符应与相应源文件的代码风格匹配。例如在 Ruby 中使用 %{created_at},在 JavaScript 中使用 %{createdAt}。确保添加链接时避免拆分句子

  • 在 Ruby/HAML 中:

    format(_("Hello %{name}"), name: 'Joe') => 'Hello Joe'
  • 在 Vue 中:

    当满足以下条件时使用 GlSprintf 组件:

    • 在翻译字符串中包含子组件
    • 在翻译字符串中包含 HTML
    • 使用 sprintf 并传递 false 作为第三个参数以阻止转义占位符值

    例如:

    <gl-sprintf :message="s__('ClusterIntegration|Learn more about %{linkStart}zones%{linkEnd}')">
      <template #link="{ content }">
        <gl-link :href="somePath">{{ content }}</gl-link>
      </template>
    </gl-sprintf>

    其他情况下,在计算属性中使用 sprintf 可能更简单。例如:

    <script>
    import { __, sprintf } from '~/locale';
    
    export default {
      ...
      computed: {
        userWelcome() {
          return sprintf(__('Hello %{username}'), { username: this.user.name });
        }
      }
      ...
    }
    </script>
    
    <template>
      <span>{{ userWelcome }}</span>
    </template>
  • 在 JavaScript 中(无法使用 Vue 时):

    import { __, sprintf } from '~/locale';
    
    sprintf(__('Hello %{username}'), { username: 'Joe' }); // => 'Hello Joe'
    

    如果需要在翻译中使用标记,请使用 sprintf 并通过传递 false 作为第三个参数阻止转义占位符值。您必须自行转义所有插值的动态值,例如使用 lodashescape

    import { escape } from 'lodash';
    import { __, sprintf } from '~/locale';
    
    let someDynamicValue = '<script>alert("evil")</script>';
    
    // 危险:
    sprintf(__('This is %{value}'), { value: `<strong>${someDynamicValue}</strong>`, false);
    // => 'This is <strong><script>alert('evil')</script></strong>'
    
    // 错误:
    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;'
    
    // 正确:
    sprintf(__('This is %{value}'), { value: `<strong>${escape(someDynamicValue)}</strong>` }, false);
    // => 'This is <strong>&lt;script&gt;alert(&#x27;evil&#x27;)&lt;/script&gt;</strong>'
    

复数形式

  • 在 Ruby/HAML 中:

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

    使用插值:

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

    避免在单数字符串中使用 %d 或计数变量。这使某些语言的翻译更自然。

  • 在 JavaScript 中:

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

    使用插值:

    n__('Last day', 'Last %d days', x)
    // => 当 x == 1 时: 'Last day'
    // => 当 x == 2 时: 'Last 2 days'
    
  • 在 Vue 中:

    组织 Vue 文件翻译字符串的推荐方法之一是将其提取到 constants.js 文件。 当存在复数字符串时这很困难,因为常量文件中无法知道 count 变量。 为此,我们建议创建一个接受 count 参数的函数:

    // .../feature/constants.js
    import { n__ } from '~/locale';
    
    export const I18N = {
      // 仅单数的字符串无需是函数
      someDaysRemain: __('Some days remain'),
      daysRemaining(count) { return n__('%d day remaining', '%d days remaining', count); },
    };

    然后在 Vue 组件中使用该函数获取正确的复数形式:

    // .../feature/components/days_remaining.vue
    import { sprintf } from '~/locale';
    import { I18N } from '../constants';
    
    <script>
      export default {
        props: {
          days: {
            type: Number,
            required: true,
          },
        },
        i18n: I18N,
      };
    </script>
    
    <template>
      <div>
        <span>
          单数字符串
          {{ $options.i18n.someDaysRemain }}
        </span>
        <span>
          复数字符串
          {{ $options.i18n.daysRemaining(days) }}
        </span>
      </div>
    </template>

n_n__ 方法仅应用于获取同一字符串的复数翻译,不应用于控制不同数量的不同字符串显示逻辑。对于相似字符串,请复数化整个句子以提供最丰富的翻译上下文。某些语言的目标复数形式数量不同。例如,简体中文在我们的翻译工具中只有一个目标复数形式。这意味着译者只能选择翻译其中一个字符串,翻译在另一种情况下无法按预期工作。

以下是一些示例:

示例 1:不同字符串

使用此方式:

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

而非此方式:

# 错误用法示例
format(n_("%{project_name}", "%d projects selected", count), project_name: 'GitLab')

示例 2:相似字符串

使用此方式:

n__('Last day', 'Last %d days', days.length)

而非此方式:

# 错误用法示例
const pluralize = n__('day', 'days', days.length)

if (days.length === 1 ) {
  return sprintf(s__('Last %{pluralize}', pluralize)
}

return sprintf(s__('Last %{dayNumber} %{pluralize}'), { dayNumber: days.length, pluralize })

命名空间

命名空间是将相关翻译分组的方式。它们通过添加竖线符号 (|) 前缀为译者提供上下文。例如:

'Namespace|Translated string'

命名空间:

  • 解决词汇歧义。例如:Promotions|Promote vs Epic|Promote
  • 允许译者专注于翻译属于同一产品区域的外部化字符串,而非任意字符串
  • 提供语言上下文帮助译者

某些语言比英语更具上下文性。例如,cancel 根据使用方式可能有不同翻译。为定义使用上下文,始终为英文 UI 文本添加命名空间。

命名空间应为 PascalCase。

  • 在 Ruby/HAML 中:

    s_('OpenedNDaysAgo|Opened')

    如果未找到翻译,则返回 Opened

  • 在 JavaScript 中:

    s__('OpenedNDaysAgo|Opened')

翻译时应移除命名空间。更多详情请参见翻译指南

HTML

我们不再直接在提交翻译的字符串中包含 HTML。原因如下:

  1. 翻译后的字符串可能意外包含无效 HTML。
  2. 翻译字符串可能成为 XSS 攻击向量,如 Open Web Application Security Project (OWASP) 所述。

要在翻译字符串中包含格式,可执行以下操作:

  • 在 Ruby/HAML 中:

    safe_format(_('Some %{strongOpen}bold%{strongClose} text.'), tag_pair(tag.strong, :strongOpen, :strongClose))
    # => 'Some <strong>bold</strong> text.'
  • 在 JavaScript 中:

      sprintf(__('Some %{strongOpen}bold%{strongClose} text.'), { strongOpen: '<strong>', strongClose: '</strong>'}, false);
    
      // => 'Some <strong>bold</strong> text.'
    
  • 在 Vue 中:

    参见插值部分。

此翻译辅助问题完成后,我们计划更新在翻译字符串中包含格式的流程。

包含尖括号

如果字符串包含非 HTML 使用的尖括号 (</>), rake gettext:lint 检查器仍会标记。为避免此错误,请使用相应的 HTML 实体代码 (&lt;&gt;):

  • 在 Ruby/HAML 中:

    safe_format(_('In &lt; 1 hour'))
    
    # => 'In < 1 hour'
  • 在 JavaScript 中:

    import { sanitize } from '~/lib/dompurify';
    
    const i18n = { LESS_THAN_ONE_HOUR: sanitize(__('In &lt; 1 hour'), { ALLOWED_TAGS: [] }) };
    
    // ... 使用字符串
    element.innerHTML = i18n.LESS_THAN_ONE_HOUR;
    
    // => 'In < 1 hour'
    
  • 在 Vue 中:

    <gl-sprintf :message="s__('In &lt; 1 hours')"/>
    
    // => 'In < 1 hour'

数字

不同语言环境可能使用不同的数字格式。为支持数字本地化,我们使用 formatNumber,它利用 toLocaleString()

默认情况下,formatNumber 使用当前用户语言环境将数字格式化为字符串。

  • 在 JavaScript 中:
import { formatNumber } from '~/locale';

// 假设 "用户偏好 > 语言" 设置为 "英语":

const tenThousand = formatNumber(10000); // "10,000" (英语环境使用逗号作为千位分隔符)
const fiftyPercent = formatNumber(0.5, { style: 'percent' }) // "50%" (其他选项传递给 toLocaleString)
  • 在 Vue 模板中:
<script>
import { formatNumber } from '~/locale';

export default {
  //...
  methods: {
    // ...
    formatNumber,
  },
}
</script>
<template>
<div class="my-number">
  {{ formatNumber(10000) }} <!-- 10,000 -->
</div>
<div class="my-percent">
  {{ formatNumber(0.5,  { style: 'percent' }) }} <!-- 50% -->
</div>
</template>

日期 / 时间

  • 在 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

  • 在 Ruby/HAML 中,有两种添加日期和时间格式的方式:

    • 使用 l 辅助函数:例如 l(active_session.created_at, format: :short)。我们为日期时间定义了一些预定义格式。如果需要添加新格式(因为其他代码部分可能受益),请将其添加到 en.yml 文件。
    • 使用 strftime:例如 milestone.start_date.strftime('%b %-d')。当 en.yml 中定义的格式都不满足日期/时间规范需求,且无需将其作为新格式添加(因为非常特定,例如仅在单个视图中使用)时,我们使用 strftime

最佳实践

最小化翻译更新

更新可能导致该字符串的翻译丢失。为最小化风险,避免更改字符串,除非它们:

  • 为用户增加价值
  • 为译者提供额外上下文

例如,避免以下更改:

- _('Number of things: %{count}') % { count: 10 }
+ n_('Number of things: %d', 10)

保持翻译动态

某些情况下,在数组或哈希中保持翻译在一起是合理的。

示例:

  • 下拉列表的映射
  • 错误消息

为存储此类数据,使用常量似乎是最佳选择。但这不适用于翻译。

例如,避免此方式:

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

此方法使用首次调用该方法的用户语言环境记忆化翻译。

为避免这些问题,保持翻译动态。

正确示例:

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

有时存在动态翻译,运行 bin/rake gettext:find 时解析器无法找到。对于这些场景,您可以使用 N_ 方法。还有另一种方法可翻译验证错误消息

拆分句子

切勿拆分句子,因为它假设所有语言的句子语法和结构相同。

例如,此方式:

{{ 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 }) }}

添加链接时避免拆分句子

在翻译句子之间使用链接时也适用此原则。否则,某些语言中这些文本无法翻译。

  • 在 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 = link_to('', zones_link_url, target: '_blank', rel: 'noopener noreferrer')
    = safe_format(s_('ClusterIntegration|Learn more about %{zones_link_start}zones%{zones_link_end}'), tag_pair(zones_link, :zones_link_start, :zones_link_end))
  • 在 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>'
        }, false)
    }}

    将链接起始和结束 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>',
        }, false)
    }}

其原因是某些语言中单词根据上下文变化。例如,日语中根据用法会添加 は(主语)和 を(宾语)。如果从句子中提取单个单词,无法正确翻译。

如有疑问,请尝试遵循此 Mozilla 开发者文档 中描述的最佳实践。

始终将字符串字面量传递给翻译辅助函数

tooling/bin/gettext_extractor locale/gitlab.pot 脚本解析代码库并提取所有来自翻译辅助函数的字符串,准备翻译。

如果字符串作为变量或函数调用传递,脚本无法解析它们。因此,请始终将字符串字面量传递给辅助函数。

// 正确
__('Some label');
s__('Namespace', 'Label');
s__('Namespace|Label');
n__('%d apple', '%d apples', appleCount);

// 错误
__(LABEL);
s__(getLabel());
s__(NAMESPACE, LABEL);
n__(LABEL_SINGULAR, LABEL_PLURAL, appleCount);

使用变量动态插入文本

当文本值作为变量用于可翻译字符串时,必须特别注意确保不同语言的语法正确性。

风险与挑战

使用变量向可翻译字符串添加文本时,可能面临以下本地化挑战:

  • 性别一致:具有语法性别的语言可能需要根据插入名词的性别使用不同形式的冠词、形容词或代词。例如,法语中冠词、形容词和一些过去分词必须与名词的性别和句子中的位置保持一致。
  • 格和变格:在具有格的语言中(如德语),插入的文本可能需要根据其在句子中的语法角色使用不同形式。
  • 词序:不同语言有不同的词序要求,插入的文本可能需要出现在句子中的不同位置才能使翻译自然。

最佳实践

  1. 尽可能避免将文本作为变量添加
    • 与其使用带变量的单个字符串,不如为每种情况创建唯一字符串。例如:
    # 替代:
    s_('WorkItem|Adds this %{workItemType} as related to %{relatedWorkItemType}')

    # 创建单独字符串:
    s_('WorkItem|Adds this task as related to incident')
    s_('WorkItem|Adds this incident as related to task')
  1. 使用主题-评论结构而非类句式排列: 当无法避免使用变量时,考虑重构消息以使用主题-评论结构而非完整句子:
   # 替代带插入变量的句子:
   s_('WorkItem|Adds this %{workItemType} as related to %{relatedWorkItemType}')

   # 使用主题-评论结构:
   s_('WorkItem|Related items: %{workItemType} → %{relatedWorkItemType}')

可翻译字符串的大小写转换

不同语言的大小写规则可能与英语不同。例如,德语中所有名词无论在句子中位置如何均需大写。避免在可翻译字符串上使用 downcasetoLocaleLowerCase()。让译者控制文本。

  • 上下文相关的大小写

虽然 toLocaleLowerCase() 方法是语言环境感知的,但它无法处理上下文特定的大小写需求。例如:

    # 这强制小写,但对许多语言可能无效:
    job_type = "CI/CD Pipeline".toLocaleLowerCase()
    s_("Jobs|Starting a new %{job_type}") % { job_type: job_type }

    # 在德语中会错误显示:
    # "Starting a new ci/cd pipeline"
    # 应显示为:
    # "Starting a new CI/CD Pipeline"  (Pipeline 是名词必须大写)

    # 在法语中可能显示:
    # "Starting a new ci/cd pipeline"
    # 应显示为:
    # "Démarrer un nouveau pipeline CI/CD"  (技术术语可能保留原始大小写)

使用新内容更新 PO 文件

现在新内容已标记为可翻译,运行以下命令更新 locale/gitlab.pot 文件:

tooling/bin/gettext_extractor locale/gitlab.pot

此命令用新提取的字符串更新 locale/gitlab.pot 文件,并移除任何未使用的字符串。更改推送到默认分支后,Crowdin 会获取这些内容并呈现供翻译。

您无需提交对 locale/[language]/gitlab.po 文件的任何更改。当从 Crowdin 合并翻译时,它们会自动更新。

如果 gitlab.pot 文件存在合并冲突,可删除该文件并使用相同命令重新生成。

验证 PO 文件

为确保翻译文件保持最新,CI 上的 static-analysis 作业中运行了检查器。要在本地检查 PO 文件的调整,可运行 rake gettext:lint

检查器考虑以下因素:

  • 有效的 PO 文件语法
  • 变量使用
    • 仅允许一个未命名变量 (%d),因为变量顺序在不同语言中可能变化
    • 消息 ID 中使用的所有变量都在翻译中使用
    • 翻译中不应使用消息 ID 中不存在的变量
  • 翻译错误
  • 尖括号 (<>) 的存在

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

`locale/zh_HK/gitlab.po` 中的错误:
  PO 语法错误
    SimplePoParser::ParserErrorSyntax error in lines
    Syntax error in msgctxt
    Syntax error in msgid
    Syntax error in msgstr
    Syntax error in message_line
    在消息文本的双引号字符后,行尾应仅包含空格。
    错误前的解析结果:'{:msgid=>["", "You are going to delete %{project_name_with_namespace}.\n", "Deleted projects CANNOT be restored!\n", "Are you ABSOLUTELY sure?"]}'
    SimplePoParser 过滤回溯:SimplePoParser::ParserError
`locale/zh_TW/gitlab.po` 中的错误:
  1 pipeline
    <%d 條流水線> 使用未知变量:[%d]
    翻译到 zh_TW 失败:参数不足

在此输出中,locale/zh_HK/gitlab.po 存在语法错误。文件 locale/zh_TW/gitlab.po 中,消息 ID 为 1 pipeline 的翻译使用了消息 ID 中不存在的变量。

添加新语言

只有当至少 10% 的字符串已翻译并批准后,才应在用户偏好中添加新语言选项。尽管可能已翻译更多字符串,但只有批准的翻译才会在 GitLab UI 中显示。

翻译比例低于 2% 的语言在 UI 中不可用。

假设您要为新语言(例如法语)添加翻译:

  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"

从 UI 手动测试翻译

要手动测试 Vue 翻译:

  1. 将 GitLab 本地化更改为非英语语言。
  2. 使用 bin/rake gettext:compile 生成 JSON 文件。