GitLabの国際化

GitLab 9.2で導入されました

国際化(i18n)の作業にはGNU gettextが使われます。この作業に最も使われているツールであり、作業を助けてくれるアプリケーションがたくさんあるからです。

note
このページで説明するすべてのrake コマンドは GitLab インスタンス上で実行する必要があります。このインスタンスは通常、GitLab Development Kit(GDK)です。

GitLab Development Kitのセットアップ(GDK)

GitLab Community Editionプロジェクトで作業するには、GDKをダウンロードして設定する必要があります。

GitLabプロジェクトの準備ができたら、翻訳作業を始めましょう。

ツール

以下のツールを使用:

  • gettext_i18n_railsこのgemを使うことで、モデル、ビュー、コントローラのコンテンツを翻訳することができます。また、以下のRakeタスクにもアクセスできます:

    • rake gettext:find: Railsアプリケーションのほぼすべてのファイルを解析して、翻訳が必要なコンテンツを探します。そしてこの内容でPOファイルを更新します。
    • rake gettext:pack: POファイルを処理し、アプリケーションが使用するバイナリMOファイルを生成します。
  • gettext_i18n_rails_jsこのgemはJavaScriptで翻訳を利用できるようにします。以下のRakeタスクを提供します:

    • rake gettext:compile: POファイルの内容を読み込み、利用可能なすべての翻訳を含むJSファイルを生成します。
  • POエディタ:POファイルを扱うのに役立つアプリケーションは複数あります。良い選択肢はPoeditで、MacOS、GNU/Linux、Windowsで利用できます。

翻訳用ページの準備

4つのファイルタイプがあります:

  • 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__() 名前空間付きコンテンツを翻訳の対象としてマークします (二重アンダースコア括弧)。
  • 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

これは、~/locale ファイルからこれらの関数をインポートしなくても、Vue テンプレートで文字列を内部化できることを意味します:

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

Frontend Jest テストでは、期待値は外部化メソッドを参照する必要はありません。外部化は Frontend のテスト環境でモックされるので、 期待値はロケール間で決定論的になります(関連する MR を参照ください)。

使用例:

// Bad. Not necessary in Frontend environment.
expect(findText()).toBe(__('Lorem ipsum dolor sit'));
// Good.
expect(findText()).toBe('Lorem ipsum dolor sit');

推奨

文字列がコンポーネント全体で再利用される場合、これらの文字列を変数として定義すると便利です。コンポーネントの$options オブジェクトにi18n プロパティを定義することをお勧めします。コンポーネント内に多用文字列と単一使用文字列が混在している場合は、この方法を使用して、外部化された文字列のローカルな単一真理源(Single Source of Truth)を作成することを検討してください。

<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.js ファイルに追加して、コンポーネント間でインポートしたくなります。しかし、この方法には複数の落とし穴があります:

  • HTMLテンプレートとコピーの間に距離ができてしまい、コードベースをナビゲートする際に、さらに複雑なレベルを追加することになります。
  • コピー文字列が本当に同じ実体であることはほとんどありません。再利用可能な変数があることの利点は、値を更新するために簡単に移動できる場所が1つあることですが、コピーの場合、まったく同じではない類似した文字列があることはよくあることです。

コピー文字列をエクスポートする際に避けるべきもう1つの方法は、specでインポートすることです。これは、より効率的なテストのように見えるかもしれませんが(コピーを変更してもテストはパスします!)、さらなる問題を引き起こします:

  • インポートした値がundefined である可能性があり、テストで偽陽性が出るかもしれません(i18n オブジェクトをインポートした場合はなおさらです。プリミティブとしての Exporant を参照してください)。
  • 何をテストしているのか(どのコピーを期待しているのか)を知ることが難しくなります。
  • アサーションを書き直すのではなく、定数の値が正しいと仮定するため、タイプミスを見逃すリスクが高くなります。
  • この方法の利点はわずかです。コンポーネントのコピーを更新し、仕様を更新しないことは、潜在的なイシューを上回るほど大きなメリットではありません。

例として

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

// Bad. What is the actual text for `MSG_ALERT_SETTINGS_FORM_ERROR`? If `wrapper.text()` returns undefined, the test may still pass with the wrong values!
expect(wrapper.text()).toBe(MSG_ALERT_SETTINGS_FORM_ERROR);
// Very bad. Same problem as above and we are going through the vm property!
expect(wrapper.text()).toBe(MyComponent.vm.i18n.buttonLabel);
// Good. What we are expecting is very clear and there can be no surprises.
expect(wrapper.text()).toBe('There was an error: Please refresh and hope for the best!');

動的翻訳

詳細については、私たちが翻訳をダイナミックに保つ方法を参照してください.

特殊なコンテンツでの作業

補間

翻訳されたテキストのプレースホルダーは、それぞれのソースファイルのコードスタイルに合わせる必要があります。例えば、Rubyでは%{created_at} 、JavaScriptでは%{createdAt}リンクを追加するときは、文章を分割しないようにしてください。

  • Ruby/HAMLの場合:

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

    GlSprintf コンポーネントを使用します:

    • 翻訳文字列に子コンポーネントを含める場合。
    • 翻訳文字列にHTMLを含めています。
    • sprintf を使っていて、プレースホルダの値がエスケープされないようにfalse を第3引数として渡しています。

    使用例:

     <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() {
           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 を第3引数として渡すことで、プレースホルダの値をエスケープしないようにします。補間された動的な値は、インスタンスンスンスlodashからescape を使用して、自分でエスケープする必要があります。

     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>'
    

複数形

  • 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'
    
  • Vueでは

    Vueファイルで翻訳された文字列を整理する推奨される方法の1つは、constants.js ファイルに展開することです。文字列が複数形になっている場合、count 定数ファイル内で変数が countわからないため、これを行うのは困難です。count これを解決 countするには、引数をcount 取る関数を作成することをお勧め countします:

     // .../feature/constants.js
     import { n__ } from '~/locale';
       
     export const I18N = {
       // Strings that are only singular don't need to be a function
       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>
           A singular string:
           {{ $options.i18n.someDaysRemain }}
         </span>
         <span>
           A plural string:
           {{ $options.i18n.daysRemaining(days) }}
         </span>
       </div>
     </template>
    

n_n__ メソッドは、同じ文字列の複数形の翻訳を取得するためにのみ使用されるべきで、異なる文字列を異なる量に対して表示するロジックを制御するために使用すべきではありません。同じような文字列の場合、翻訳時に最も文脈を提供するために文全体を複数形にします。言語によっては、対象となる複数形の数量が異なるものがあります。たとえば、中国語(簡体字)の場合、私たちの翻訳ツールではターゲット複数形は1つしかありません。つまり、翻訳者は文字列のうちの1つだけを翻訳するように選択しなければならず、他のケースでは翻訳が意図したとおりに動作しません。

以下に例を示します:

例 1:異なる文字列の場合

これを使います:

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

この代わりに

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

例 2:類似文字列の場合

これを使います:

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

この代わりに

# incorrect usage example
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 vsEpic|Promote.
  • 翻訳者が任意の文字列ではなく、同じ製品分野に属する外部化された文字列の翻訳に集中できるようにします。
  • 翻訳者を支援する言語的背景を提供します。

名前空間が意味をなさない場合もあります。例えば、”Cancel “のようなどこにでもあるUIの単語やフレーズや、”Save changes “のようなフレーズでは、名前空間は逆効果になる可能性があります。

名前空間は PascalCase であるべきです。

  • Ruby/HAMLの場合:

     s_('OpenedNDaysAgo|Opened')
    

    翻訳が見つからない場合はOpened が返されます。

  • JavaScript では

     s__('OpenedNDaysAgo|Opened')
    

名前空間は翻訳から削除してください。詳細は翻訳ガイドラインを参照してください。

HTML

翻訳のために提出される文字列には、HTMLを直接含めなくなりました。これは

  1. 翻訳された文字列が誤って無効なHTMLを含んでしまう可能性があるからです。
  2. 翻訳された文字列は、Open Web Application Security Project(OWASP) が指摘しているように、XSS の攻撃ベクトルになる可能性があります。

翻訳された文字列に書式を含めるには、次のようにします:

  • 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: [] }) };
       
     // ... using the string
     element.innerHTML = i18n.LESS_THAN_ONE_HOUR;
       
     // => 'In < 1 hour'
    
  • Vueでは

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

数字

異なるロケールでは、異なる数値フォーマットを使用することがあります。数字のローカライズをサポートするために、私たちはtoLocaleString()を活用したformatNumber を使用しています。

デフォルトでは、formatNumber は現在のユーザー・ロケールを使用して数字を文字列としてフォーマットします。

  • JavaScript では
import { formatNumber } from '~/locale';

// Assuming "User Preferences > Language" is set to "English":

const tenThousand = formatNumber(10000); // "10,000" (uses comma as decimal symbol in English locale)
const fiftyPercent = formatNumber(0.5, { style: 'percent' }) // "50%" (other options are passed to 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 では、日付や時刻に書式を追加する方法は 2 つあります:

    • ** l ヘルパーを使う方法**: たとえば、l(active_session.created_at, format: :short) 。Ruby/HAMLには、日付と 時刻のための定義済みの書式がいくつかあります。新しい書式を追加する必要がある場合は、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 Developer ドキュメントに記載されているベストプラクティスに従うようにしてください。

翻訳ヘルパーには常に文字列リテラルを渡してください。

bin/rake gettext:regenerate スクリプトはコードベースを解析し、翻訳ヘルパーから翻訳可能な文字列をすべて取り出します。

文字列が変数もしくは関数呼び出しとして渡された場合、スクリプトは文字列を解決することができません。そのため、ヘルパーには常に文字列リテラルを渡すようにしてください。

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

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

新しいコンテンツでのPOファイルの更新

新しいコンテンツが翻訳用にマークされたので、このコマンドを実行してlocale/gitlab.pot ファイルを更新します:

bin/rake gettext:regenerate

このコマンドはlocale/gitlab.pot ファイルを新しく内部化された文字列で更新し、使われていない文字列を削除します。変更がデフォルトブランチに反映されると、Crowdinがそれを拾って翻訳用に表示します。

locale/[language]/gitlab.po ファイルへの変更をチェックインする必要はありません。Crowdin からの翻訳がマージされると、自動的に更新されます。

gitlab.pot ファイルにマージ競合がある場合、そのファイルを削除し、同じコマンドで再生成することができます。

PO ファイルの検証

翻訳ファイルを常に最新の状態に保つために、static-analysis ジョブの一部として CI 上で実行される linter があります。POファイルの調整を内部でリントするには、rake gettext:lint.

リンターは以下を考慮します:

  • 有効なPOファイル構文。
  • 変数の使用法。
    • 言語によって変数の順序が変わる可能性があるため、無名 (%d) 変数は 1 つだけです。
    • メッセージ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 delete %{project_name_with_namespace}.\\n", "Deleted projects 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 に構文エラーがあります。この出力では、1 pipeline に構文エラーがあります。locale/zh_TW/gitlab.po のファイルには、 のメッセージにはない変数があります。

新しい言語の追加

新しい言語は、文字列の少なくとも10%が翻訳され、承認された後にユーザー設定のオプションとして追加してください。より多くの文字列が翻訳されたとしても、GitLab UIでは承認された翻訳だけが表示されます。

note
GitLab 13.3で導入されました:翻訳数が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ファイルを生成します。