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:
Matjaž Horvat 2018-02-13 17:38:43 +01:00 коммит произвёл GitHub
Родитель eb4fd72aa4
Коммит b76c53a78e
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
6 изменённых файлов: 912 добавлений и 618 удалений

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

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