- GitLab 개발 키트(GDK) 설정
 - 도구
 - 페이지 번역 준비
 - 번역된 문자열 수정하기
 - 특별 콘텐츠 작업
 - Best practices
 - 새 콘텐츠로 PO 파일 업데이트하기
 - 새로운 언어 추가
 - UI에서 번역 매뉴얼 테스트
 
GitLab의 국제화
- GitLab 9.2에서 소개되었습니다.
 
국제화(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을 사용하여 모델, 뷰, 컨트롤러에서 내용을 번역할 수 있습니다. 이는 내부적으로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를 사용하고 세 번째 인수로 플레이스홀더 값을 이스케이프하지 않도록 전달하세요. 자신이 보간된 동적 값을 이스케이프해야 합니다. 예를 들어lodash의escape를 사용하세요.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 <strong><script>alert('evil')</script></strong>' // 괜찮음: sprintf(__('This is %{value}'), { value: `<strong>${escape(someDynamicValue)}</strong>` }, false); // => 'This is <strong><script>alert('evil')</script></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|PromotevsEpic|Promote. - 번역자가 임의의 것이 아닌 동일한 제품 영역에 속하는 외부화된 문자열을 번역하는 데 집중하도록 합니다.
 - 번역자가 돕는 언어적 문맥을 제공합니다.
 
어떤 경우에는 네임스페이스가 적절하지 않을 수도 있습니다. 예를 들어, “Cancel”과 같은 보편적인 UI 단어 및 구문과 “Save changes”와 같은 구문에 대해서는 네임스페이스가 역효과를 낼 수 있습니다.
네임스페이스는 PascalCase여야 합니다.
- 
Ruby/HAML:
s_('OpenedNDaysAgo|Opened')번역에서 네임스페이스는 제거되어야 합니다. 자세한 내용은 번역 가이드라인을 참조하세요.
 - 
JavaScript:
s__('OpenedNDaysAgo|Opened') 
번역에서 네임스페이스는 제거되어야 합니다. 자세한 내용은 번역 가이드라인을 참조하세요.
HTML
이제 더 이상 번역에 제출되는 문자열에 직접 HTML을 포함하지 않습니다. 이는 다음과 같은 이유로입니다.
- 번역된 문자열에 유효하지 않은 HTML이 포함될 우려가 있습니다.
 - 번역된 문자열이 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 엔터티 코드(< 또는 >)를 사용하십시오:
- 
루비/HAML:
safe_format(_('In < 1 hour')) # => 'In < 1 hour' - 
자바스크립트:
import { sanitize } from '~/lib/dompurify'; const i18n = { LESS_THAN_ONE_HOUR: sanitize(__('In < 1 hour'), { ALLOWED_TAGS: [] }) }; // ... 문자열 이용 element.innerHTML = i18n.LESS_THAN_ONE_HOUR; // => 'In < 1 hour' - 
Vue:
<gl-sprintf :message="s__('In < 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.po는 1 pipeline 메시지 ID에 없는 변수를 사용하고 있습니다.
새로운 언어 추가
최소 10% 이상의 문자열이 번역되고 승인된 경우에만 새 언어를 사용자 기본 설정의 옵션으로 추가해야 합니다. 문자열의 수가 더 많을 수 있지만, 승인된 번역만이 GitLab UI에 표시됩니다.
새 언어(예: 프랑스어)의 번역을 추가하려면 다음을 수행합니다:
- 
lib/gitlab/i18n.rb에 새 언어를 등록합니다:... AVAILABLE_LANGUAGES = { ..., 'fr' => 'Français' }.freeze ... - 
다음 명령어를 사용하여 언어를 추가합니다:
bin/rake gettext:add_language[fr]특정 지역을 위해 언어를 추가하려면 비슷한 명령어를 사용합니다. 지역은 밑줄(
_)로 구분하고 대문자로 지정해야 합니다. 예를 들어:bin/rake gettext:add_language[en_GB] - 
언어를 추가하면 새 디렉터리가
locale/fr/경로에 생성됩니다. 이제 PO 편집기를 사용하여locale/fr/gitlab.edit.po에 있는 PO 파일을 편집할 수 있습니다. - 
번역을 업데이트한 후 PO 파일을 처리하여 이진 MO 파일을 생성하고 번역이 포함된 JSON 파일을 업데이트해야 합니다:
bin/rake gettext:compile - 
번역된 내용을 보려면 기본 언어를 변경해야 합니다. 이 기능은 사용자의 설정(
/profile)에서 찾을 수 있습니다. - 
변경 사항을 확인한 후 새 파일을 커밋해야 합니다. 예:
git add locale/fr/ app/assets/javascripts/locale/fr/ git commit -m "Value Stream Analytics 페이지에 대한 프랑스어 번역 추가" 
UI에서 번역 매뉴얼 테스트
Vue 번역을 매뉴얼으로 테스트하려면 다음을 수행합니다:
- GitLab 로컬라이제이션을 영어가 아닌 다른 언어로 변경합니다.
 - 
bin/rake gettext:compile을 사용하여 JSON 파일을 생성합니다. 
도움말