зеркало из https://github.com/mozilla/pontoon.git
Update to fluent libraries to 0.6.2 (#840)
Includes: * Switch to PyPI install * Support for Terms (treated same as Messages) * Support for GroupComments as a replacement for Section comments * Remove reference to Tags * Reflect the introduction of type Placeable * Replace serializeExpression() with fluentSerializer.serializeExpression() * Use `isinstance` instead of `type` * Factor out isSelectExpressionElement() * In unsaved changes check, if translation cannot be parsed, return source editor value * If no textarea has focus, select first * Add check for matching Message keys * runChecks() as a standalone function * Run checks when switching from source view to rich mode * Properly render multiple selectors
This commit is contained in:
Родитель
eb4fd72aa4
Коммит
b76c53a78e
|
@ -1,30 +1,9 @@
|
|||
/* global FluentSyntax */
|
||||
var fluentParser = new FluentSyntax.FluentParser({ withSpans: false });
|
||||
var fluentSerializer = new FluentSyntax.FluentSerializer();
|
||||
|
||||
/* Public functions used across different files */
|
||||
var Pontoon = (function (my) {
|
||||
var fluentParser = new FluentSyntax.FluentParser({ withSpans: false });
|
||||
var fluentSerializer = new FluentSyntax.FluentSerializer();
|
||||
|
||||
// TODO: Replace with fluentSerializer.serializeExpression() on fluent.js 0.6 update
|
||||
function serializeExpression(expression) {
|
||||
switch (expression.type) {
|
||||
case 'MessageReference':
|
||||
return expression.id.name;
|
||||
case 'ExternalArgument':
|
||||
return '$' + expression.id.name;
|
||||
case 'NumberExpression':
|
||||
return expression.value || expression.val.value;
|
||||
case 'StringExpression':
|
||||
return '"' + (expression.value || expression.val.value) + '"';
|
||||
case 'NamedArgument':
|
||||
return expression.name.name + ': ' + serializeExpression(expression.val);
|
||||
case 'CallExpression':
|
||||
var args = expression.args.map(function (arg) {
|
||||
return serializeExpression(arg);
|
||||
});
|
||||
return expression.callee.name + '(' + args.join(', ') + ')';
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Render original string element: simple string value or a variant.
|
||||
|
@ -103,8 +82,8 @@ var Pontoon = (function (my) {
|
|||
}
|
||||
|
||||
// Render SelectExpression
|
||||
if (element.type === 'SelectExpression') {
|
||||
element.variants.forEach(function (item) {
|
||||
if (Pontoon.fluent.isSelectExpressionElement(element)) {
|
||||
element.expression.variants.forEach(function (item) {
|
||||
content += renderOriginalElement(item.key.value || item.key.name, item.value.elements);
|
||||
});
|
||||
}
|
||||
|
@ -139,9 +118,9 @@ var Pontoon = (function (my) {
|
|||
}
|
||||
|
||||
// Render SelectExpression
|
||||
if (element.type === 'SelectExpression') {
|
||||
var expression = serializeExpression(element.expression);
|
||||
content = '<li data-expression="' + expression + '"><ul>' + content;
|
||||
if (Pontoon.fluent.isSelectExpressionElement(element)) {
|
||||
var expression = fluentSerializer.serializeExpression(element.expression.expression);
|
||||
content += '<li data-expression="' + expression + '"><ul>';
|
||||
|
||||
var isPlural = Pontoon.fluent.isPluralElement(element);
|
||||
if (isPlural && !isTranslated) {
|
||||
|
@ -150,7 +129,7 @@ var Pontoon = (function (my) {
|
|||
});
|
||||
}
|
||||
else {
|
||||
element.variants.forEach(function (item) {
|
||||
element.expression.variants.forEach(function (item) {
|
||||
content += renderEditorElement(
|
||||
item.key.value || item.key.name,
|
||||
item.value.elements,
|
||||
|
@ -249,7 +228,7 @@ var Pontoon = (function (my) {
|
|||
(translationAST && !self.isSupportedMessage(translationAST)) ||
|
||||
(!translationAST && !self.isSupportedMessage(entityAST))
|
||||
) {
|
||||
value = entity.key + ' = \n';
|
||||
value = entity.key + ' = ';
|
||||
|
||||
if (translationAST) {
|
||||
value = translation.string;
|
||||
|
@ -339,19 +318,17 @@ var Pontoon = (function (my) {
|
|||
* Message is supported if all value elements
|
||||
* and all attribute elements are of type:
|
||||
* - TextElement
|
||||
* - ExternalArgument
|
||||
* - MessageReference
|
||||
* - SelectExpression?
|
||||
* - Placeable with expression type ExternalArgument, MessageReference or SelectExpression
|
||||
*/
|
||||
isSupportedMessage: function (ast) {
|
||||
var self = this;
|
||||
|
||||
function elementsSupported(elements) {
|
||||
return elements.every(function(item) {
|
||||
return [
|
||||
'TextElement',
|
||||
'ExternalArgument',
|
||||
'MessageReference',
|
||||
'SelectExpression'
|
||||
].indexOf(item.type) >= 0;
|
||||
return elements.every(function(element) {
|
||||
return (
|
||||
self.isSimpleElement(element) ||
|
||||
self.isSelectExpressionElement(element)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -372,15 +349,59 @@ var Pontoon = (function (my) {
|
|||
/*
|
||||
* Is element of type that can be presented as a simple string:
|
||||
* - TextElement
|
||||
* - ExternalArgument
|
||||
* - MessageReference
|
||||
* - Placeable with expression type ExternalArgument or MessageReference
|
||||
*/
|
||||
isSimpleElement: function (element) {
|
||||
return [
|
||||
'TextElement',
|
||||
'ExternalArgument',
|
||||
'MessageReference'
|
||||
].indexOf(element.type) >= 0;
|
||||
if (element.type === 'TextElement') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Placeable
|
||||
if (
|
||||
element.expression &&
|
||||
[
|
||||
'ExternalArgument',
|
||||
'MessageReference'
|
||||
].indexOf(element.expression.type) >= 0
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
|
||||
/*
|
||||
* Is element a SelectExpression?
|
||||
*
|
||||
* We evaluate that by checking if element expression has variants defined.
|
||||
* If yes, the element must be a SelectExpression according to Fluent ASDL:
|
||||
* https://github.com/projectfluent/fluent/blob/master/spec/fluent.asdl
|
||||
*/
|
||||
isSelectExpressionElement: function (element) {
|
||||
return element.expression && element.expression.variants;
|
||||
},
|
||||
|
||||
|
||||
/*
|
||||
* Is element representing a pluralized string?
|
||||
*
|
||||
* Keys of all variants of such elements are either
|
||||
* CLDR plurals or numbers.
|
||||
*/
|
||||
isPluralElement: function (element) {
|
||||
if (!this.isSelectExpressionElement(element)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var CLDRplurals = ['zero', 'one', 'two', 'few', 'many', 'other'];
|
||||
|
||||
return element.expression.variants.every(function (item) {
|
||||
return (
|
||||
CLDRplurals.indexOf(item.key.name) !== -1 ||
|
||||
item.key.type === 'NumberExpression'
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
|
@ -397,7 +418,6 @@ var Pontoon = (function (my) {
|
|||
if (
|
||||
ast &&
|
||||
!ast.attributes.length &&
|
||||
!ast.tags.length &&
|
||||
ast.value &&
|
||||
ast.value.elements.every(function(item) {
|
||||
return self.isSimpleElement(item);
|
||||
|
@ -410,28 +430,6 @@ var Pontoon = (function (my) {
|
|||
},
|
||||
|
||||
|
||||
/*
|
||||
* Is element representing a pluralized string?
|
||||
*
|
||||
* Keys of all variants of such elements are either
|
||||
* CLDR plurals or numbers.
|
||||
*/
|
||||
isPluralElement: function (element) {
|
||||
if (!element.variants) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var CLDRplurals = ['zero', 'one', 'two', 'few', 'many', 'other'];
|
||||
|
||||
return element.variants.every(function (item) {
|
||||
return (
|
||||
CLDRplurals.indexOf(item.key.name) !== -1 ||
|
||||
item.key.type === 'NumberExpression'
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/*
|
||||
* Serialize value with placeables into a simple string
|
||||
*
|
||||
|
@ -443,35 +441,36 @@ var Pontoon = (function (my) {
|
|||
var startMarker = '';
|
||||
var endMarker = '';
|
||||
|
||||
elements.forEach(function (item) {
|
||||
if (item.type === 'TextElement') {
|
||||
elements.forEach(function (element) {
|
||||
if (element.type === 'TextElement') {
|
||||
if (markPlaceables) {
|
||||
translatedValue += Pontoon.markXMLTags(item.value);
|
||||
translatedValue += Pontoon.markXMLTags(element.value);
|
||||
}
|
||||
else {
|
||||
translatedValue += item.value;
|
||||
translatedValue += element.value;
|
||||
}
|
||||
}
|
||||
else if (item.type === 'ExternalArgument') {
|
||||
if (markPlaceables) {
|
||||
startMarker = '<mark class="placeable" title="External Argument">';
|
||||
endMarker = '</mark>';
|
||||
else if (element.type === 'Placeable') {
|
||||
if (element.expression.type === 'ExternalArgument') {
|
||||
if (markPlaceables) {
|
||||
startMarker = '<mark class="placeable" title="External Argument">';
|
||||
endMarker = '</mark>';
|
||||
}
|
||||
translatedValue += startMarker + '{$' + element.expression.id.name + '}' + endMarker;
|
||||
}
|
||||
translatedValue += startMarker + '{$' + item.id.name + '}' + endMarker;
|
||||
}
|
||||
else if (item.type === 'MessageReference') {
|
||||
if (markPlaceables) {
|
||||
startMarker = '<mark class="placeable" title="Message Reference">';
|
||||
endMarker = '</mark>';
|
||||
else if (element.expression.type === 'MessageReference') {
|
||||
if (markPlaceables) {
|
||||
startMarker = '<mark class="placeable" title="Message Reference">';
|
||||
endMarker = '</mark>';
|
||||
}
|
||||
translatedValue += startMarker + '{' + element.expression.id.name + '}' + endMarker;
|
||||
}
|
||||
else if (self.isSelectExpressionElement(element)) {
|
||||
var variantElements = element.expression.variants.filter(function (variant) {
|
||||
return variant.default;
|
||||
})[0].value.elements;
|
||||
translatedValue += self.serializePlaceables(variantElements);
|
||||
}
|
||||
translatedValue += startMarker + '{' + item.id.name + '}' + endMarker;
|
||||
}
|
||||
else if (item.variants) {
|
||||
var variantElements = item.variants.filter(function (item) {
|
||||
return item.default;
|
||||
})[0].value.elements;
|
||||
|
||||
translatedValue += self.serializePlaceables(variantElements);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -635,7 +634,8 @@ var Pontoon = (function (my) {
|
|||
|
||||
|
||||
/*
|
||||
* Return translation in the editor as FTL source
|
||||
* Return translation in the editor as FTL source to be used
|
||||
* in unsaved changes check
|
||||
*/
|
||||
getTranslationSource: function () {
|
||||
var entity = Pontoon.getEditorEntity();
|
||||
|
@ -648,6 +648,11 @@ var Pontoon = (function (my) {
|
|||
|
||||
var translation = this.serializeTranslation(entity, fallback);
|
||||
|
||||
// If translation broken, incomplete or empty
|
||||
if (translation.error) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
// Special case: empty translations in rich FTL editor don't serialize properly
|
||||
if (this.isFTLEditorEnabled()) {
|
||||
var richTranslation = $.map(
|
||||
|
@ -657,7 +662,7 @@ var Pontoon = (function (my) {
|
|||
).join('');
|
||||
|
||||
if (!richTranslation.length) {
|
||||
translation = entity.key + ' = \n';
|
||||
translation = entity.key + ' = ';
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -674,7 +679,7 @@ var Pontoon = (function (my) {
|
|||
}
|
||||
|
||||
var entityAST = fluentParser.parseEntry(entity.original);
|
||||
var content = entity.key + ' = '; // Initialize untranslated string
|
||||
var content = entity.key + ' = ';
|
||||
var valueElements = $('#ftl-area .main-value ul:first > li:visible');
|
||||
var attributeElements = $('#ftl-area .attributes ul:first > li:visible');
|
||||
var value = '';
|
||||
|
@ -723,25 +728,7 @@ var Pontoon = (function (my) {
|
|||
}
|
||||
|
||||
var ast = fluentParser.parseEntry(content);
|
||||
var error = null;
|
||||
|
||||
// Parse error
|
||||
if (ast.type === 'Junk') {
|
||||
error = ast.annotations[0].message;
|
||||
}
|
||||
// TODO: Should be removed by bug 1237667
|
||||
// Detect missing values
|
||||
else if (entityAST && ast && entityAST.value && !ast.value) {
|
||||
error = 'Please make sure to fill in the value';
|
||||
}
|
||||
// Detect missing attributes
|
||||
else if (
|
||||
entityAST.attributes &&
|
||||
ast.attributes &&
|
||||
entityAST.attributes.length !== ast.attributes.length
|
||||
) {
|
||||
error = 'Please make sure to fill in all the attributes';
|
||||
}
|
||||
var error = this.runChecks(ast, entityAST);
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
|
@ -753,6 +740,37 @@ var Pontoon = (function (my) {
|
|||
},
|
||||
|
||||
|
||||
/*
|
||||
* Perform error checks for provided translationAST and entityAST.
|
||||
*/
|
||||
runChecks: function (translationAST, entityAST) {
|
||||
// Parse error
|
||||
if (translationAST.type === 'Junk') {
|
||||
return translationAST.annotations[0].message;
|
||||
}
|
||||
|
||||
// TODO: Should be removed by bug 1237667
|
||||
// Detect missing values
|
||||
else if (entityAST.value && !translationAST.value) {
|
||||
return 'Please make sure to fill in the value';
|
||||
}
|
||||
|
||||
// Detect missing attributes
|
||||
else if (
|
||||
entityAST.attributes &&
|
||||
translationAST.attributes &&
|
||||
entityAST.attributes.length !== translationAST.attributes.length
|
||||
) {
|
||||
return 'Please make sure to fill in all the attributes';
|
||||
}
|
||||
|
||||
// Detect Message ID mismatch
|
||||
else if (entityAST.id.name !== translationAST.id.name) {
|
||||
return 'Please make sure the translation key matches the source string key';
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/*
|
||||
* Get simplified preview of the FTL message, used when full presentation not possible
|
||||
* due to lack of real estate (e.g. string list).
|
||||
|
@ -841,6 +859,16 @@ $(function () {
|
|||
|
||||
var translated = (translation && translation !== entity.key + ' = ');
|
||||
|
||||
// Perform error checks
|
||||
if (translated) {
|
||||
var translationAST = fluentParser.parseEntry(translation);
|
||||
var entityAST = fluentParser.parseEntry(entity.original);
|
||||
var error = Pontoon.fluent.runChecks(translationAST, entityAST);
|
||||
if (error) {
|
||||
return Pontoon.endLoader(error, 'error', 5000);
|
||||
}
|
||||
}
|
||||
|
||||
var isRichEditorSupported = Pontoon.fluent.renderEditor({
|
||||
pk: translated, // An indicator that the string is translated
|
||||
string: translation,
|
||||
|
@ -857,7 +885,7 @@ $(function () {
|
|||
|
||||
// If translation broken, incomplete or empty
|
||||
if (translation.error) {
|
||||
translation = entity.key + ' = \n';
|
||||
translation = entity.key + ' = ';
|
||||
}
|
||||
|
||||
$('#translation').val(translation);
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -1744,7 +1744,7 @@ var Pontoon = (function (my) {
|
|||
return;
|
||||
}
|
||||
|
||||
var textarea = $('#editor textarea:visible:focus'),
|
||||
var textarea = $('#editor textarea:visible:focus, #editor textarea:visible:first');
|
||||
selectionStart = textarea.prop('selectionStart'),
|
||||
selectionEnd = textarea.prop('selectionEnd'),
|
||||
placeable = $(this).text(),
|
||||
|
|
|
@ -258,17 +258,17 @@ def _serialize_elements(elements):
|
|||
response = ''
|
||||
|
||||
for element in elements:
|
||||
if type(element) == ast.TextElement:
|
||||
if isinstance(element, ast.TextElement):
|
||||
response += element.value
|
||||
|
||||
elif type(element) == ast.Placeable:
|
||||
if type(element.expression) == ast.ExternalArgument:
|
||||
elif isinstance(element, ast.Placeable):
|
||||
if isinstance(element.expression, ast.ExternalArgument):
|
||||
response += '{ $' + element.expression.id.name + ' }'
|
||||
|
||||
elif type(element.expression) == ast.MessageReference:
|
||||
elif isinstance(element.expression, ast.MessageReference):
|
||||
response += '{ ' + element.expression.id.name + ' }'
|
||||
|
||||
elif hasattr(element, 'expression') and hasattr(element.expression, 'variants'):
|
||||
elif hasattr(element.expression, 'variants'):
|
||||
variant_elements = filter(
|
||||
lambda x: x.default, element.expression.variants
|
||||
)[0].value.elements
|
||||
|
@ -284,7 +284,7 @@ def as_simple_translation(source):
|
|||
translation_ast = parser.parse_entry(source)
|
||||
|
||||
# Non-FTL string or string with an error
|
||||
if type(translation_ast) == ast.Junk:
|
||||
if isinstance(translation_ast, ast.Junk):
|
||||
return source
|
||||
|
||||
# Value: use entire AST
|
||||
|
|
|
@ -20,6 +20,7 @@ log = logging.getLogger(__name__)
|
|||
|
||||
parser = FluentParser()
|
||||
serializer = FluentSerializer()
|
||||
localizable_entries = (ast.Message, ast.Term)
|
||||
|
||||
|
||||
class FTLEntity(VCSTranslation):
|
||||
|
@ -74,12 +75,9 @@ class FTLResource(ParsedResource):
|
|||
else:
|
||||
raise
|
||||
|
||||
def get_comment(obj):
|
||||
return [obj.comment.content] if obj.comment else []
|
||||
|
||||
section_comment = None
|
||||
group_comment = []
|
||||
for obj in self.structure.body:
|
||||
if type(obj) == ast.Message:
|
||||
if isinstance(obj, localizable_entries):
|
||||
key = obj.id.name
|
||||
|
||||
# Do not store translation comments in the database
|
||||
|
@ -87,19 +85,20 @@ class FTLResource(ParsedResource):
|
|||
obj.comment = None
|
||||
|
||||
translation = serializer.serialize_entry(obj)
|
||||
comment = [obj.comment.content] if obj.comment else []
|
||||
|
||||
self.entities[key] = FTLEntity(
|
||||
key,
|
||||
translation,
|
||||
'',
|
||||
{None: translation},
|
||||
(section_comment or []) + get_comment(obj),
|
||||
group_comment + comment,
|
||||
self.order
|
||||
)
|
||||
self.order += 1
|
||||
|
||||
elif type(obj) == ast.Section:
|
||||
section_comment = get_comment(obj)
|
||||
elif isinstance(obj, ast.GroupComment):
|
||||
group_comment = [obj.content]
|
||||
|
||||
@property
|
||||
def translations(self):
|
||||
|
@ -122,7 +121,7 @@ class FTLResource(ParsedResource):
|
|||
|
||||
# Use list() to iterate over a copy, leaving original free to modify
|
||||
for obj in list(entities):
|
||||
if type(obj) == ast.Message:
|
||||
if isinstance(obj, localizable_entries):
|
||||
index = entities.index(obj)
|
||||
entity = self.entities[obj.id.name]
|
||||
|
||||
|
|
|
@ -67,6 +67,8 @@ django-webpack-loader==0.5.0 \
|
|||
factory-boy==2.5.2 \
|
||||
--hash=sha256:102c8141511443df01d354610d3b268924100654316709b43ac04648b50bf703 \
|
||||
--hash=sha256:cd8306e64c3a115deca136685e945db88ffe171382012ec938ed241a20dd7eba
|
||||
fluent==0.6.2 \
|
||||
--hash=sha256:b7b5a12f592fad4ed5347c6fef481ea4143b6aeee00ddad0b1355e678e0de27b
|
||||
gunicorn==19.3.0 \
|
||||
--hash=sha256:a5179cde922d2b4e045ee5b11e87323ee77d363ae40294fc8252f25d6a0eaf06 \
|
||||
--hash=sha256:8bc835082882ad9a012cd790c460011e4d96bf3512d98a04d3dabbe45393a089
|
||||
|
@ -110,8 +112,6 @@ whitenoise==1.0.6 \
|
|||
--hash=sha256:dac9419db3ece27bb53c7433243fc22ed42cf68de9d6c4129390e1d9aefe6310
|
||||
|
||||
# Dependencies loaded from outside pypi.
|
||||
https://github.com/projectfluent/python-fluent/archive/c7819dd.zip#egg=fluent==0.4.2 \
|
||||
--hash=sha256:3d479bf226ebeaa5419842cd58c9324217fb1600b2cf94c62b339775a8ace14d
|
||||
https://github.com/mathjazz/silme/archive/v0.9.3.zip#egg=silme==0.9.3 \
|
||||
--hash=sha256:9aa618f068ed71ecc8820dd7538aa39e499ed4ce61a0074edbcfcf1008da1d1c
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче