- GitLab Development Kit(GDK) 설정
- 도구
- 번역을 위한 페이지 준비
- 번역된 문자열 변경하기
- 특수 콘텐츠 작업하기
- 모범 사례
- 새로운 내용으로 PO 파일 업데이트하기
- 새로운 언어 추가
- UI에서 번역 수동 테스트하기
GitLab의 국제화
국제화(i18n)를 다루기 위해,
GNU gettext가 사용되며, 이는 이 작업에 가장 많이 사용되는 도구입니다. 이를 지원하는 여러 애플리케이션이 있습니다.
rake
명령은 GitLab 인스턴스에서 실행되어야 합니다. 이 인스턴스는 일반적으로 GitLab Development Kit(GDK)입니다.GitLab Development Kit(GDK) 설정
GitLab Community Edition 프로젝트에서 작업하려면, GDK를 통해 다운로드하고 구성해야 합니다.
GitLab 프로젝트를 준비한 후, 번역 작업을 시작할 수 있습니다.
도구
다음 도구들이 사용됩니다:
-
번역과 관련된 일상적인 개발 작업을 도와주는 사용자 정의 작성 도구:
-
tooling/bin/gettext_extractor locale/gitlab.pot
: 번역할 새로운 콘텐츠를 스캔합니다. -
rake gettext:compile
: PO 파일의 내용을 읽고, 프론트엔드의 모든 사용 가능한 번역을 포함하는 JS 파일을 생성합니다. -
rake gettext:lint
: PO 파일을 검증합니다.
-
-
gettext_i18n_rails
: 이 젬은 모델, 뷰 및 컨트롤러의 내용을 번역할 수 있게 해줍니다. 내부적으로fast_gettext
를 사용합니다.또한 일상적인 작업에서는 거의 필요하지 않은 다음의 Rake 작업에 접근할 수 있습니다:
-
rake gettext:add_language[language]
: 새 언어 추가하기. -
rake gettext:find
: 번역을 위해 마크된 콘텐츠를 찾기 위해 Rails 애플리케이션의 거의 모든 파일을 파싱합니다. 이후, 이 콘텐츠로 PO 파일을 업데이트합니다. -
rake gettext:pack
: PO 파일을 처리하고 애플리케이션이 사용하는 바이너리 MO 파일을 생성합니다.
-
-
PO 편집기: PO 파일 작업에 도움을 줄 수 있는 여러 애플리케이션이 있습니다. 좋은 옵션은 macOS, GNU/Linux 및 Windows에서 사용할 수 있는 Poedit입니다.
번역을 위한 페이지 준비
네 가지 파일 유형이 있습니다:
- 루비 파일: 모델 및 컨트롤러.
- HAML 파일: 뷰 파일.
- ERB 파일: 이메일 템플릿에 사용됩니다.
- 자바스크립트 파일: 주로 Vue 템플릿에서 작업합니다.
루비 파일
원시 문자열로 작업하는 메서드나 변수가 있을 경우, 예를 들어:
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)
프론트엔드 Jest 테스트에서 기대치는 외부화 메서드를 참조할 필요가 없습니다. 프론트엔드 테스트 환경에서는 외부화가 목(mock) 처리되므로, 기대치는 로케일에 따라 결정론적입니다 (관련 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
를 사용하고 세 번째 인수로false
를 전달하여 플레이스홀더 값이 이스케이프되지 않도록 해야 합니다.동적 값을 이스케이프해야 하며, 예를 들어
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>'
복수형
-
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 텍스트에 네임스페이스를 추가하세요.
네임스페이스는 PascalCase여야 합니다.
-
Ruby/HAML에서는:
s_('OpenedNDaysAgo|Opened')
번역이 발견되지 않으면
Opened
가 반환됩니다. -
JavaScript에서는:
s__('OpenedNDaysAgo|Opened')
네임스페이스는 번역에서 제거되어야 합니다. 자세한 내용은 번역 가이드라인을 참조하세요.
HTML
이제 더 이상 번역을 제출할 때 문자열에 HTML을 직접 포함하지 않습니다. 그 이유는 다음과 같습니다:
-
번역된 문자열에 유효하지 않은 HTML이 포함될 수 있습니다.
-
번역된 문자열이 XSS에 대한 공격 벡터가 될 수 있습니다. 이는 Open Web Application Security Project (OWASP)에 의해 언급되었습니다.
번역된 문자열에 서식을 포함하려면 다음을 수행할 수 있습니다:
-
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 엔티티 코드(<
또는 >
)를 사용하세요:
-
Ruby/HAML에서는:
safe_format(_('In < 1 hour')) # => 'In < 1 hour'
-
JavaScript에서는:
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()
을 활용합니다.
기본적으로 formatNumber
는 현재 사용자 로케일을 사용하여 숫자를 문자열로 형식화합니다.
- 자바스크립트에서:
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
을 사용합니다.
-
Ruby/HAML에서는 날짜와 시간의 형식을 추가하는 두 가지 방법이 있습니다:
-
l
헬퍼 사용: 예를 들어,l(active_session.created_at, format: :short)
입니다. 우리는 날짜와 시간에 대한 몇 가지 미리 정의된 형식이 있습니다. 새로운 형식을 추가해야 하는 경우, 다른 코드 부분에서 유용할 수 있으므로en.yml
파일에 추가하세요. -
strftime
사용: 예를 들어,milestone.start_date.strftime('%b %-d')
입니다. 우리는en.yml
에서 정의된 형식이 우리의 날짜/시간 사양과 일치하지 않을 경우strftime
을 사용하며, 매우 특정하여 새로운 형식으로 추가할 필요가 없을 때 사용합니다(예: 단일 보기에서만 사용).
-
모범 사례
번역 업데이트 최소화
업데이트는 이 문자열의 번역 손실을 초래할 수 있습니다. 위험을 최소화하기 위해 문자열에 대한 변경을 피하세요 단, 다음과 같은 경우는 제외합니다:
- 사용자에게 가치를 추가합니다.
- 번역가를 위한 추가 컨텍스트를 포함합니다.
예를 들어, 다음과 같은 변경은 피하세요:
- _('사물의 수: %{count}') % { count: 10 }
+ n_('사물의 수: %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에서(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 개발자 문서에 설명된 모범 사례를 따르도록 하세요.
항상 문자열 리터럴을 번역 도우미에 전달하십시오
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 오류가 발생했습니다.
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
파일은
메시지 ID 1 pipeline
에 없는 변수를 번역에 포함하고 있습니다.
새로운 언어 추가
새로운 언어는 최소 10%의 문자열이 번역 및 승인된 경우에만 사용자 설정의 옵션으로 추가되어야 합니다. 더 많은 문자열이 번역되었더라도, 승인된 번역만 GitLab UI에 표시됩니다.
참고:
번역이 2% 미만인 언어는 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 파일을 편집하기 위해 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 파일을 생성합니다.