GitLab의 국제화

국제화(i18n) 작업을 위해 GNU gettext가 사용됩니다. 이 작업에 대한 가장 많이 사용되는 도구인데 많은 응용 프로그램이 이 도구로 작업하는 데 도움을 줍니다.

참고: 이 페이지에서 설명하는 모든 rake 명령은 GitLab 인스턴스에서 실행해야 합니다. 이 인스턴스는 일반적으로 GitLab 개발 키트 (GDK)입니다.

GitLab 개발 키트 (GDK) 설정

GitLab 커뮤니티 에디션 프로젝트에서 작업하려면 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 믹스인을 사용하여 다음 함수들을 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)

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일 수 있으며 테스트에서 false-positive를 얻을 수 있습니다 (특히 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 파일이 오래되어 있으면 사전 푸시 검사와 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() {
          sprintf(__('Hello %{username}'), { username: this.user.name });
        }
      }
      ...
    }
    </script>
    
    <template>
      <span>{{ userWelcome }}</span>
    </template>
    
  • JavaScript(적용할 수 없는 경우):

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

    번역 내에서 마크업을 사용해야 하는 경우, 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 또는 count 변수를 사용하지 마십시오. 이렇게 하면 일부 언어에서 자연스러운 번역이 가능해집니다.

  • 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 파일로 추출하는 것입니다. 계수 변수가 상수 파일 내에서 알 수 없기 때문에 이를 수행하는 것은 어려울 수 있습니다. 이를 극복하기 위해 계수 매개변수를 포함하는 함수를 만드는 것을 권장합니다:

    // .../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 })

네임스페이스

네임스페이스는 함께 속하는 번역을 그룹화하는 방법입니다. 번역자에게 제품 영역에 속하는 외부화된 문자열을 번역하는 데 집중할 수 있도록 제공되는 문맥을 제공합니다. 네임스페이스에는 바 (|) 다음에 접두사를 추가하여 모호함을 해결합니다. 예를 들어:

'네임스페이스|번역된 문자열'

네임스페이스는 한국어 번역에서도 명확한 번역을 위해 추가되어야 하는 경우가 있습니다. 예를 들어, ‘취소’는 사용 방식에 따라 다양한 방법으로 번역될 수 있습니다.

네임스페이스는 파스칼 케이스여야 합니다.

  • Ruby/HAML에서:

    s_('OpenedNDaysAgo|Opened')
    

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

  • JavaScript에서:

    s__('OpenedNDaysAgo|Opened')
    

HTML

더 이상 번역에 직접 HTML을 포함하지 않습니다. 이는 다음과 같은 이유로입니다:

  1. 번역된 문자열에 잘못된 HTML이 포함될 수 있습니다.
  2. 번역된 문자열은 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: [] }) };
    
    // ... 문자열 사용
    element.innerHTML = i18n.LESS_THAN_ONE_HOUR;
    
    // => 'In < 1 hour'
    
  • Vue에서:

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

숫자

다양한 위치에서는 다른 숫자 형식을 사용할 수 있습니다. 숫자를 지원하기 위해 우리는 현재 사용자의 로캘을 사용하여 문자열로 숫자를 형식화하는 formatNumber를 사용합니다.

기본적으로 formatNumber는 숫자를 현재 사용자 로캘을 사용하여 문자열로 형식화합니다.

  • JavaScript에서:
import { formatNumber } from '~/locale';

// "사용자 기본 설정 > 언어"가 "English"로 설정된 경우:

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'). 우리는 strftime을 사용하여 en.yml에 정의된 형식과 일치하지 않을 때, 그리고 단일 뷰에서만 사용되는 매우 특별한 경우가 아니라면 새로운 형식으로 추가할 필요가 없는 경우에 사용합니다.

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

할 수 없는 번역이 존재하는 경우 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 문서에 기술된 모범 사례를 따르도록 노력하십시오.

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

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.po 파일에 어떤 변경 사항도 체크인할 필요가 없습니다. 이 파일들은 Crowdin의 번역이 통합될 때 자동으로 업데이트됩니다.

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

PO 파일 유효성 검사

번역 파일을 최신 상태로 유지하기 위해, ‘static-analysis’ 작업의 일부로 CI에서 실행되는 린터가 있습니다. PO 파일의 조정을 린트하기 위해 로컬에서 rake gettext:lint를 실행할 수 있습니다.

린터는 다음을 고려합니다:

  • 유효한 PO 파일 구문.
  • 변수 사용.
    • 서로 다른 언어에서 변수의 순서가 변경될 수 있으므로 단 하나의 이름 없는 (%d) 변수만 사용합니다.
    • 메시지 ID에 사용된 모든 변수는 번역에서 사용됩니다.
    • 번역에 메시지 ID에 없는 변수를 사용하지 않아야 합니다.
  • 번역 중 오류.
  • 꺽쇠 (< 또는 >)의 존재.

오류는 파일별, 메시지 ID별로 그룹화됩니다:

`locale/zh_HK/gitlab.po`의 오류:
  PO 구문 오류
    lines의 SimplePoParser::ParserErrorSyntax 오류
    msgctxt의 구문 오류
    msgid의 구문 오류
    msgstr의 구문 오류
    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 파일은 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 "Value Stream Analytics 페이지를 위한 프랑스어 번역 추가"
    

UI에서 수동으로 번역 테스트하기

Vue 번역을 수동으로 테스트하려면 다음 단계를 따르세요:

  1. GitLab 로컬라이제이션을 영어 이외의 다른 언어로 변경합니다.
  2. bin/rake gettext:compile을 사용하여 JSON 파일을 생성합니다.