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:
Shane Tomlinson 2017-04-11 17:02:47 +01:00 коммит произвёл Vlad Filippov
Родитель 1b879f1142
Коммит c818489447
9 изменённых файлов: 806 добавлений и 912 удалений

Просмотреть файл

@ -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']);

1619
npm-shrinkwrap.json сгенерированный

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -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",