- 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|Promote
vsEpic|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 파일을 생성합니다.