GitLab의 국제화

국제화(i18n) 작업을 위해 GNU gettext가 사용됩니다. 이 작업을 위한 가장 많이 사용되는 도구이며, 우리가 작업하는 데 도움이 되는 많은 애플리케이션이 있습니다.

참고: 이 페이지에서 설명하는 모든 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은 모델, 뷰, 컨트롤러에서 콘텐츠를 번역할 수 있도록 합니다. 이 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") }

이것은 클래스 로딩 시 번역되며 에러 메시지가 항상 기본로케일로 되어 있게 됩니다. 액티브 레코드의 :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 템플릿에서 문자열을 외부화(externalize)할 수 있어서 ~/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 객체를 가져 오는 경우 더욱 그렇습니다, 원시값으로 상수 내보내기 참조).
  • 무엇을 테스트하고 있는지를 알기가 힘들며 (어떤 복사를 예상하는지).
  • 오타를 놓치기 쉬우므로 상수의 값이 올바른지 계속 기대하지 않고 있다는 가정을 하게 됩니다.
  • 이 접근 방식의 이점은 덜 중요합니다. 컴포넌트에서 복사를 업데이트하고 스펙을 업데이트하지 않으면, 이는 잠재적인 문제보다 크게 이익을 가져다주지 않습니다.

예시:

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에서:

    • 번역 문자열에 하위 구성 요소를 포함하는 경우
    • 번역 문자열에 HTML을 포함하는 경우
    • sprintf를 사용하고 세 번째 인수로 false를 전달하여 이스케이프를 방지하는 경우

    위 경우에는 GlSprintf 컴포넌트를 사용합니다.

    예시:

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

    번역 내부에 마크업을 사용해야 하는 경우, sprintf를 사용하고 세 번째 인수로 false를 전달하여 이스케이프를 방지해야 합니다. 보간된 동적 값은 반드시 스스로 이스케이프해야 합니다.

    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 파일로 추출하는 것입니다. 이것은 복수화된 문자열이 있는 경우 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|번역된 문자열'

네임스페이스:

  • 단어의 모호성을 해소합니다. 예: Promotions|PromoteEpic|Promote.
  • 임의의 것이 아닌 동일한 제품 영역에 속하는 외부화된 문자열을 번역하는 데 번역자가 집중할 수 있게 합니다.
  • 번역자가 도와주는 언어적 맥락을 제공합니다.

일부 경우에는 네임스페이스가 적절하지 않을 수 있습니다. 예를 들어 “취소”와 같은 모든 UI 단어 및 구문에는 네임스페이스가 역효과를 낼 수 있습니다.

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

  • Ruby/HAML:

    s_('OpenedNDaysAgo|Opened')
    

    번역이 찾을 수 없는 경우 Opened가 반환됩니다.

  • JavaScript:

    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를 사용합니다.

  • 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에 정의된 형식이 필요하지 않거나 하나의 형식이 매우 특별하여 새로 추가할 필요가 없는 경우에 사용합니다(예: 단일 뷰에서만 사용될 경우).

최상의 실천 방법

번역 업데이트 최소화

업데이트는 해당 문자열의 번역이 손실될 수 있습니다. 위험을 최소화하려면 문자열을 변경하지 마십시오.

  • 사용자에게 가치를 추가합니다.
  • 번역을 위한 추가 컨텍스트를 포함합니다.

예를 들어, 다음과 같은 변경을 피하세요:

- _('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의 경우(When Vue cannot be used), 다음과 같이 대신:

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

새 콘텐츠로 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
    이중 인용부터 라인 끝까지 공백만 있어야 함.
    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
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인 메시지에 없는 변수가 있는 번역이 있습니다.

새 언어 추가

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

참고: 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 "Value Stream Analytics 페이지에 대한 프랑스어 번역 추가"
    

UI에서 번역을 수동으로 테스트하십시오

Vue 번역을 수동으로 테스트하려면:

  1. GitLab 로캘을 영어가 아닌 다른 언어로 변경하십시오.
  2. bin/rake gettext:compile을 사용하여 JSON 파일을 생성하십시오.