GitLab의 국제화

국제화(i18n) 작업을 위해서 GNU gettext가 사용됩니다. 가장 많이 사용되는 도구이며 이 작업에 도움을 주는 여러 애플리케이션이 있기 때문입니다.

caution
이 페이지에서 설명하는 모든 rake 명령은 GitLab 인스턴스에서 실행해야 합니다. 이 인스턴스는 보통 GitLab 개발 키트(GDK)입니다.

GitLab 개발 키트(GDK) 설정

GitLab Community Edition 프로젝트에서 작업하려면 GDK를 통해 다운로드하고 구성해야 합니다.

GitLab 프로젝트를 준비한 후 번역 작업을 시작할 수 있습니다.

도구

다음 도구들이 사용됩니다:

  • 번역 작업을 위한 일상적인 개발 작업을 지원하는 사용자 정의된 도구들:

    • tooling/bin/gettext_extractor locale/gitlab.pot: 새로운 내용을 번역하기 위해 모든 소스 파일을 스캔합니다.
    • rake gettext:compile: PO 파일의 내용을 읽어 Frontend를 위한 모든 가능한 번역이 포함된 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

즉, ~/locale 파일에서 이러한 함수들을 가져와야 할 필요가 없이 Vue 템플릿에서 문자열을 외부화할 수 있습니다:

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

Vue 컴포넌트 자바스크립트에서 문자열을 번역해야 하는 경우에는 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 참조).

예:

// 안 좋음. Frontend 환경에서는 필요하지 않습니다.
expect(findText()).toBe(__('Lorem ipsum dolor sit'));
// 좋음.
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 템플릿과 복사 간에 거리가 생기며, 코드베이스를 탐색하는 동안 추가 복잡성 수준이 증가합니다.
  • 재사용 가능한 변수를 갖는 이점은 값 업데이트를 위해 하나의 쉬운 장소를 갖는 것인데, 복사에서는 관련이 없는 문자열이 비슷한 경우가 많기 때문에 이러한 이점은 꽤 흔합니다.

또한 복사 문자열을 내보내는 것을 테스트에 가져오는 것은 회피해야 할 다른 실천입니다. 복사를 변경하면 테스트가 여전히 통과할 것이라는 것처럼 보일지 모르지만 여러 가지 문제를 야기할 수 있습니다:

  • 가져온 값이 undefined일 수 있으며 잘못된 값에 대해 거짓 긍정적인 결과를 얻을 수 있습니다(특히 i18n 객체를 가져오는 경우에는 더 그렇습니다. export constants as primitives 참조).
  • 우리가 무엇을 테스트하고 있는지 파악하기가 어려울 수 있습니다(어떤 복사를 기대하는지).
  • 오타가 더 많이 놓치게 되며 우리는 정확한 문장을 다시 작성하는 대신 상수값이 올바른지를 가정합니다.
  • 이러한 접근 방식의 이점은 작은 편입니다. 컴포넌트의 복사를 업데이트하고 스펙을 업데이트하지 않는 것은 잠재적인 문제를 뛰어넘는 충분한 이점이 아닙니다.

예시:

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 파일이 오래되어 pre-push 검사 및 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>
    

    다른 경우에는, 계산된 속성을 사용하는 것이 더 간단할 수 있습니다. 예를 들어:

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

    번역 내에서 마크업을 사용해야 하는 경우, 세 번째 인수로 false를 전달하여 sprintf를 사용하고 플레이스홀더 값을 이스케이프하지 않도록 해야 합니다. 예를 들어, 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 })

네임스페이스

네임스페이스는 함께 속하는 번역을 그룹화하는 방법입니다. 번역자가 Prefix| 뒤에 오는 접두사를 추가함으로써 번역에 대한 맥락을 제공합니다. 예를 들면:

'Namespace|Translated string'

네임스페이스:

  • 단어의 모호함을 해소합니다. 예를 들어: Promotions|Promote vs Epic|Promote.
  • 임의의 단어보다는 제품 영역에 속한 외부화된 문자열의 번역에 초점을 맞추게 합니다.
  • 번역자가 언어적 맥락을 제공하여 번역에 도움이 됩니다.

일부 경우에는 네임스페이스가 적합하지 않습니다. 예를 들어 “취소”나 “변경 사항 저장”과 같은 보편적인 UI 단어나 구절의 경우에는 네임스페이스가 도움이 되지 않을 수 있습니다.

네임스페이스는 PascalCase여야 합니다.

  • Ruby/HAML:

    s_('OpenedNDaysAgo|Opened')
    

    번역에서 네임스페이스는 제거되어야 합니다. 자세한 내용은 번역 가이드라인을 참조하세요.

HTML

문자열에 직접 HTML을 포함하지 않습니다. 이는 다음과 같은 이유로입니다:

  1. 번역된 문자열에 유효하지 않은 HTML이 포함될 수 있습니다.
  2. 번역된 문자열은 OWASP(Open Web Application Security Project)에서 언급한 바와 같이 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: [] }) };
      
    // ... 문자열 사용
    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'))) // 2063년 4월 5일

이는 Intl.DateTimeFormat을 사용합니다.

  • Ruby/HAML에서, 날짜와 시간에 형식을 추가하는 두 가지 방법이 있습니다:

    • l 헬퍼 사용: 예를 들어, l(active_session.created_at, format: :short). 날짜, 시간에 대해 몇 가지 사전 정의된 형식이 있습니다. 다른 부분에서 이를 활용할 수 있으면 새 형식을 추가해야 하는 경우, 이를 파일 en.yml에 추가합니다.
    • strftime 사용: 예를 들어, milestone.start_date.strftime('%b %-d'). 우리는 필요한 날짜/시간 사양과 en.yml에 정의된 형식 중 일치하는 것이 없는 경우 또는 단일 뷰에서만 사용되는 등 매우 특수한 경우에는 새 형식을 추가할 필요 없이 strftime을 사용합니다.

Best practices

번역 업데이트 최소화

업데이트는 해당 문자열의 번역이 손실될 수 있습니다. 위험을 최소화하기 위해 다음과 같은 경우를 제외하고 문자열의 변경을 피하십시오:

  • 사용자에게 가치를 제공하는 경우.
  • 번역자에게 추가 컨텍스트를 제공하는 경우.

예를 들어, 다음과 같은 변경을 피하십시오:

- _('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 documentation에서 설명한 최상의 방법을 따르도록 노력하세요.

항상 번역 도우미에 문자열 리터럴 전달하기

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

새 콘텐츠로 PO 파일 업데이트하기

이제 새 콘텐츠가 번역을 위해 표시됐으므로 다음 명령을 실행하여 locale/gitlab.pot 파일을 업데이트하세요:

tooling/bin/gettext_extractor locale/gitlab.pot

이 명령은 locale/gitlab.pot 파일을 신규 외부화된 문자열로 업데이트하고 사용되지 않는 문자열을 제거합니다. 변경 사항이 기본 브랜치에 있으면 Crowdin에서 변경 사항을 가져가 번역합니다.

locale/[language]/gitlab.pot 파일에 별도의 변경 사항을 체크인할 필요가 없습니다. Crowdin의 번역이 Merge되면 파일이 자동으로 업데이트됩니다.

gitlab.pot 파일에 Merge 충돌이 있는 경우 동일한 명령을 사용하여 파일을 삭제하고 다시 생성할 수 있습니다.

PO 파일 유효성 검사

번역 파일을 최신 상태로 유지하기 위해 static-analysis 작업의 일환으로 CI에서 실행되는 린터가 있습니다. 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 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에는 구문 오류가 있습니다. 파일 locale/zh_TW/gitlab.po에는 번역에는 없는 변수가 1 pipeline 메시지 ID에 사용됩니다.

새 언어 추가

새 언어는 적어도 10% 이상의 문자열이 번역되고 승인된 경우에만 사용자 기본 설정의 옵션으로 추가되어야 합니다. 번역된 문자열의 수가 많더라도 승인된 번역만 GitLab UI에 표시됩니다.

note
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/ 경로에 생성됩니다. 이제 locale/fr/gitlab.edit.po에 위치한 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 파일을 생성합니다.