GitLab 국제화(i18n)
GitLab v19.1국제화(i18n) 작업에는 GNU gettext를 사용합니다. 이 페이지에 설명된 모든 rake 명령어는 GitLab 인스턴스에서 실행해야 합니다. GitLab Community Edition 프로젝트에서 작업하려면 GDK를 통해 다운로드하고 설정해야 합니다.
국제화(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: 이 gem은 모델, 뷰, 컨트롤러에서 콘텐츠를 번역할 수 있게 해줍니다. 내부적으로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이 좋은 선택입니다.
번역을 위한 페이지 준비#
다음에 제공되는 헬퍼를 사용하여 문자열을 번역 가능으로 표시해야 합니다. 문자열은 사용 맥락이 명확하지 않을 수 있는 도구에서 번역된다는 점을 유념하세요. 번역자에게 더 많은 맥락을 제공하기 위해 도메인별 문자열에 네임스페이스를 사용하는 것을 고려하세요.
파일 유형은 네 가지가 있습니다:
-
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)의 메시지는 외부화할 필요가 없습니다.
Rails 모델 오류 메시지#
Rails 모델에서 오류 메시지를 추가할 때, 오류 메시지가 완전한 문장인 경우 특정 속성에 오류를 추가하지 않는 것이 좋습니다. Rails는 속성 이름을 자동으로 사람이 읽기 좋은 형태로 변환하고 오류 메시지 앞에 추가하는데, 이로 인해 번역하기 어려운 분리된 문장이 만들어집니다.
예를 들어, 다음을 피하세요:
# Bad: Rails prepends "Squash option" to the message
errors.add(:squash_option, _('cannot be used with wildcard branch rules. Use an exact branch name.'))
대신, 완전한 문장으로 :base에 오류를 추가하세요:
# Good: Complete sentence that translators can work with
errors.add(:base, _('Squash option cannot be used with wildcard branch rules. Use an exact branch name.'))
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
즉, ~/locale 파일에서 이러한 함수를 가져오지 않고도 Vue 템플릿에서 문자열을 외부화할 수 있습니다:
<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 테스트에서 기대값은 외부화 메서드를 참조할 필요가 없습니다. 외부화는 프론트엔드 테스트 환경에서 모킹되어 있으므로, 기대값은 로케일에 따라 결정론적입니다(관련 MR 참고).
예시:
// Bad. Not necessary in Frontend environment.
expect(findText()).toBe(__('Lorem ipsum dolor sit'));
// Good.
expect(findText()).toBe('Lorem ipsum dolor sit');
권장 사항#
번역은 사용되는 위치와 최대한 가까운 곳에 배치하세요. 가능하면 번역을 담은 변수 대신 인라인 번역을 사용하세요.
번역의 가장 좋은 설명은 키 자체입니다. 이는 코드 가독성을 향상시키고 코드 맥락을 보존하는 인지적 부담을 줄여줍니다. 또한 번역 외에 변수를 별도로 유지할 필요가 없어 리팩토링이 쉬워집니다.
// Bad. A variable is defined far from where it is used
const TITLE = __('Organisations');
function transform() {
return TITLE;
}
// Good.
function transform() {
return __('Organisations');
}
공유 번역#
번역이 파일이나 모듈 내 여러 곳에서 사용될 수 있는 경우가 있습니다. 이 경우 번역을 공유하는 변수를 사용할 수 있지만 다음 사항을 고려해야 합니다:
-
인라인 번역이 코드 명확성이 더 높습니다. 번역을 변수에 넣는 유일한 이유로 DRY 원칙을 사용하지 마세요.
-
번역을 삽입하거나 결합할 때 주의하세요. 자세한 내용은 변수를 사용한 동적 텍스트 삽입을 참고하세요.
-
두 번역이 같은 영문 키를 공유한다고 해서 다른 언어에서도 동일한 번역을 의미하지는 않습니다. 적절한 경우 네임스페이스를 사용하는 것을 고려하세요.
특정 경우에 번역이 있는 변수를 사용하는 것이 더 나은 경우, 변수를 선언하고 배치하는 방법에 대한 다음 지침을 따르세요.
JavaScript 파일에서는 번역이 있는 상수를 선언하세요:
const ORGANISATIONS_TITLE = __('Organisations');
Vue Single-File 컴포넌트에서는 컴포넌트의 $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 또는 i18n.js 파일에 추가하고 모듈 전체에서 가져올 수 있습니다. 그러나 이는 코드베이스에 또 다른 복잡성을 추가하므로 신중하게 사용해야 합니다.
복사 문자열을 내보낼 때 피해야 할 또 다른 방법은 스펙에서 가져오는 것입니다. 더 효율적인 테스트처럼 보일 수 있지만(복사를 변경해도 테스트가 통과됩니다!), 추가적인 문제를 일으킵니다:
-
가져오는 값이
undefined일 위험이 있어 테스트에서 false-positive가 발생할 수 있습니다(i18n객체를 가져오는 경우 더욱 그렇습니다. 상수를 기본형으로 내보내기 참고). -
무엇을 테스트하는지 파악하기 어렵습니다(어떤 복사를 기대해야 하는지).
-
어서션을 다시 작성하지 않고 상수 값이 올바른 값이라고 가정하기 때문에 오타가 놓칠 위험이 더 높습니다.
-
이 접근 방식의 이점은 미미합니다. 컴포넌트의 복사를 업데이트하고 스펙을 업데이트하지 않는 것은 잠재적인 문제를 능가하기에 충분히 큰 이점이 아닙니다.
예시:
import { MSG_ALERT_SETTINGS_FORM_ERROR } from 'path/to/constants.js';
// Bad. What is the actual text for `MSG_ALERT_SETTINGS_FORM_ERROR`? If `wrapper.text()` returns undefined, the test may still pass with the wrong values!
expect(wrapper.text()).toBe(MSG_ALERT_SETTINGS_FORM_ERROR);
// Very bad. Same problem as above and we are going through the vm property!
expect(wrapper.text()).toBe(MyComponent.vm.i18n.buttonLabel);
// Good. What we are expecting is very clear and there can be no surprises.
expect(wrapper.text()).toBe('There was an error: Please refresh and hope for the best!');
동적 번역#
자세한 내용은 번역을 동적으로 유지하기를 참고하세요.
번역된 문자열 변경하기#
GitLab에서 소스 문자열을 변경하는 경우, 변경 사항을 푸시하기 전에 pot 파일을 업데이트해야 합니다.
pot 파일이 최신 상태가 아니면, pre-push 검사와 gettext용 파이프라인 job이 실패합니다.
특수 콘텐츠 작업#
보간(Interpolation)#
번역된 텍스트의 플레이스홀더는 해당 소스 파일의 코드 스타일과 일치해야 합니다. 예를 들어 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() {
return 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-es의 escape를 사용하여 보간된 동적 값을 직접 이스케이프해야 합니다.
import { escape } from 'lodash-es';
import { __, sprintf } from '~/locale';
let someDynamicValue = '<script>alert("evil")</script>';
// Dangerous:
sprintf(__('This is %{value}'), { value: `<strong>${someDynamicValue}</strong>`, false);
// => 'This is <strong><script>alert('evil')</script></strong>'
// Incorrect:
sprintf(__('This is %{value}'), { value: `<strong>${someDynamicValue}</strong>` });
// => 'This is <strong><script>alert('evil')</script></strong>'
// OK:
sprintf(__('This is %{value}'), { value: `<strong>${escape(someDynamicValue)}</strong>` }, false);
// => 'This is <strong><script>alert('evil')</script></strong>'
복수형#
GitLab은 복수형 처리에 GNU gettext를 사용합니다. n_() 및 n__() 구문, CLDR 복수형 카테고리, 일반적인 안티패턴에 대한 지침은 복수형을 참고하세요.
네임스페이스#
네임스페이스는 함께 속하는 번역을 그룹화하는 방법입니다. 바 기호(|) 뒤에 접두사를 추가하여 번역자에게 맥락을 제공합니다. 예를 들어:
'Namespace|Translated string'
네임스페이스는:
-
단어의 모호함을 해소합니다. 예:
Promotions|Promote대Epic|Promote. -
번역자가 임의의 문자열 대신 동일한 제품 영역에 속하는 외부화된 문자열 번역에 집중할 수 있게 합니다.
-
번역자에게 언어적 맥락을 제공합니다.
일부 언어는 영어보다 맥락 의존적입니다.
예를 들어, cancel은 사용 방법에 따라 다르게 번역될 수 있습니다.
사용 맥락을 정의하기 위해, 영어 UI 텍스트에는 항상 네임스페이스를 추가하세요.
네임스페이스는 PascalCase여야 합니다.
네임스페이스를 선택할 때, 더 나은 맥락을 제공하기 위해 광범위한 카테고리보다 세밀한 하위 카테고리를 선호하세요.
Feature|와 같은 일반적인 네임스페이스 대신, 기능 영역 또는 액션 맥락을 설명하는 더 구체적인 하위 카테고리를 사용하는 것을 고려하세요.
이는 적절한 맥락 없이는 모호할 수 있는 일반적으로 보이는 문자열에 특히 중요합니다.
예를 들어:
-
WorkItems|Add to대신WorkItemsStatusConfigure|Add to를 사용하세요. -
MergeRequest|Approve대신MergeRequestReviewActions|Approve를 사용하세요. -
Project|Delete대신ProjectSettingsGeneral|Delete를 사용하세요.
이 접근 방식은 번역자가 다음을 이해하는 데 도움이 됩니다:
-
문자열이 나타나는 특정 UI 맥락.
-
의도된 액션 또는 목적.
-
문자열이 동일한 기능 영역의 다른 유사한 문자열과 어떻게 관련되는지.
-
Ruby/HAML에서:
s_('OpenedNDaysAgo|Opened')
번역을 찾을 수 없으면 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 엔티티 코드(< 또는 >)를 대신 사용하세요:
- 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: [] }) };
// ... using the string
element.innerHTML = i18n.LESS_THAN_ONE_HOUR;
// => 'In < 1 hour'
- Vue에서:
<gl-sprintf :message="s__('In < 1 hours')"/>
// => 'In < 1 hour'
숫자#
다른 로케일은 다른 숫자 형식을 사용할 수 있습니다. 숫자의 현지화를 지원하기 위해 toLocaleString()을 활용하는 formatNumber를 사용합니다.
기본적으로 formatNumber는 현재 사용자 로케일을 사용하여 숫자를 문자열로 형식화합니다.
- JavaScript에서:
import { formatNumber } from '~/locale';
// Assuming "User Preferences > Language" is set to "English":
const tenThousand = formatNumber(10000); // "10,000" (uses comma as decimal symbol in English locale)
const fiftyPercent = formatNumber(0.5, { style: 'percent' }) // "50%" (other options are passed to 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에 정의된 형식 중 필요한 날짜/시간 사양과 일치하는 것이 없고, 매우 특수한 경우(예: 단일 뷰에서만 사용되는 경우)라 새 형식으로 추가할 필요가 없을 때strftime을 사용합니다.
모범 사례#
번역 업데이트 최소화#
업데이트는 해당 문자열의 번역 손실을 초래할 수 있습니다. 위험을 최소화하려면 다음과 같은 경우가 아니면 문자열 변경을 피하세요:
-
사용자에게 가치를 추가하는 경우.
-
번역자를 위한 추가적인 맥락을 포함하는 경우.
예를 들어, 다음과 같은 변경을 피하세요:
- _('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에서(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 문서에 설명된 모범 사례를 따르세요.
항상 번역 헬퍼에 문자열 리터럴을 전달하기#
tooling/bin/gettext_extractor locale/gitlab.pot 스크립트는 코드베이스를 파싱하고 번역 헬퍼에서 번역할 준비가 된 모든 문자열을 추출합니다.
스크립트는 변수, 함수 호출 또는 문자열 보간으로 전달된 경우 문자열을 확인할 수 없습니다. 따라서 항상 헬퍼에 문자열 리터럴을 전달해야 합니다.
- Ruby/HAML에서:
# Good
_('Some label');
_("Some label");
_('Hi %{name}') % { name: 'Luki' }
s_('Namespace|Label');
n_('%d apple', '%d apples', appleCount);
# Bad
_(LABEL);
_('Hi %{name}' % { name: 'Luki' })
_("Step: #{count}");
s_(get_label());
n_(label_singular, label_plural, appleCount);
- JavaScript에서:
// Good
__('Some label');
s__('Namespace', 'Label');
s__('Namespace|Label');
n__('%d apple', '%d apples', appleCount);
// Bad
__(LABEL);
__(`Step: ${count}`);
s__(getLabel());
s__(NAMESPACE, LABEL);
n__(LABEL_SINGULAR, LABEL_PLURAL, appleCount);
변수를 사용한 동적 텍스트 삽입#
번역 가능한 문자열에서 변수로 텍스트 값을 사용하는 경우, 다양한 언어에서 문법적 정확성을 보장하기 위해 특별한 주의가 필요합니다.
위험 요소 및 과제#
번역 가능한 문자열에 변수를 사용하여 텍스트를 추가할 때 여러 현지화 문제가 발생할 수 있습니다:
-
성(Gender) 일치: 문법적 성이 있는 언어는 삽입된 명사의 성에 따라 다른 형태의 관사, 형용사 또는 대명사가 필요할 수 있습니다. 예를 들어, 프랑스어에서는 관사, 형용사 및 일부 과거 분사가 명사의 성별 및 문장 내 위치와 일치해야 합니다.
-
격과 어형 변화: 격이 있는 언어(독일어 등)에서 삽입된 텍스트는 문장에서 문법적 역할에 따라 다른 형태가 필요할 수 있습니다.
-
어순: 다른 언어는 다른 어순 요구 사항이 있으며, 삽입된 텍스트는 자연스러운 번역을 위해 문장의 다른 위치에 나타나야 할 수 있습니다.
모범 사례#
-
가능하면 변수로 텍스트를 추가하지 마세요:
변수가 있는 하나의 문자열 대신, 각 경우에 대해 고유한 문자열을 생성하세요. 예를 들어:
# Instead of:
s_('WorkItem|Adds this %{workItemType} as related to %{relatedWorkItemType}')
# Create separate strings:
s_('WorkItem|Adds this task as related to incident')
s_('WorkItem|Adds this incident as related to task')
- 문장형 대신 주제-설명 구조 사용: 변수 사용을 피할 수 없는 경우, 완전한 문장 대신 주제-설명 구조를 사용하도록 메시지를 재구성하는 것을 고려하세요:
# Instead of a sentence with inserted variables:
s_('WorkItem|Adds this %{workItemType} as related to %{relatedWorkItemType}')
# Use topic-comment structure:
s_('WorkItem|Related items: %{workItemType} → %{relatedWorkItemType}')
패턴 결합#
네임스페이싱, 복수형 처리, 파라미터 치환과 같은 여러 기능이 필요한 복잡한 시나리오를 처리하기 위해 번역 헬퍼 함수를 결합할 수 있습니다.
예를 들어:
ns_('BulkImport|%{count} placeholder user has been created.', 'BulkImport|%{count} placeholder users have been created.', @success_count)
번역 가능한 문자열의 대소문자 변환#
다른 언어는 영어와 다른 대문자 표기 규칙이 있을 수 있습니다. 예를 들어, 독일어에서는 문장 내 위치와 관계없이 모든 명사가 대문자로 표기됩니다. 번역 가능한 문자열에 downcase 또는 toLocaleLowerCase()를 사용하지 마세요. 번역자가 텍스트를 제어할 수 있도록 하세요.
-
맥락 의존적인 경우:
toLocaleLowerCase()메서드는 로케일을 인식하지만, 맥락별 대문자 표기 요구 사항을 처리할 수 없습니다. 예를 들어:
# This forces lowercase, but it may not work for many languages:
job_type = "CI/CD Pipeline".toLocaleLowerCase()
s_("Jobs|Starting a new %{job_type}") % { job_type: job_type }
# In German this would incorrectly show:
# "Starting a new ci/cd pipeline"
# When it should be:
# "Starting a new CI/CD Pipeline" (Pipeline is a noun and must be capitalized)
# In French it might show:
# "Starting a new ci/cd pipeline"
# When it should be:
# "Démarrer un nouveau pipeline CI/CD" (technical terms might keep original case)
새 콘텐츠로 PO 파일 업데이트하기#
이제 새 콘텐츠가 번역 대상으로 표시되었으므로, 다음 명령어를 실행하여 locale/gitlab.pot 파일을 업데이트하세요:
tooling/bin/gettext_extractor locale/gitlab.pot
이 명령어는 새로 외부화된 문자열로 locale/gitlab.pot 파일을 업데이트하고 사용하지 않는 문자열을 제거합니다. 변경 사항이 기본 브랜치에 올라오면 Crowdin이 이를 가져와 번역을 위해 제공합니다.
locale/[language]/gitlab.po 파일의 변경 사항은 체크인할 필요가 없습니다. Crowdin의 번역이 병합될 때 자동으로 업데이트됩니다.
gitlab.pot 파일에 머지 충돌이 있는 경우, 파일을 삭제하고 동일한 명령어를 사용하여 다시 생성할 수 있습니다.
PO 파일 유효성 검사#
번역 파일을 최신 상태로 유지하기 위해 static-analysis job의 일부로 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 파일에는 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 편집기를 사용하여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 "Add French translations for Value Stream Analytics page"
UI에서 번역 수동으로 테스트하기#
Vue 번역을 수동으로 테스트하려면:
-
GitLab 현지화를 영어 이외의 다른 언어로 변경하세요.
-
bin/rake gettext:compile을 사용하여 JSON 파일을 생성하세요.