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

이는 Vue 템플릿에서 문자열을 외부화할 수 있다는 것을 의미합니다. 이를 통해 ~/locale 파일에서 이러한 함수를 가져올 필요가 없습니다:

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

Vue 컴포넌트의 JavaScript에서 문자열을 번역해야 하는 경우, JavaScript 파일 섹션에서 설명한대로 필요한 외부화 함수를 가져올 수 있습니다.

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

권장 사항

컴포넌트 전체에서 문자열을 재사용하는 경우, 이러한 문자열을 변수로 정의하는 것이 유용할 수 있습니다. 우리는 컴포넌트의 $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.js 파일에 추가하고 컴포넌트 전체에서 가져오는 것이 유혹적일 수 있습니다. 그러나 이 접근 방식에는 여러 가지 함정이 있습니다:

  • HTML 템플릿과 복제 사이에 거리를 만들어 코드베이스를 이동하는 동안 추가 수준의 복잡성이 생깁니다.
  • 재사용 가능한 변수를 가지는 이점은 한 곳에서만 값을 업데이트하면 되는 간편함인데, 복사의 경우 약간 다른 문자열이 있는 것이 일반적이므로 이를 업데이트하는 것이 큰 이점이 되지 못합니다.

복사 문자열을 내보낼 때 피해야 할 다른 실천은 사양에서 이를 가져오는 것입니다. 복사를 변경하더라도 여전히 테스트가 효율적인 것처럼 보일 수 있지만, 이로 인해 추가적인 문제가 발생합니다:

  • 가져온 값이 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() {
          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를 사용하고 세 번째 인수로 플레이스홀더 값을 이스케이프하지 않도록 전달하세요. 자신이 보간된 동적 값을 이스케이프해야 합니다. 예를 들어 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>'
    

    Note: This translation has multiple code blocks and complex structures. It’s important to maintain the markdown formatting as close to the original as possible for code readability.

복수형

  • 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|Translated string'

네임스페이스는 다음과 같은 경우에 유용합니다.

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

어떤 경우에는 네임스페이스가 적절하지 않을 수도 있습니다. 예를 들어, “Cancel”과 같은 보편적인 UI 단어 및 구문과 “Save changes”와 같은 구문에 대해서는 네임스페이스가 역효과를 낼 수 있습니다.

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

  • Ruby/HAML:

    s_('OpenedNDaysAgo|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;)를 사용하십시오:

  • 루비/HAML:

     safe_format(_('In &lt; 1 hour'))
       
     # => 'In < 1 hour'
    
  • 자바스크립트:

    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()을 활용합니다.

  • 자바스크립트:
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>

날짜/시간

  • 자바스크립트:
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을 활용합니다.

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

    • l 헬퍼 사용: 예를 들어, l(active_session.created_at, format: :short). dates에 대한 몇 가지 사전 정의된 형식과 times이 있습니다. 새로운 형식을 추가해야 하는 경우, 다른 부분에서 이 형식이 유용할 수 있으므로 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

가끔 파서가 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 }) }}

링크를 추가할 때 문장을 분할하지 마세요

번역된 문장 사이에 링크를 사용할 때도 마찬가지입니다. 그렇지 않으면 특정 언어로는 번역할 수 없습니다.

  • 루비/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>
    
  • 자바스크립트의 경우 (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.po 파일에 대한 변경 사항을 확인할 필요는 없습니다. 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.po1 pipeline 메시지 ID에 없는 변수를 사용하고 있습니다.

새로운 언어 추가

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

UI에서 번역 매뉴얼 테스트

Vue 번역을 매뉴얼으로 테스트하려면 다음을 수행합니다:

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