feat(l10n): Add support for msgctxt when translating. (#4916) r=vladikoff,shane-tomlinson
Use a triple / (///) to give a t a context that will be used as a comment for the l10n team. Fixes #3128
This commit is contained in:
Родитель
1b879f1142
Коммит
c818489447
|
@ -42,6 +42,12 @@ define(function (require, exports, module) {
|
|||
// The words "Web Session" are coming soon to the device & apps view, see #4585.
|
||||
t('Web Session');
|
||||
|
||||
// For #3128, PR #4916 - We added a 'msgctxt' comment to these buttons
|
||||
// to allow the l10n team differntiate between headers and buttons. This
|
||||
// string is kept and used as a fallback for locales that have it
|
||||
// translated but have not yet translated the contextualized variant.
|
||||
t('Sign in');
|
||||
|
||||
/**
|
||||
* Replace instances of %s and %(name)s with their corresponding values in
|
||||
* the context
|
||||
|
|
|
@ -44,12 +44,23 @@ define(function (require, exports, module) {
|
|||
* Gets a translated value by key but returns the key if nothing is found.
|
||||
* Does string interpolation on %s and %(named)s.
|
||||
* @method get
|
||||
* @param {String} key
|
||||
* @param {String} context
|
||||
* @param {String} stringToTranslate
|
||||
* @param {Object} [context={}]
|
||||
* @returns {String}
|
||||
*/
|
||||
get (key, context) {
|
||||
var translation = this.__translations__[key];
|
||||
get (stringToTranslate, context = {}) {
|
||||
const translations = this.__translations__;
|
||||
let translation;
|
||||
|
||||
if (context.msgctxt) {
|
||||
const stringWithContextPrefix = `${context.msgctxt}\u0004${stringToTranslate}`;
|
||||
// If a translation exists with a context prefix, use that. If no translation exists
|
||||
// with the context prefix, try to find a string without the context prefix.
|
||||
translation = translations[stringWithContextPrefix] || translations[stringToTranslate];
|
||||
} else {
|
||||
translation = translations[stringToTranslate];
|
||||
}
|
||||
|
||||
/**
|
||||
* See http://www.lehman.cuny.edu/cgi-bin/man-cgi?msgfmt+1
|
||||
* and
|
||||
|
@ -77,7 +88,7 @@ define(function (require, exports, module) {
|
|||
translation = $.trim(translation);
|
||||
|
||||
if (! translation) {
|
||||
translation = key;
|
||||
translation = stringToTranslate;
|
||||
}
|
||||
|
||||
return this.interpolate(translation, context);
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
</div>
|
||||
|
||||
<div class="button-row">
|
||||
<button id="submit-btn" type="submit" class="disabled">{{#t}}Sign in{{/t}}</button>
|
||||
<button id="submit-btn" type="submit" class="disabled">{{buttonSignInText}}</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
|
|
@ -3,10 +3,10 @@
|
|||
<h1 id="fxa-signin-header">
|
||||
{{#serviceName}}
|
||||
<!-- L10N: For languages structured like English, the second phrase can read "to continue to %(serviceName)s" -->
|
||||
{{#t}}Sign in{{/t}} <span class="service">{{#t}}Continue to %(serviceName)s{{/t}}</span>
|
||||
{{ headerSignInText }} <span class="service">{{#t}}Continue to %(serviceName)s{{/t}}</span>
|
||||
{{/serviceName}}
|
||||
{{^serviceName}}
|
||||
{{#t}}Sign in{{/t}}
|
||||
{{ headerSignInText }}
|
||||
{{/serviceName}}
|
||||
</h1>
|
||||
</header>
|
||||
|
@ -40,7 +40,7 @@
|
|||
</div>
|
||||
|
||||
<div class="button-row">
|
||||
<button id="submit-btn" type="submit" class="disabled">{{#t}}Sign in{{/t}}</button>
|
||||
<button id="submit-btn" type="submit" class="disabled">{{buttonSignInText}}</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
@ -52,7 +52,7 @@
|
|||
|
||||
{{^chooserAskForPassword}}
|
||||
<div class="button-row">
|
||||
<button type="submit" class="use-logged-in">{{#t}}Sign in{{/t}}</button>
|
||||
<button type="submit" class="use-logged-in">{{buttonSignInText}}</button>
|
||||
</div>
|
||||
|
||||
<div class="links">
|
||||
|
@ -72,7 +72,7 @@
|
|||
</div>
|
||||
|
||||
<div class="button-row">
|
||||
<button id="submit-btn" type="submit" class="disabled">{{#t}}Sign in{{/t}}</button>
|
||||
<button id="submit-btn" type="submit" class="disabled">{{buttonSignInText}}</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ define(function (require, exports, module) {
|
|||
|
||||
const _ = require('underscore');
|
||||
const AuthErrors = require('lib/auth-errors');
|
||||
const BaseView = require('views/base');
|
||||
const { cancelEventThen, t } = require('views/base');
|
||||
const Cocktail = require('cocktail');
|
||||
const FormView = require('views/form');
|
||||
const NullBehavior = require('views/behaviors/null');
|
||||
|
@ -149,14 +149,18 @@ define(function (require, exports, module) {
|
|||
},
|
||||
|
||||
context () {
|
||||
/// submit button
|
||||
const buttonSignInText = this.translate(t('Sign in'), { msgctxt: 'submit button' });
|
||||
|
||||
return {
|
||||
buttonSignInText,
|
||||
email: this.relier.get('email'),
|
||||
password: this._formPrefill.get('password')
|
||||
};
|
||||
},
|
||||
|
||||
events: _.extend({}, SignInView.prototype.events, {
|
||||
'click a[href="/reset_password"]': BaseView.cancelEventThen('_navigateToForceResetPassword')
|
||||
'click a[href="/reset_password"]': cancelEventThen('_navigateToForceResetPassword')
|
||||
}),
|
||||
|
||||
beforeDestroy () {
|
||||
|
|
|
@ -86,10 +86,18 @@ define(function (require, exports, module) {
|
|||
var hasSuggestedAccount = suggestedAccount.get('email');
|
||||
var email = this.getEmail();
|
||||
|
||||
/// submit button
|
||||
const buttonSignInText = this.translate(t('Sign in'), { msgctxt: 'submit button' });
|
||||
|
||||
/// header text
|
||||
const headerSignInText = this.translate(t('Sign in'), { msgctxt: 'header text' });
|
||||
|
||||
return {
|
||||
buttonSignInText,
|
||||
chooserAskForPassword: this._suggestedAccountAskPassword(suggestedAccount),
|
||||
email: email,
|
||||
error: this.error,
|
||||
headerSignInText,
|
||||
isAmoMigration: this.isAmoMigration(),
|
||||
isSyncMigration: this.isSyncMigration(),
|
||||
password: this._formPrefill.get('password'),
|
||||
|
|
|
@ -11,9 +11,11 @@ define(function (require, exports, module) {
|
|||
const Translator = require('lib/translator');
|
||||
|
||||
// translations taken from Persona's db_LB translations.
|
||||
var TRANSLATIONS = {
|
||||
const TRANSLATIONS = {
|
||||
// use one direct translation to prepare for simpler json files.
|
||||
/* eslint-disable sorting/sort-object-props */
|
||||
'%s, Persona requires cookies to remember you.': '%s, Ԁǝɹsouɐ ɹǝbnıɹǝs ɔooʞıǝs ʇo ɹǝɯǝɯqǝɹ ʎon\u02D9',
|
||||
'header\u0004%s, Persona requires cookies to remember you.': '%s, Ԁǝɹsouɐ ɹǝbnıɹǝs ɔooʞıǝs ʇo ɹǝɯǝɯqǝɹ ʎon\u02D9 - header',
|
||||
'Error encountered trying to register: %(email)s.': [
|
||||
null,
|
||||
'Ǝɹɹoɹ ǝuɔonuʇǝɹǝp ʇɹʎıuƃ ʇo ɹǝƃısʇɹɐʇıou: %(email)s\u02D9'
|
||||
|
@ -22,23 +24,24 @@ define(function (require, exports, module) {
|
|||
null,
|
||||
'\u22A5ɥǝɹǝ ʍɐs ɐ dɹoqʅǝɯ ʍıʇɥ ʎonɹ sıƃund ʅıuʞ\u02D9 Hɐs ʇɥıs ɐppɹǝss ɐʅɹǝɐpʎ qǝǝu ɹǝƃısʇǝɹǝp\xBF'
|
||||
]
|
||||
/* eslint-enable sorting/sort-object-props */
|
||||
};
|
||||
|
||||
describe('lib/translator', function () {
|
||||
describe('lib/translator', () => {
|
||||
var translator;
|
||||
|
||||
beforeEach(function () {
|
||||
beforeEach(() => {
|
||||
// Bringing back the David Bowie's Labrynth
|
||||
translator = new Translator('db-LB', ['db-LB']);
|
||||
translator.set(TRANSLATIONS);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
afterEach(() => {
|
||||
translator = null;
|
||||
});
|
||||
|
||||
describe('get', function () {
|
||||
it('returns translated string when it exists', function () {
|
||||
describe('get', () => {
|
||||
it('returns translated string when it exists', () => {
|
||||
var stringToTranslate =
|
||||
'There was a problem with your signup link. Has this address already been registered?';
|
||||
var translation = translator.get(stringToTranslate);
|
||||
|
@ -46,20 +49,33 @@ define(function (require, exports, module) {
|
|||
'⊥ɥǝɹǝ ʍɐs ɐ dɹoqʅǝɯ ʍıʇɥ ʎonɹ sıƃund ʅıuʞ˙ Hɐs ʇɥıs ɐppɹǝss ɐʅɹǝɐpʎ qǝǝu ɹǝƃısʇǝɹǝp¿');
|
||||
});
|
||||
|
||||
it('returns untranslated string when translation does not exist', function () {
|
||||
it('msgctxt annotation', () => {
|
||||
const stringToTranslate =
|
||||
'%s, Persona requires cookies to remember you.';
|
||||
let translation = translator.get(stringToTranslate, { msgctxt: 'header' });
|
||||
assert.equal(translation, '%s, Ԁǝɹsouɐ ɹǝbnıɹǝs ɔooʞıǝs ʇo ɹǝɯǝɯqǝɹ ʎon\u02D9 - header');
|
||||
|
||||
translation = translator.get(stringToTranslate);
|
||||
assert.equal(translation, '%s, Ԁǝɹsouɐ ɹǝbnıɹǝs ɔooʞıǝs ʇo ɹǝɯǝɯqǝɹ ʎon\u02D9');
|
||||
|
||||
translation = translator.get(stringToTranslate, { msgctxt: 'non existent' });
|
||||
assert.equal(translation, '%s, Ԁǝɹsouɐ ɹǝbnıɹǝs ɔooʞıǝs ʇo ɹǝɯǝɯqǝɹ ʎon\u02D9');
|
||||
});
|
||||
|
||||
it('returns untranslated string when translation does not exist', () => {
|
||||
var stringToTranslate = 'this string is untranslated';
|
||||
var translation = translator.get(stringToTranslate);
|
||||
assert.equal(translation, stringToTranslate);
|
||||
});
|
||||
|
||||
it('can do string interpolation on unnamed `%s` when given array context', function () {
|
||||
it('can do string interpolation on unnamed `%s` when given array context', () => {
|
||||
var stringToTranslate = '%s, Persona requires cookies to remember you.';
|
||||
var translation = translator.get(stringToTranslate, ['testuser@testuser.com']);
|
||||
assert.equal(translation,
|
||||
'testuser@testuser.com, Ԁǝɹsouɐ ɹǝbnıɹǝs ɔooʞıǝs ʇo ɹǝɯǝɯqǝɹ ʎon˙');
|
||||
});
|
||||
|
||||
it('can do string interpolation on named `%(name)s` when given array context', function () {
|
||||
it('can do string interpolation on named `%(name)s` when given array context', () => {
|
||||
var stringToTranslate = 'Error encountered trying to register: %(email)s.';
|
||||
var translation = translator.get(stringToTranslate, {
|
||||
email: 'testuser@testuser.com'
|
||||
|
@ -68,7 +84,7 @@ define(function (require, exports, module) {
|
|||
'Ǝɹɹoɹ ǝuɔonuʇǝɹǝp ʇɹʎıuƃ ʇo ɹǝƃısʇɹɐʇıou: testuser@testuser.com˙');
|
||||
});
|
||||
|
||||
it('can do interpolation multiple times with an array', function () {
|
||||
it('can do interpolation multiple times with an array', () => {
|
||||
var stringToTranslate = 'Hi %s, you have been signed in since %s';
|
||||
var translation = translator.get(stringToTranslate, [
|
||||
'testuser@testuser.com', 'noon'
|
||||
|
@ -78,7 +94,7 @@ define(function (require, exports, module) {
|
|||
'Hi testuser@testuser.com, you have been signed in since noon');
|
||||
});
|
||||
|
||||
it('can do interpolation multiple times with an object', function () {
|
||||
it('can do interpolation multiple times with an object', () => {
|
||||
var stringToTranslate = 'Hi %(email)s, you have been signed in since %(time)s';
|
||||
var translation = translator.get(stringToTranslate, {
|
||||
email: 'testuser@testuser.com',
|
||||
|
@ -89,14 +105,14 @@ define(function (require, exports, module) {
|
|||
'Hi testuser@testuser.com, you have been signed in since noon');
|
||||
});
|
||||
|
||||
it('does no replacement on %s and %(name)s if not in context', function () {
|
||||
it('does no replacement on %s and %(name)s if not in context', () => {
|
||||
var stringToTranslate = 'Hi %s, you have been signed in since %(time)s';
|
||||
var translation = translator.get(stringToTranslate);
|
||||
|
||||
assert.equal(translation, stringToTranslate);
|
||||
});
|
||||
|
||||
it('leaves remaining %s if not enough items in context', function () {
|
||||
it('leaves remaining %s if not enough items in context', () => {
|
||||
var stringToTranslate = 'Hi %s, you have been signed in since %s';
|
||||
var translation = translator.get(stringToTranslate, ['testuser@testuser.com']);
|
||||
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -71,7 +71,7 @@
|
|||
"helmet": "3.1.0",
|
||||
"i18n-abide": "0.0.25",
|
||||
"joi": "10.2.2",
|
||||
"jsxgettext-recursive": "1.0.1",
|
||||
"jsxgettext-recursive": "git://github.com/vladikoff/jsxgettext-recursive#msgctxt-support",
|
||||
"load-grunt-tasks": "3.5.2",
|
||||
"lodash": "4.17.2",
|
||||
"mkdirp": "0.5.1",
|
||||
|
|
Загрузка…
Ссылка в новой задаче