--- a/third_party/python/compare-locales/compare_locales/__init__.py
+++ b/third_party/python/compare-locales/compare_locales/__init__.py
@@ -1,1 +1,1 @@
-version = "2.1"
+version = "2.5.1"
--- a/third_party/python/compare-locales/compare_locales/checks.py
+++ b/third_party/python/compare-locales/compare_locales/checks.py
@@ -208,17 +208,18 @@ class DTDChecker(Checker):
if self.extra_tests is not None and 'android-dtd' in self.extra_tests:
self.processContent = True
self.__known_entities = None
def known_entities(self, refValue):
if self.__known_entities is None and self.reference is not None:
self.__known_entities = set()
for ent in self.reference:
- self.__known_entities.update(self.entities_for_value(ent.val))
+ self.__known_entities.update(
+ self.entities_for_value(ent.raw_val))
return self.__known_entities if self.__known_entities is not None \
else self.entities_for_value(refValue)
def entities_for_value(self, value):
reflist = set(m.group(1).encode('utf-8')
for m in self.eref.finditer(value))
reflist -= self.xmllist
return reflist
@@ -243,17 +244,17 @@ class DTDChecker(Checker):
{'spec': spec.pattern})
def check(self, refEnt, l10nEnt):
"""Try to parse the refvalue inside a dummy element, and keep
track of entities that we need to define to make that work.
Return a checker that offers just those entities.
"""
- refValue, l10nValue = refEnt.val, l10nEnt.val
+ refValue, l10nValue = refEnt.raw_val, l10nEnt.raw_val
# find entities the refValue references,
# reusing markup from DTDParser.
reflist = self.known_entities(refValue)
inContext = self.entities_for_value(refValue)
entities = ''.join('<!ENTITY %s "">' % s for s in sorted(reflist))
parser = sax.make_parser()
parser.setFeature(sax.handler.feature_external_ges, False)
@@ -429,65 +430,63 @@ class DTDChecker(Checker):
yield ('error', m.end(0)+offset, msg, 'android')
class FluentChecker(Checker):
'''Tests to run on Fluent (FTL) files.
'''
pattern = re.compile('.*\.ftl')
- # Positions yielded by FluentChecker.check are absolute offsets from the
- # beginning of the file. This is different from the base Checker behavior
- # which yields offsets from the beginning of the current entity's value.
def check(self, refEnt, l10nEnt):
ref_entry = refEnt.entry
l10n_entry = l10nEnt.entry
# verify that values match, either both have a value or none
if ref_entry.value is not None and l10n_entry.value is None:
- yield ('error', l10n_entry.span.start,
- 'Missing value', 'fluent')
+ yield ('error', 0, 'Missing value', 'fluent')
if ref_entry.value is None and l10n_entry.value is not None:
- yield ('error', l10n_entry.value.span.start,
- 'Obsolete value', 'fluent')
+ offset = l10n_entry.value.span.start - l10n_entry.span.start
+ yield ('error', offset, 'Obsolete value', 'fluent')
# verify that we're having the same set of attributes
ref_attr_names = set((attr.id.name for attr in ref_entry.attributes))
ref_pos = dict((attr.id.name, i)
for i, attr in enumerate(ref_entry.attributes))
l10n_attr_counts = \
Counter(attr.id.name for attr in l10n_entry.attributes)
l10n_attr_names = set(l10n_attr_counts)
l10n_pos = dict((attr.id.name, i)
for i, attr in enumerate(l10n_entry.attributes))
# check for duplicate Attributes
# only warn to not trigger a merge skip
for attr_name, cnt in l10n_attr_counts.items():
if cnt > 1:
+ offset = (
+ l10n_entry.attributes[l10n_pos[attr_name]].span.start
+ - l10n_entry.span.start)
yield (
'warning',
- l10n_entry.attributes[l10n_pos[attr_name]].span.start,
+ offset,
'Attribute "{}" occurs {} times'.format(
attr_name, cnt),
'fluent')
missing_attr_names = sorted(ref_attr_names - l10n_attr_names,
key=lambda k: ref_pos[k])
for attr_name in missing_attr_names:
- yield ('error', l10n_entry.span.start,
- 'Missing attribute: ' + attr_name, 'fluent')
+ yield ('error', 0, 'Missing attribute: ' + attr_name, 'fluent')
obsolete_attr_names = sorted(l10n_attr_names - ref_attr_names,
key=lambda k: l10n_pos[k])
obsolete_attrs = [
attr
for attr in l10n_entry.attributes
if attr.id.name in obsolete_attr_names
]
for attr in obsolete_attrs:
- yield ('error', attr.span.start,
+ yield ('error', attr.span.start - l10n_entry.span.start,
'Obsolete attribute: ' + attr.id.name, 'fluent')
def getChecker(file, extra_tests=None):
if PropertiesChecker.use(file):
return PropertiesChecker(extra_tests)
if DTDChecker.use(file):
return DTDChecker(extra_tests)
--- a/third_party/python/compare-locales/compare_locales/compare.py
+++ b/third_party/python/compare-locales/compare_locales/compare.py
@@ -5,20 +5,17 @@
'Mozilla l10n compare locales tool'
import codecs
import os
import shutil
import re
from collections import defaultdict
-try:
- from json import dumps
-except:
- from simplejson import dumps
+from json import dumps
from compare_locales import parser
from compare_locales import paths, mozpath
from compare_locales.checks import getChecker
class Tree(object):
def __init__(self, valuetype):
new file mode 100644
--- /dev/null
+++ b/third_party/python/compare-locales/compare_locales/merge.py
@@ -0,0 +1,115 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+'Merge resources across channels.'
+
+from collections import OrderedDict, defaultdict
+from codecs import encode
+
+
+from compare_locales import parser as cl
+from compare_locales.compare import AddRemove
+
+
+class MergeNotSupportedError(ValueError):
+ pass
+
+
+def merge_channels(name, *resources):
+ try:
+ parser = cl.getParser(name)
+ except UserWarning:
+ raise MergeNotSupportedError(
+ 'Unsupported file format ({}).'.format(name))
+
+ # A map of comments to the keys of entities they belong to.
+ comments = {}
+
+ def parse_resource(resource):
+ # The counter dict keeps track of number of identical comments.
+ counter = defaultdict(int)
+ parser.readContents(resource)
+ pairs = [get_key_value(entity, counter) for entity in parser.walk()]
+ return OrderedDict(pairs)
+
+ def get_key_value(entity, counter):
+ if isinstance(entity, cl.Comment):
+ counter[entity.all] += 1
+ # Use the (value, index) tuple as the key. AddRemove will
+ # de-deplicate identical comments at the same index.
+ return ((entity.all, counter[entity.all]), entity)
+
+ if isinstance(entity, cl.Whitespace):
+ # Use the Whitespace instance as the key so that it's always
+ # unique. Adjecent whitespace will be folded into the longer one in
+ # prune.
+ return (entity, entity)
+
+ # When comments change, AddRemove gives us one 'add' and one 'delete'
+ # (because a comment's key is its content). In merge_two we'll try to
+ # de-duplicate comments by looking at the entity they belong to. Set
+ # up the back-reference from the comment to its entity here.
+ if isinstance(entity, cl.Entity) and entity.pre_comment:
+ comments[entity.pre_comment] = entity.key
+
+ return (entity.key, entity)
+
+ entities = reduce(
+ lambda x, y: merge_two(comments, x, y),
+ map(parse_resource, resources))
+
+ return encode(serialize_legacy_resource(entities), parser.encoding)
+
+
+def merge_two(comments, newer, older):
+ diff = AddRemove()
+ diff.set_left(newer.keys())
+ diff.set_right(older.keys())
+
+ def get_entity(key):
+ entity = newer.get(key, None)
+
+ # Always prefer the newer version.
+ if entity is not None:
+ return entity
+
+ entity = older.get(key)
+
+ # If it's an old comment attached to an entity, try to find that
+ # entity in newer and return None to use its comment instead in prune.
+ if isinstance(entity, cl.Comment) and entity in comments:
+ next_entity = newer.get(comments[entity], None)
+ if next_entity is not None and next_entity.pre_comment:
+ # We'll prune this before returning the merged result.
+ return None
+
+ return entity
+
+ # Create a flat sequence of all entities in order reported by AddRemove.
+ contents = [(key, get_entity(key)) for _, key in diff]
+
+ def prune(acc, cur):
+ _, entity = cur
+ if entity is None:
+ # Prune Nones which stand for duplicated comments.
+ return acc
+
+ if len(acc) and isinstance(entity, cl.Whitespace):
+ _, prev_entity = acc[-1]
+
+ if isinstance(prev_entity, cl.Whitespace):
+ # Prefer the longer whitespace.
+ if len(entity.all) > len(prev_entity.all):
+ acc[-1] = (entity, entity)
+ return acc
+
+ acc.append(cur)
+ return acc
+
+ pruned = reduce(prune, contents, [])
+ return OrderedDict(pruned)
+
+
+def serialize_legacy_resource(entities):
+ return "".join((entity.all for entity in entities.values()))
--- a/third_party/python/compare-locales/compare_locales/parser.py
+++ b/third_party/python/compare-locales/compare_locales/parser.py
@@ -3,16 +3,23 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
import re
import bisect
import codecs
from collections import Counter
import logging
+try:
+ from html import unescape as html_unescape
+except ImportError:
+ from HTMLParser import HTMLParser
+ html_parser = HTMLParser()
+ html_unescape = html_parser.unescape
+
from fluent.syntax import FluentParser as FTLParser
from fluent.syntax import ast as ftl
__constructors = []
# The allowed capabilities for the Parsers. They define the exact strategy
# used by ContentComparer.merge.
@@ -27,92 +34,75 @@ CAN_SKIP = 2
CAN_MERGE = 4
class EntityBase(object):
'''
Abstraction layer for a localizable entity.
Currently supported are grammars of the form:
- 1: pre white space
- 2: entity definition
- 3: entity key (name)
- 4: entity value
- 5: post white space
- <--[1]
+ 1: entity definition
+ 2: entity key (name)
+ 3: entity value
+
<!ENTITY key "value">
- <-------[2]--------->
+ <--- definition ---->
'''
- def __init__(self, ctx, pre_comment,
- span, pre_ws_span, def_span,
- key_span, val_span, post_span):
+ def __init__(self, ctx, pre_comment, span, key_span, val_span):
self.ctx = ctx
self.span = span
- self.pre_ws_span = pre_ws_span
- self.def_span = def_span
self.key_span = key_span
self.val_span = val_span
- self.post_span = post_span
self.pre_comment = pre_comment
- pass
def position(self, offset=0):
"""Get the 1-based line and column of the character
with given offset into the Entity.
If offset is negative, return the end of the Entity.
"""
if offset < 0:
pos = self.span[1]
else:
pos = self.span[0] + offset
- return self.ctx.lines(pos)[0]
+ return self.ctx.linecol(pos)
def value_position(self, offset=0):
"""Get the 1-based line and column of the character
with given offset into the value.
If offset is negative, return the end of the value.
"""
+ assert self.val_span is not None
if offset < 0:
pos = self.val_span[1]
else:
pos = self.val_span[0] + offset
- return self.ctx.lines(pos)[0]
+ return self.ctx.linecol(pos)
# getter helpers
def get_all(self):
return self.ctx.contents[self.span[0]:self.span[1]]
- def get_pre_ws(self):
- return self.ctx.contents[self.pre_ws_span[0]:self.pre_ws_span[1]]
-
- def get_def(self):
- return self.ctx.contents[self.def_span[0]:self.def_span[1]]
-
def get_key(self):
return self.ctx.contents[self.key_span[0]:self.key_span[1]]
def get_raw_val(self):
+ if self.val_span is None:
+ return None
return self.ctx.contents[self.val_span[0]:self.val_span[1]]
- def get_post(self):
- return self.ctx.contents[self.post_span[0]:self.post_span[1]]
-
# getters
all = property(get_all)
- pre_ws = property(get_pre_ws)
- definition = property(get_def)
key = property(get_key)
val = property(get_raw_val)
raw_val = property(get_raw_val)
- post = property(get_post)
def __repr__(self):
return self.key
re_br = re.compile('<br\s*/?>', re.U)
re_sgml = re.compile('</?\w+.*?>', re.U | re.M)
def count_words(self):
@@ -127,111 +117,104 @@ class EntityBase(object):
return self.key == other.key and self.val == other.val
class Entity(EntityBase):
pass
class Comment(EntityBase):
- def __init__(self, ctx, span, pre_ws_span, def_span,
- post_span):
+ def __init__(self, ctx, span):
self.ctx = ctx
self.span = span
- self.pre_ws_span = pre_ws_span
- self.def_span = def_span
- self.post_span = post_span
+ self.val_span = None
@property
def key(self):
return None
- @property
- def val(self):
- return None
-
def __repr__(self):
return self.all
class Junk(object):
'''
An almost-Entity, representing junk data that we didn't parse.
This way, we can signal bad content as stuff we don't understand.
And the either fix that, or report real bugs in localizations.
'''
junkid = 0
def __init__(self, ctx, span):
self.ctx = ctx
self.span = span
- self.pre_ws = self.definition = self.post = ''
self.__class__.junkid += 1
self.key = '_junk_%d_%d-%d' % (self.__class__.junkid, span[0], span[1])
def position(self, offset=0):
"""Get the 1-based line and column of the character
with given offset into the Entity.
If offset is negative, return the end of the Entity.
"""
if offset < 0:
pos = self.span[1]
else:
pos = self.span[0] + offset
- return self.ctx.lines(pos)[0]
+ return self.ctx.linecol(pos)
# getter helpers
def get_all(self):
return self.ctx.contents[self.span[0]:self.span[1]]
# getters
all = property(get_all)
+ raw_val = property(get_all)
val = property(get_all)
def __repr__(self):
return self.key
class Whitespace(EntityBase):
'''Entity-like object representing an empty file with whitespace,
if allowed
'''
def __init__(self, ctx, span):
self.ctx = ctx
- self.key_span = self.val_span = self.span = span
- self.def_span = self.pre_ws_span = (span[0], span[0])
- self.post_span = (span[1], span[1])
+ self.span = self.key_span = self.val_span = span
def __repr__(self):
return self.raw_val
class Parser(object):
capabilities = CAN_SKIP | CAN_MERGE
- tail = re.compile('\s+\Z')
+ reWhitespace = re.compile('\s+', re.M)
class Context(object):
"Fixture for content and line numbers"
def __init__(self, contents):
self.contents = contents
+ # Subclasses may use bitmasks to keep state.
+ self.state = 0
self._lines = None
- def lines(self, *positions):
- # return line and column tuples, 1-based
+ def linecol(self, position):
+ "Returns 1-based line and column numbers."
if self._lines is None:
nl = re.compile('\n', re.M)
self._lines = [m.end()
for m in nl.finditer(self.contents)]
- line_nrs = [bisect.bisect(self._lines, p) for p in positions]
- # compute columns
- pos_ = [
- (1 + line, 1 + p - (self._lines[line-1] if line else 0))
- for line, p in zip(line_nrs, positions)]
- return pos_
+
+ line_offset = bisect.bisect(self._lines, position)
+ line_start = self._lines[line_offset - 1] if line_offset else 0
+ col_offset = position - line_start
+
+ return line_offset + 1, col_offset + 1
def __init__(self):
if not hasattr(self, 'encoding'):
self.encoding = 'utf-8'
self.ctx = None
self.last_comment = None
def readFile(self, file):
@@ -246,106 +229,98 @@ class Parser(object):
'''Read contents and create parsing context.
contents are in native encoding, but with normalized line endings.
'''
(contents, length) = codecs.getdecoder(self.encoding)(contents)
self.ctx = Parser.Context(contents)
def parse(self):
- l = []
- m = {}
- for e in self:
- m[e.key] = len(l)
- l.append(e)
- return (l, m)
+ list_ = list(self)
+ map_ = dict((e.key, i) for i, e in enumerate(list_))
+ return (list_, map_)
def __iter__(self):
- return self.walk(onlyEntities=True)
+ return self.walk(only_localizable=True)
- def walk(self, onlyEntities=False):
+ def walk(self, only_localizable=False):
if not self.ctx:
# loading file failed, or we just didn't load anything
return
ctx = self.ctx
contents = ctx.contents
- offset = 0
- entity, offset = self.getEntity(ctx, offset)
- while entity:
- if (not onlyEntities or
- isinstance(entity, Entity) or
- type(entity) is Junk):
+
+ next_offset = 0
+ while next_offset < len(contents):
+ entity = self.getNext(ctx, next_offset)
+
+ if isinstance(entity, (Entity, Junk)):
yield entity
- entity, offset = self.getEntity(ctx, offset)
- if len(contents) > offset:
- yield Junk(ctx, (offset, len(contents)))
+ elif not only_localizable:
+ yield entity
+
+ next_offset = entity.span[1]
- def getEntity(self, ctx, offset):
+ def getNext(self, ctx, offset):
+ m = self.reWhitespace.match(ctx.contents, offset)
+ if m:
+ return Whitespace(ctx, m.span())
m = self.reKey.match(ctx.contents, offset)
if m:
- offset = m.end()
- entity = self.createEntity(ctx, m)
- return (entity, offset)
+ return self.createEntity(ctx, m)
m = self.reComment.match(ctx.contents, offset)
if m:
- offset = m.end()
- self.last_comment = Comment(ctx, *[m.span(i) for i in xrange(4)])
- return (self.last_comment, offset)
- return self.getTrailing(ctx, offset, self.reKey, self.reComment)
+ self.last_comment = Comment(ctx, m.span())
+ return self.last_comment
+ return self.getJunk(ctx, offset, self.reKey, self.reComment)
- def getTrailing(self, ctx, offset, *expressions):
+ def getJunk(self, ctx, offset, *expressions):
junkend = None
for exp in expressions:
m = exp.search(ctx.contents, offset)
if m:
junkend = min(junkend, m.start()) if junkend else m.start()
- if junkend is None:
- if self.tail.match(ctx.contents, offset):
- white_end = len(ctx.contents)
- return (Whitespace(ctx, (offset, white_end)), white_end)
- else:
- return (None, offset)
- return (Junk(ctx, (offset, junkend)), junkend)
+ return Junk(ctx, (offset, junkend or len(ctx.contents)))
def createEntity(self, ctx, m):
pre_comment = self.last_comment
self.last_comment = None
- return Entity(ctx, pre_comment,
- *[m.span(i) for i in xrange(6)])
+ return Entity(ctx, pre_comment, m.span(), m.span('key'), m.span('val'))
@classmethod
def findDuplicates(cls, entities):
found = Counter(entity.key for entity in entities)
for entity_id, cnt in found.items():
if cnt > 1:
yield '{} occurs {} times'.format(entity_id, cnt)
def getParser(path):
for item in __constructors:
if re.search(item[0], path):
return item[1]
raise UserWarning("Cannot find Parser")
-# Subgroups of the match will:
-# 1: pre white space
-# 2: pre comments
-# 3: entity definition
-# 4: entity key (name)
-# 5: entity value
-# 6: post comment (and white space) in the same line (dtd only)
-# <--[1]
-# <!-- pre comments --> <--[2]
-# <!ENTITY key "value"> <!-- comment -->
-#
-# <-------[3]---------><------[6]------>
+class DTDEntity(Entity):
+ @property
+ def val(self):
+ '''Unescape HTML entities into corresponding Unicode characters.
+
+ Named (&), decimal (&), and hex (& and &) formats
+ are supported. Unknown entities are left intact.
+ As of Python 2.7 and Python 3.6 the following 252 named entities are
+ recognized and unescaped:
-class DTDEntity(Entity):
+ https://github.com/python/cpython/blob/2.7/Lib/htmlentitydefs.py
+ https://github.com/python/cpython/blob/3.6/Lib/html/entities.py
+ '''
+ return html_unescape(self.raw_val)
+
def value_position(self, offset=0):
# DTDChecker already returns tuples of (line, col) positions
if isinstance(offset, tuple):
line_pos, col_pos = offset
line, col = super(DTDEntity, self).value_position()
if line_pos == 1:
col = col + col_pos
else:
@@ -369,58 +344,54 @@ class DTDParser(Parser):
u'\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F' + \
u'\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD'
# + \U00010000-\U000EFFFF seems to be unsupported in python
# NameChar ::= NameStartChar | "-" | "." | [0-9] | #xB7 |
# [#x0300-#x036F] | [#x203F-#x2040]
NameChar = NameStartChar + ur'\-\.0-9' + u'\xB7\u0300-\u036F\u203F-\u2040'
Name = '[' + NameStartChar + '][' + NameChar + ']*'
- reKey = re.compile('(?:(?P<pre>\s*)(?P<entity><!ENTITY\s+(?P<key>' + Name +
- ')\s+(?P<val>\"[^\"]*\"|\'[^\']*\'?)\s*>)'
- '(?P<post>\s+)?)',
+ reKey = re.compile('<!ENTITY\s+(?P<key>' + Name + ')\s+'
+ '(?P<val>\"[^\"]*\"|\'[^\']*\'?)\s*>',
re.DOTALL | re.M)
# add BOM to DTDs, details in bug 435002
reHeader = re.compile(u'^\ufeff')
- reComment = re.compile('(\s*)(<!--(-?[%s])*?-->)(\s*)' % CharMinusDash,
+ reComment = re.compile('<!--(?P<val>-?[%s])*?-->' % CharMinusDash,
re.S)
- rePE = re.compile(u'(?:(\s*)'
- u'(<!ENTITY\s+%\s+(' + Name +
- u')\s+SYSTEM\s+(\"[^\"]*\"|\'[^\']*\')\s*>\s*%' + Name +
- u';)([ \t]*(?:' + XmlComment + u'\s*)*\n?)?)')
+ rePE = re.compile(u'<!ENTITY\s+%\s+(?P<key>' + Name + ')\s+'
+ u'SYSTEM\s+(?P<val>\"[^\"]*\"|\'[^\']*\')\s*>\s*'
+ u'%' + Name + ';'
+ u'(?:[ \t]*(?:' + XmlComment + u'\s*)*\n?)?')
- def getEntity(self, ctx, offset):
+ def getNext(self, ctx, offset):
'''
- Overload Parser.getEntity to special-case ParsedEntities.
+ Overload Parser.getNext to special-case ParsedEntities.
Just check for a parsed entity if that method claims junk.
<!ENTITY % foo SYSTEM "url">
%foo;
'''
if offset is 0 and self.reHeader.match(ctx.contents):
offset += 1
- entity, inneroffset = Parser.getEntity(self, ctx, offset)
+ entity = Parser.getNext(self, ctx, offset)
if (entity and isinstance(entity, Junk)) or entity is None:
m = self.rePE.match(ctx.contents, offset)
if m:
- inneroffset = m.end()
self.last_comment = None
- entity = DTDEntity(ctx, '', *[m.span(i) for i in xrange(6)])
- return (entity, inneroffset)
+ entity = DTDEntity(
+ ctx, '', m.span(), m.span('key'), m.span('val'))
+ return entity
def createEntity(self, ctx, m):
valspan = m.span('val')
valspan = (valspan[0]+1, valspan[1]-1)
pre_comment = self.last_comment
self.last_comment = None
return DTDEntity(ctx, pre_comment,
- m.span(),
- m.span('pre'),
- m.span('entity'), m.span('key'), valspan,
- m.span('post'))
+ m.span(), m.span('key'), valspan)
class PropertiesEntity(Entity):
escape = re.compile(r'\\((?P<uni>u[0-9a-fA-F]{1,4})|'
'(?P<nl>\n\s*)|(?P<single>.))', re.M)
known_escapes = {'n': '\n', 'r': '\r', 't': '\t', '\\': '\\'}
@property
@@ -433,38 +404,36 @@ class PropertiesEntity(Entity):
return ''
return self.known_escapes.get(found['single'], found['single'])
return self.escape.sub(unescape, self.raw_val)
class PropertiesParser(Parser):
def __init__(self):
- self.reKey = re.compile('^(\s*)'
- '([^#!\s\n][^=:\n]*?)\s*[:=][ \t]*', re.M)
- self.reComment = re.compile('(\s*)(((?:[#!][^\n]*\n?)+))', re.M)
+ self.reKey = re.compile(
+ '(?P<key>[^#!\s\n][^=:\n]*?)\s*[:=][ \t]*', re.M)
+ self.reComment = re.compile('(?:[#!][^\n]*\n)*(?:[#!][^\n]*)', re.M)
self._escapedEnd = re.compile(r'\\+$')
self._trailingWS = re.compile(r'\s*(?:\n|\Z)', re.M)
Parser.__init__(self)
- def getEntity(self, ctx, offset):
+ def getNext(self, ctx, offset):
# overwritten to parse values line by line
contents = ctx.contents
+
+ m = self.reWhitespace.match(contents, offset)
+ if m:
+ return Whitespace(ctx, m.span())
+
m = self.reComment.match(contents, offset)
if m:
- spans = [m.span(i) for i in xrange(3)]
- start_trailing = offset = m.end()
- while offset < len(contents):
- m = self._trailingWS.match(contents, offset)
- if not m:
- break
- offset = m.end()
- spans.append((start_trailing, offset))
- self.last_comment = Comment(ctx, *spans)
- return (self.last_comment, offset)
+ self.last_comment = Comment(ctx, m.span())
+ return self.last_comment
+
m = self.reKey.match(contents, offset)
if m:
startline = offset = m.end()
while True:
endval = nextline = contents.find('\n', offset)
if nextline == -1:
endval = offset = len(contents)
break
@@ -472,147 +441,142 @@ class PropertiesParser(Parser):
_e = self._escapedEnd.search(contents, offset, nextline)
offset = nextline + 1
if _e is None:
break
# backslashes at end of line, if 2*n, not escaped
if len(_e.group()) % 2 == 0:
break
startline = offset
+
# strip trailing whitespace
ws = self._trailingWS.search(contents, startline)
if ws:
endval = ws.start()
- offset = ws.end()
+
pre_comment = self.last_comment
self.last_comment = None
entity = PropertiesEntity(
ctx, pre_comment,
- (m.start(), offset), # full span
- m.span(1), # leading whitespan
- (m.start(2), offset), # entity def span
- m.span(2), # key span
- (m.end(), endval), # value span
- (offset, offset)) # post comment span, empty
- return (entity, offset)
- return self.getTrailing(ctx, offset, self.reKey, self.reComment)
+ (m.start(), endval), # full span
+ m.span('key'),
+ (m.end(), endval)) # value span
+ return entity
+
+ return self.getJunk(ctx, offset, self.reKey, self.reComment)
class DefinesInstruction(EntityBase):
'''Entity-like object representing processing instructions in inc files
'''
- def __init__(self, ctx, span, pre_ws_span, def_span, val_span, post_span):
+ def __init__(self, ctx, span, val_span):
self.ctx = ctx
self.span = span
- self.pre_ws_span = pre_ws_span
- self.def_span = def_span
self.key_span = self.val_span = val_span
- self.post_span = post_span
def __repr__(self):
return self.raw_val
class DefinesParser(Parser):
# can't merge, #unfilter needs to be the last item, which we don't support
capabilities = CAN_COPY
- tail = re.compile(r'(?!)') # never match
+ reWhitespace = re.compile('\n+', re.M)
+
+ EMPTY_LINES = 1 << 0
+ PAST_FIRST_LINE = 1 << 1
def __init__(self):
- self.reComment = re.compile(
- '((?:[ \t]*\n)*)'
- '((?:^# .*?(?:\n|\Z))+)'
- '((?:[ \t]*(?:\n|\Z))*)', re.M)
- self.reKey = re.compile('((?:[ \t]*\n)*)'
- '(#define[ \t]+(\w+)(?:[ \t](.*?))?(?:\n|\Z))'
- '((?:[ \t]*(?:\n|\Z))*)',
- re.M)
- self.rePI = re.compile('((?:[ \t]*\n)*)'
- '(#(\w+)[ \t]+(.*?)(?:\n|\Z))'
- '((?:[ \t]*(?:\n|\Z))*)',
- re.M)
+ self.reComment = re.compile('(?:^# .*?\n)*(?:^# [^\n]*)', re.M)
+ # corresponds to
+ # https://hg.mozilla.org/mozilla-central/file/72ee4800d4156931c89b58bd807af4a3083702bb/python/mozbuild/mozbuild/preprocessor.py#l561 # noqa
+ self.reKey = re.compile(
+ '#define[ \t]+(?P<key>\w+)(?:[ \t](?P<val>[^\n]*))?', re.M)
+ self.rePI = re.compile('#(?P<val>\w+[ \t]+[^\n]+)', re.M)
Parser.__init__(self)
- def getEntity(self, ctx, offset):
+ def getNext(self, ctx, offset):
contents = ctx.contents
+
+ m = self.reWhitespace.match(contents, offset)
+ if m:
+ if ctx.state & self.EMPTY_LINES:
+ return Whitespace(ctx, m.span())
+ if ctx.state & self.PAST_FIRST_LINE and len(m.group()) == 1:
+ return Whitespace(ctx, m.span())
+ else:
+ return Junk(ctx, m.span())
+
+ # We're not in the first line anymore.
+ ctx.state |= self.PAST_FIRST_LINE
+
m = self.reComment.match(contents, offset)
if m:
- offset = m.end()
- self.last_comment = Comment(ctx, *[m.span(i) for i in xrange(4)])
- return (self.last_comment, offset)
+ self.last_comment = Comment(ctx, m.span())
+ return self.last_comment
m = self.reKey.match(contents, offset)
if m:
- offset = m.end()
- return (self.createEntity(ctx, m), offset)
+ return self.createEntity(ctx, m)
m = self.rePI.match(contents, offset)
if m:
- offset = m.end()
- return (DefinesInstruction(ctx, *[m.span(i) for i in xrange(5)]),
- offset)
- return self.getTrailing(ctx, offset,
- self.reComment, self.reKey, self.rePI)
+ instr = DefinesInstruction(ctx, m.span(), m.span('val'))
+ if instr.val == 'filter emptyLines':
+ ctx.state |= self.EMPTY_LINES
+ if instr.val == 'unfilter emptyLines':
+ ctx.state &= ~ self.EMPTY_LINES
+ return instr
+ return self.getJunk(
+ ctx, offset, self.reComment, self.reKey, self.rePI)
class IniSection(EntityBase):
'''Entity-like object representing sections in ini files
'''
- def __init__(self, ctx, span, pre_ws_span, def_span, val_span, post_span):
+ def __init__(self, ctx, span, val_span):
self.ctx = ctx
self.span = span
- self.pre_ws_span = pre_ws_span
- self.def_span = def_span
self.key_span = self.val_span = val_span
- self.post_span = post_span
def __repr__(self):
return self.raw_val
class IniParser(Parser):
'''
Parse files of the form:
# initial comment
[cat]
whitespace*
#comment
string=value
...
'''
def __init__(self):
- self.reComment = re.compile(
- '((?:[ \t]*\n)*)'
- '((?:^[;#].*?(?:\n|\Z))+)'
- '((?:[ \t]*(?:\n|\Z))*)', re.M)
- self.reSection = re.compile(
- '((?:[ \t]*\n)*)'
- '(\[(.*?)\])'
- '((?:[ \t]*(?:\n|\Z))*)', re.M)
- self.reKey = re.compile(
- '((?:[ \t]*\n)*)'
- '((.+?)=(.*))'
- '((?:[ \t]*(?:\n|\Z))*)', re.M)
+ self.reComment = re.compile('(?:^[;#][^\n]*\n)*(?:^[;#][^\n]*)', re.M)
+ self.reSection = re.compile('\[(?P<val>.*?)\]', re.M)
+ self.reKey = re.compile('(?P<key>.+?)=(?P<val>.*)', re.M)
Parser.__init__(self)
- def getEntity(self, ctx, offset):
+ def getNext(self, ctx, offset):
contents = ctx.contents
+ m = self.reWhitespace.match(contents, offset)
+ if m:
+ return Whitespace(ctx, m.span())
m = self.reComment.match(contents, offset)
if m:
- offset = m.end()
- self.last_comment = Comment(ctx, *[m.span(i) for i in xrange(4)])
- return (self.last_comment, offset)
+ self.last_comment = Comment(ctx, m.span())
+ return self.last_comment
m = self.reSection.match(contents, offset)
if m:
- offset = m.end()
- return (IniSection(ctx, *[m.span(i) for i in xrange(5)]), offset)
+ return IniSection(ctx, m.span(), m.span('val'))
m = self.reKey.match(contents, offset)
if m:
- offset = m.end()
- return (self.createEntity(ctx, m), offset)
- return self.getTrailing(ctx, offset,
- self.reComment, self.reSection, self.reKey)
+ return self.createEntity(ctx, m)
+ return self.getJunk(
+ ctx, offset, self.reComment, self.reSection, self.reKey)
class FluentAttribute(EntityBase):
ignored_fields = ['span']
def __init__(self, entity, attr_node):
self.ctx = entity.ctx
self.attr = attr_node
@@ -637,20 +601,26 @@ class FluentEntity(Entity):
self.ctx = ctx
self.span = (start, end)
self.key_span = (entry.id.span.start, entry.id.span.end)
if entry.value is not None:
self.val_span = (entry.value.span.start, entry.value.span.end)
else:
- self.val_span = (0, 0)
+ self.val_span = None
self.entry = entry
+ # EntityBase instances are expected to have pre_comment. It's used by
+ # other formats to associate a Comment with an Entity. FluentEntities
+ # don't need it because message comments are part of the entry AST and
+ # are not separate Comment instances.
+ self.pre_comment = None
+
_word_count = None
def count_words(self):
if self._word_count is None:
self._word_count = 0
def count_words(node):
if isinstance(node, ftl.TextElement):
@@ -660,58 +630,93 @@ class FluentEntity(Entity):
self.entry.traverse(count_words)
return self._word_count
def equals(self, other):
return self.entry.equals(
other.entry, ignored_fields=self.ignored_fields)
- # Positions yielded by FluentChecker.check are absolute offsets from the
- # beginning of the file. This is different from the base Checker behavior
- # which yields offsets from the beginning of the current entity's value.
- def position(self, pos=None):
- if pos is None:
- pos = self.entry.span.start
- return self.ctx.lines(pos)[0]
-
- # FluentEntities don't differentiate between entity and value positions
- # because all positions are absolute from the beginning of the file.
- def value_position(self, pos=None):
- return self.position(pos)
+ # In Fluent we treat entries as a whole. FluentChecker reports errors at
+ # offsets calculated from the beginning of the entry.
+ def value_position(self, offset=0):
+ return self.position(offset)
@property
def attributes(self):
for attr_node in self.entry.attributes:
yield FluentAttribute(self, attr_node)
+class FluentSection(EntityBase):
+ def __init__(self, ctx, entry):
+ self.entry = entry
+ self.ctx = ctx
+
+ self.span = (entry.span.start, entry.span.end)
+ self.key_span = self.val_span = (
+ entry.name.span.start, entry.name.span.end)
+
+
class FluentParser(Parser):
capabilities = CAN_SKIP
def __init__(self):
super(FluentParser, self).__init__()
self.ftl_parser = FTLParser()
- def walk(self, onlyEntities=False):
+ def walk(self, only_localizable=False):
if not self.ctx:
# loading file failed, or we just didn't load anything
return
+
resource = self.ftl_parser.parse(self.ctx.contents)
+
+ if resource.comment:
+ last_span_end = resource.comment.span.end
+
+ if not only_localizable:
+ if 0 < resource.comment.span.start:
+ yield Whitespace(
+ self.ctx, (0, resource.comment.span.start))
+ yield Comment(
+ self.ctx,
+ (resource.comment.span.start, resource.comment.span.end))
+ else:
+ last_span_end = 0
+
for entry in resource.body:
+ if not only_localizable:
+ if entry.span.start > last_span_end:
+ yield Whitespace(
+ self.ctx, (last_span_end, entry.span.start))
+
if isinstance(entry, ftl.Message):
yield FluentEntity(self.ctx, entry)
elif isinstance(entry, ftl.Junk):
start = entry.span.start
end = entry.span.end
# strip leading whitespace
start += re.match('\s*', entry.content).end()
# strip trailing whitespace
ws, we = re.search('\s*$', entry.content).span()
end -= we - ws
yield Junk(self.ctx, (start, end))
+ elif isinstance(entry, ftl.Comment) and not only_localizable:
+ span = (entry.span.start, entry.span.end)
+ yield Comment(self.ctx, span)
+ elif isinstance(entry, ftl.Section) and not only_localizable:
+ yield FluentSection(self.ctx, entry)
+
+ last_span_end = entry.span.end
+
+ # Yield Whitespace at the EOF.
+ if not only_localizable:
+ eof_offset = len(self.ctx.contents)
+ if eof_offset > last_span_end:
+ yield Whitespace(self.ctx, (last_span_end, eof_offset))
__constructors = [('\\.dtd$', DTDParser()),
('\\.properties$', PropertiesParser()),
('\\.ini$', IniParser()),
('\\.inc$', DefinesParser()),
('\\.ftl$', FluentParser())]
--- a/third_party/python/compare-locales/compare_locales/paths.py
+++ b/third_party/python/compare-locales/compare_locales/paths.py
@@ -130,17 +130,17 @@ class ProjectConfig(object):
Assert that no rules are set.
Also, normalize output already here.
'''
assert not self.rules
def filter_(module, path, entity=None):
try:
rv = filter(module, path, entity=entity)
- except:
+ except BaseException: # we really want to handle EVERYTHING here
return 'error'
rv = {
True: 'error',
False: 'ignore',
'report': 'warning'
}.get(rv, rv)
assert rv in ('error', 'ignore', 'warning', None)
return rv
@@ -429,17 +429,17 @@ class TOMLParser(object):
self.data = None
self.pc = ProjectConfig()
self.pc.PATH = path
def load(self):
try:
with open(self.path, 'rb') as fin:
self.data = toml.load(fin)
- except:
+ except (toml.TomlError, IOError):
raise ConfigNotFound(self.path)
def processEnv(self):
assert self.data is not None
self.pc.add_environment(**self.data.get('env', {}))
def processLocales(self):
assert self.data is not None
@@ -537,35 +537,35 @@ class L10nConfigParser(object):
# optional defaults to be passed to the inner ConfigParser (unused?)
self.defaults = kwargs
def getDepth(self, cp):
'''Get the depth for the comparison from the parsed l10n.ini.
'''
try:
depth = cp.get('general', 'depth')
- except:
+ except (NoSectionError, NoOptionError):
depth = '.'
return depth
def getFilters(self):
'''Get the test functions from this ConfigParser and all children.
Only works with synchronous loads, used by compare-locales, which
is local anyway.
'''
filter_path = mozpath.join(mozpath.dirname(self.inipath), 'filter.py')
try:
- l = {}
- execfile(filter_path, {}, l)
- if 'test' in l and callable(l['test']):
- filters = [l['test']]
+ local = {}
+ execfile(filter_path, {}, local)
+ if 'test' in local and callable(local['test']):
+ filters = [local['test']]
else:
filters = []
- except:
+ except BaseException: # we really want to handle EVERYTHING here
filters = []
for c in self.children:
filters += c.getFilters()
return filters
def loadConfigs(self):
new file mode 100644
--- /dev/null
+++ b/third_party/python/compare-locales/compare_locales/plurals.py
@@ -0,0 +1,160 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+'Mapping of locales to CLDR plural categories as implemented by PluralForm.jsm'
+
+CATEGORIES_BY_INDEX = (
+ # 0 (Chinese)
+ ('other',),
+ # 1 (English)
+ ('one', 'other'),
+ # 2 (French)
+ ('one', 'other'),
+ # 3 (Latvian)
+ ('zero', 'one', 'other'),
+ # 4 (Scottish Gaelic)
+ ('one', 'two', 'few', 'other'),
+ # 5 (Romanian)
+ ('one', 'few', 'other'),
+ # 6 (Lithuanian)
+ # CLDR: one, few, many (fractions), other
+ ('one', 'other', 'few'),
+ # 7 (Russian)
+ # CLDR: one, few, many, other (fractions)
+ ('one', 'few', 'many'),
+ # 8 (Slovak)
+ # CLDR: one, few, many (fractions), other
+ ('one', 'few', 'other'),
+ # 9 (Polish)
+ # CLDR: one, few, many, other (fractions)
+ ('one', 'few', 'many'),
+ # 10 (Slovenian)
+ ('one', 'two', 'few', 'other'),
+ # 11 (Irish Gaelic)
+ ('one', 'two', 'few', 'many', 'other'),
+ # 12 (Arabic)
+ # CLDR: zero, one, two, few, many, other
+ ('one', 'two', 'few', 'many', 'other', 'zero'),
+ # 13 (Maltese)
+ ('one', 'few', 'many', 'other'),
+ # 14 (Macedonian)
+ # CLDR: one, other
+ ('one', 'two', 'other'),
+ # 15 (Icelandic)
+ ('one', 'other'),
+ # 16 (Breton)
+ ('one', 'two', 'few', 'many', 'other'),
+ # 17 (Shuar)
+ # CLDR: (missing)
+ ('zero', 'other')
+)
+
+CATEGORIES_BY_LOCALE = {
+ 'ach': CATEGORIES_BY_INDEX[1],
+ 'af': CATEGORIES_BY_INDEX[1],
+ 'an': CATEGORIES_BY_INDEX[1],
+ 'ar': CATEGORIES_BY_INDEX[12],
+ 'as': CATEGORIES_BY_INDEX[1],
+ 'ast': CATEGORIES_BY_INDEX[1],
+ 'az': CATEGORIES_BY_INDEX[0],
+ 'be': CATEGORIES_BY_INDEX[7],
+ 'bg': CATEGORIES_BY_INDEX[1],
+ 'bn-BD': CATEGORIES_BY_INDEX[1],
+ 'bn-IN': CATEGORIES_BY_INDEX[1],
+ 'br': CATEGORIES_BY_INDEX[1],
+ 'bs': CATEGORIES_BY_INDEX[1],
+ 'ca': CATEGORIES_BY_INDEX[1],
+ 'cak': CATEGORIES_BY_INDEX[1],
+ 'cs': CATEGORIES_BY_INDEX[8],
+ 'cy': CATEGORIES_BY_INDEX[1],
+ 'da': CATEGORIES_BY_INDEX[1],
+ 'de': CATEGORIES_BY_INDEX[1],
+ 'dsb': CATEGORIES_BY_INDEX[10],
+ 'el': CATEGORIES_BY_INDEX[1],
+ 'en-GB': CATEGORIES_BY_INDEX[1],
+ 'en-US': CATEGORIES_BY_INDEX[1],
+ 'en-ZA': CATEGORIES_BY_INDEX[1],
+ 'eo': CATEGORIES_BY_INDEX[1],
+ 'es-AR': CATEGORIES_BY_INDEX[1],
+ 'es-CL': CATEGORIES_BY_INDEX[1],
+ 'es-ES': CATEGORIES_BY_INDEX[1],
+ 'es-MX': CATEGORIES_BY_INDEX[1],
+ 'et': CATEGORIES_BY_INDEX[1],
+ 'eu': CATEGORIES_BY_INDEX[1],
+ 'fa': CATEGORIES_BY_INDEX[0],
+ 'ff': CATEGORIES_BY_INDEX[1],
+ 'fi': CATEGORIES_BY_INDEX[1],
+ 'fr': CATEGORIES_BY_INDEX[2],
+ 'fy-NL': CATEGORIES_BY_INDEX[1],
+ 'ga-IE': CATEGORIES_BY_INDEX[11],
+ 'gd': CATEGORIES_BY_INDEX[4],
+ 'gl': CATEGORIES_BY_INDEX[1],
+ 'gn': CATEGORIES_BY_INDEX[1],
+ 'gu-IN': CATEGORIES_BY_INDEX[2],
+ 'he': CATEGORIES_BY_INDEX[1],
+ 'hi-IN': CATEGORIES_BY_INDEX[1],
+ 'hr': CATEGORIES_BY_INDEX[7],
+ 'hsb': CATEGORIES_BY_INDEX[10],
+ 'hu': CATEGORIES_BY_INDEX[1],
+ 'hy-AM': CATEGORIES_BY_INDEX[1],
+ 'ia': CATEGORIES_BY_INDEX[1],
+ 'id': CATEGORIES_BY_INDEX[0],
+ 'is': CATEGORIES_BY_INDEX[15],
+ 'it': CATEGORIES_BY_INDEX[1],
+ 'ja': CATEGORIES_BY_INDEX[0],
+ 'ja-JP-mac': CATEGORIES_BY_INDEX[0],
+ 'jiv': CATEGORIES_BY_INDEX[17],
+ 'ka': CATEGORIES_BY_INDEX[0],
+ 'kab': CATEGORIES_BY_INDEX[1],
+ 'kk': CATEGORIES_BY_INDEX[1],
+ 'km': CATEGORIES_BY_INDEX[1],
+ 'kn': CATEGORIES_BY_INDEX[1],
+ 'ko': CATEGORIES_BY_INDEX[0],
+ 'lij': CATEGORIES_BY_INDEX[1],
+ 'lo': CATEGORIES_BY_INDEX[0],
+ 'lt': CATEGORIES_BY_INDEX[6],
+ 'ltg': CATEGORIES_BY_INDEX[3],
+ 'lv': CATEGORIES_BY_INDEX[3],
+ 'mai': CATEGORIES_BY_INDEX[1],
+ 'mk': CATEGORIES_BY_INDEX[15],
+ 'ml': CATEGORIES_BY_INDEX[1],
+ 'mr': CATEGORIES_BY_INDEX[1],
+ 'ms': CATEGORIES_BY_INDEX[1],
+ 'my': CATEGORIES_BY_INDEX[1],
+ 'nb-NO': CATEGORIES_BY_INDEX[1],
+ 'ne-NP': CATEGORIES_BY_INDEX[1],
+ 'nl': CATEGORIES_BY_INDEX[1],
+ 'nn-NO': CATEGORIES_BY_INDEX[1],
+ 'oc': CATEGORIES_BY_INDEX[1],
+ 'or': CATEGORIES_BY_INDEX[1],
+ 'pa-IN': CATEGORIES_BY_INDEX[1],
+ 'pl': CATEGORIES_BY_INDEX[9],
+ 'pt-BR': CATEGORIES_BY_INDEX[1],
+ 'pt-PT': CATEGORIES_BY_INDEX[1],
+ 'rm': CATEGORIES_BY_INDEX[1],
+ 'ro': CATEGORIES_BY_INDEX[1],
+ 'ru': CATEGORIES_BY_INDEX[7],
+ 'si': CATEGORIES_BY_INDEX[1],
+ 'sk': CATEGORIES_BY_INDEX[8],
+ 'sl': CATEGORIES_BY_INDEX[10],
+ 'son': CATEGORIES_BY_INDEX[1],
+ 'sq': CATEGORIES_BY_INDEX[1],
+ 'sr': CATEGORIES_BY_INDEX[7],
+ 'sv-SE': CATEGORIES_BY_INDEX[1],
+ 'ta': CATEGORIES_BY_INDEX[1],
+ 'te': CATEGORIES_BY_INDEX[1],
+ 'th': CATEGORIES_BY_INDEX[0],
+ 'tl': CATEGORIES_BY_INDEX[1],
+ 'tr': CATEGORIES_BY_INDEX[0],
+ 'trs': CATEGORIES_BY_INDEX[1],
+ 'uk': CATEGORIES_BY_INDEX[7],
+ 'ur': CATEGORIES_BY_INDEX[1],
+ 'uz': CATEGORIES_BY_INDEX[0],
+ 'vi': CATEGORIES_BY_INDEX[1],
+ 'wo': CATEGORIES_BY_INDEX[0],
+ 'xh': CATEGORIES_BY_INDEX[1],
+ 'zam': CATEGORIES_BY_INDEX[1],
+ 'zh-CN': CATEGORIES_BY_INDEX[1],
+ 'zh-TW': CATEGORIES_BY_INDEX[0]
+}
--- a/third_party/python/compare-locales/compare_locales/tests/test_defines.py
+++ b/third_party/python/compare-locales/compare_locales/tests/test_defines.py
@@ -6,90 +6,186 @@
import unittest
from compare_locales.tests import ParserTestMixin
mpl2 = '''\
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
-# You can obtain one at http://mozilla.org/MPL/2.0/.
-'''
+# You can obtain one at http://mozilla.org/MPL/2.0/.'''
class TestDefinesParser(ParserTestMixin, unittest.TestCase):
filename = 'defines.inc'
def testBrowser(self):
- self._test(mpl2 + '''#filter emptyLines
+ self._test(mpl2 + '''
+#filter emptyLines
#define MOZ_LANGPACK_CREATOR mozilla.org
# If non-English locales wish to credit multiple contributors, uncomment this
# variable definition and use the format specified.
# #define MOZ_LANGPACK_CONTRIBUTORS <em:contributor>Joe Solon</em:contributor>
#unfilter emptyLines
''', (
('Comment', mpl2),
+ ('Whitespace', '\n'),
('DefinesInstruction', 'filter emptyLines'),
+ ('Whitespace', '\n\n'),
('MOZ_LANGPACK_CREATOR', 'mozilla.org'),
+ ('Whitespace', '\n\n'),
('Comment', '#define'),
- ('DefinesInstruction', 'unfilter emptyLines')))
+ ('Whitespace', '\n\n'),
+ ('DefinesInstruction', 'unfilter emptyLines'),
+ ('Junk', '\n\n')))
def testBrowserWithContributors(self):
- self._test(mpl2 + '''#filter emptyLines
+ self._test(mpl2 + '''
+#filter emptyLines
#define MOZ_LANGPACK_CREATOR mozilla.org
# If non-English locales wish to credit multiple contributors, uncomment this
# variable definition and use the format specified.
#define MOZ_LANGPACK_CONTRIBUTORS <em:contributor>Joe Solon</em:contributor>
#unfilter emptyLines
''', (
('Comment', mpl2),
+ ('Whitespace', '\n'),
('DefinesInstruction', 'filter emptyLines'),
+ ('Whitespace', '\n\n'),
('MOZ_LANGPACK_CREATOR', 'mozilla.org'),
+ ('Whitespace', '\n\n'),
('Comment', 'non-English'),
+ ('Whitespace', '\n'),
('MOZ_LANGPACK_CONTRIBUTORS',
'<em:contributor>Joe Solon</em:contributor>'),
- ('DefinesInstruction', 'unfilter emptyLines')))
+ ('Whitespace', '\n\n'),
+ ('DefinesInstruction', 'unfilter emptyLines'),
+ ('Junk', '\n\n')))
def testCommentWithNonAsciiCharacters(self):
- self._test(mpl2 + '''#filter emptyLines
+ self._test(mpl2 + '''
+#filter emptyLines
# e.g. #define seamonkey_l10n <DT><A HREF="urn:foo">SeaMonkey v češtině</a>
#define seamonkey_l10n_long
#unfilter emptyLines
''', (
('Comment', mpl2),
+ ('Whitespace', '\n'),
('DefinesInstruction', 'filter emptyLines'),
+ ('Whitespace', '\n\n'),
('Comment', u'češtině'),
+ ('Whitespace', '\n'),
('seamonkey_l10n_long', ''),
+ ('Whitespace', '\n\n'),
+ ('DefinesInstruction', 'unfilter emptyLines'),
+ ('Junk', '\n\n')))
+
+ def test_no_empty_lines(self):
+ self._test('''#define MOZ_LANGPACK_CREATOR mozilla.org
+#define MOZ_LANGPACK_CREATOR mozilla.org
+''', (
+ ('MOZ_LANGPACK_CREATOR', 'mozilla.org'),
+ ('Whitespace', '\n'),
+ ('MOZ_LANGPACK_CREATOR', 'mozilla.org'),
+ ('Whitespace', '\n')))
+
+ def test_empty_line_between(self):
+ self._test('''#define MOZ_LANGPACK_CREATOR mozilla.org
+
+#define MOZ_LANGPACK_CREATOR mozilla.org
+''', (
+ ('MOZ_LANGPACK_CREATOR', 'mozilla.org'),
+ ('Junk', '\n'),
+ ('MOZ_LANGPACK_CREATOR', 'mozilla.org'),
+ ('Whitespace', '\n')))
+
+ def test_empty_line_at_the_beginning(self):
+ self._test('''
+#define MOZ_LANGPACK_CREATOR mozilla.org
+#define MOZ_LANGPACK_CREATOR mozilla.org
+''', (
+ ('Junk', '\n'),
+ ('MOZ_LANGPACK_CREATOR', 'mozilla.org'),
+ ('Whitespace', '\n'),
+ ('MOZ_LANGPACK_CREATOR', 'mozilla.org'),
+ ('Whitespace', '\n')))
+
+ def test_filter_empty_lines(self):
+ self._test('''#filter emptyLines
+
+#define MOZ_LANGPACK_CREATOR mozilla.org
+#define MOZ_LANGPACK_CREATOR mozilla.org
+#unfilter emptyLines''', (
+ ('DefinesInstruction', 'filter emptyLines'),
+ ('Whitespace', '\n\n'),
+ ('MOZ_LANGPACK_CREATOR', 'mozilla.org'),
+ ('Whitespace', '\n'),
+ ('MOZ_LANGPACK_CREATOR', 'mozilla.org'),
+ ('Whitespace', '\n'),
('DefinesInstruction', 'unfilter emptyLines')))
+ def test_unfilter_empty_lines_with_trailing(self):
+ self._test('''#filter emptyLines
+
+#define MOZ_LANGPACK_CREATOR mozilla.org
+#define MOZ_LANGPACK_CREATOR mozilla.org
+#unfilter emptyLines
+''', (
+ ('DefinesInstruction', 'filter emptyLines'),
+ ('Whitespace', '\n\n'),
+ ('MOZ_LANGPACK_CREATOR', 'mozilla.org'),
+ ('Whitespace', '\n'),
+ ('MOZ_LANGPACK_CREATOR', 'mozilla.org'),
+ ('Whitespace', '\n'),
+ ('DefinesInstruction', 'unfilter emptyLines'),
+ ('Whitespace', '\n')))
+
def testToolkit(self):
self._test('''#define MOZ_LANG_TITLE English (US)
''', (
- ('MOZ_LANG_TITLE', 'English (US)'),))
+ ('MOZ_LANG_TITLE', 'English (US)'),
+ ('Whitespace', '\n')))
def testToolkitEmpty(self):
self._test('', tuple())
def test_empty_file(self):
'''Test that empty files generate errors
defines.inc are interesting that way, as their
content is added to the generated file.
'''
self._test('\n', (('Junk', '\n'),))
self._test('\n\n', (('Junk', '\n\n'),))
self._test(' \n\n', (('Junk', ' \n\n'),))
+ def test_whitespace_value(self):
+ '''Test that there's only one whitespace between key and value
+ '''
+ # funny formatting of trailing whitespace to make it explicit
+ # and flake-8 happy
+ self._test('''\
+#define one \n\
+#define two \n\
+#define tre \n\
+''', (
+ ('one', ''),
+ ('Whitespace', '\n'),
+ ('two', ' '),
+ ('Whitespace', '\n'),
+ ('tre', ' '),
+ ('Whitespace', '\n'),))
+
if __name__ == '__main__':
unittest.main()
--- a/third_party/python/compare-locales/compare_locales/tests/test_dtd.py
+++ b/third_party/python/compare-locales/compare_locales/tests/test_dtd.py
@@ -25,84 +25,94 @@ class TestDTD(ParserTestMixin, unittest.
<!ENTITY good.two "two">
<!ENTITY bad.two "bad "quoted" word">
<!ENTITY good.three "three">
<!ENTITY good.four "good ' quote">
<!ENTITY good.five "good 'quoted' word">
'''
quoteRef = (
('good.one', 'one'),
- ('Junk', '<!ENTITY bad.one "bad " quote">'),
+ ('Whitespace', '\n'),
+ ('Junk', '<!ENTITY bad.one "bad " quote">\n'),
('good.two', 'two'),
- ('Junk', '<!ENTITY bad.two "bad "quoted" word">'),
+ ('Whitespace', '\n'),
+ ('Junk', '<!ENTITY bad.two "bad "quoted" word">\n'),
('good.three', 'three'),
+ ('Whitespace', '\n'),
('good.four', 'good \' quote'),
- ('good.five', 'good \'quoted\' word'),)
+ ('Whitespace', '\n'),
+ ('good.five', 'good \'quoted\' word'),
+ ('Whitespace', '\n'),)
def test_quotes(self):
self._test(self.quoteContent, self.quoteRef)
def test_apos(self):
qr = re.compile('[\'"]', re.M)
def quot2apos(s):
return qr.sub(lambda m: m.group(0) == '"' and "'" or '"', s)
self._test(quot2apos(self.quoteContent),
- map(lambda t: (t[0], quot2apos(t[1])), self.quoteRef))
+ ((ref[0], quot2apos(ref[1])) for ref in self.quoteRef))
def test_parsed_ref(self):
self._test('''<!ENTITY % fooDTD SYSTEM "chrome://brand.dtd">
%fooDTD;
''',
(('fooDTD', '"chrome://brand.dtd"'),))
def test_trailing_comment(self):
self._test('''<!ENTITY first "string">
<!ENTITY second "string">
<!--
<!ENTITY commented "out">
-->
''',
- (('first', 'string'), ('second', 'string'),
- ('Comment', 'out')))
+ (
+ ('first', 'string'),
+ ('Whitespace', '\n'),
+ ('second', 'string'),
+ ('Whitespace', '\n'),
+ ('Comment', 'out'),
+ ('Whitespace', '\n')))
def test_license_header(self):
p = parser.getParser('foo.dtd')
p.readContents(self.resource('triple-license.dtd'))
entities = list(p.walk())
self.assert_(isinstance(entities[0], parser.Comment))
self.assertIn('MPL', entities[0].all)
- e = entities[1]
+ e = entities[2]
self.assert_(isinstance(e, parser.Entity))
self.assertEqual(e.key, 'foo')
self.assertEqual(e.val, 'value')
- self.assertEqual(len(entities), 2)
+ self.assertEqual(len(entities), 4)
p.readContents('''\
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this file,
- You can obtain one at http://mozilla.org/MPL/2.0/. -->
<!ENTITY foo "value">
''')
entities = list(p.walk())
self.assert_(isinstance(entities[0], parser.Comment))
self.assertIn('MPL', entities[0].all)
- e = entities[1]
+ e = entities[2]
self.assert_(isinstance(e, parser.Entity))
self.assertEqual(e.key, 'foo')
self.assertEqual(e.val, 'value')
- self.assertEqual(len(entities), 2)
+ self.assertEqual(len(entities), 4)
def testBOM(self):
self._test(u'\ufeff<!ENTITY foo.label "stuff">'.encode('utf-8'),
(('foo.label', 'stuff'),))
def test_trailing_whitespace(self):
self._test('<!ENTITY foo.label "stuff">\n \n',
- (('foo.label', 'stuff'),))
+ (('foo.label', 'stuff'), ('Whitespace', '\n \n')))
def test_unicode_comment(self):
self._test('<!-- \xe5\x8f\x96 -->',
(('Comment', u'\u53d6'),))
def test_empty_file(self):
self._test('', tuple())
self._test('\n', (('Whitespace', '\n'),))
@@ -113,38 +123,60 @@ class TestDTD(ParserTestMixin, unittest.
self.parser.readContents('''\
<!ENTITY one "value">
<!ENTITY two "other
escaped value">
''')
one, two = list(self.parser)
self.assertEqual(one.position(), (1, 1))
self.assertEqual(one.value_position(), (1, 16))
- self.assertEqual(one.position(-1), (2, 1))
+ self.assertEqual(one.position(-1), (1, 23))
self.assertEqual(two.position(), (2, 1))
self.assertEqual(two.value_position(), (2, 16))
self.assertEqual(two.value_position(-1), (3, 14))
self.assertEqual(two.value_position(10), (3, 5))
- def test_post(self):
- self.parser.readContents('<!ENTITY a "a"><!ENTITY b "b">')
- a, b = list(self.parser)
- self.assertEqual(a.post, '')
- self.parser.readContents('<!ENTITY a "a"> <!ENTITY b "b">')
- a, b = list(self.parser)
- self.assertEqual(a.post, ' ')
-
def test_word_count(self):
self.parser.readContents('''\
<!ENTITY a "one">
<!ENTITY b "one<br>two">
<!ENTITY c "one<span>word</span>">
<!ENTITY d "one <a href='foo'>two</a> three">
''')
a, b, c, d = list(self.parser)
self.assertEqual(a.count_words(), 1)
self.assertEqual(b.count_words(), 2)
self.assertEqual(c.count_words(), 1)
self.assertEqual(d.count_words(), 3)
+ def test_html_entities(self):
+ self.parser.readContents('''\
+<!ENTITY named "&">
+<!ENTITY numcode "&">
+<!ENTITY shorthexcode "&">
+<!ENTITY longhexcode "&">
+<!ENTITY unknown "&unknownEntity;">
+''')
+ entities = iter(self.parser)
+
+ entity = next(entities)
+ self.assertEqual(entity.raw_val, '&')
+ self.assertEqual(entity.val, '&')
+
+ entity = next(entities)
+ self.assertEqual(entity.raw_val, '&')
+ self.assertEqual(entity.val, '&')
+
+ entity = next(entities)
+ self.assertEqual(entity.raw_val, '&')
+ self.assertEqual(entity.val, '&')
+
+ entity = next(entities)
+ self.assertEqual(entity.raw_val, '&')
+ self.assertEqual(entity.val, '&')
+
+ entity = next(entities)
+ self.assertEqual(entity.raw_val, '&unknownEntity;')
+ self.assertEqual(entity.val, '&unknownEntity;')
+
if __name__ == '__main__':
unittest.main()
--- a/third_party/python/compare-locales/compare_locales/tests/test_ftl.py
+++ b/third_party/python/compare-locales/compare_locales/tests/test_ftl.py
@@ -1,15 +1,16 @@
# -*- coding: utf-8 -*-
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
import unittest
+from compare_locales import parser
from compare_locales.tests import ParserTestMixin
class TestFluentParser(ParserTestMixin, unittest.TestCase):
maxDiff = None
filename = 'foo.ftl'
def test_equality_same(self):
@@ -97,17 +98,17 @@ h =
abc =
A
B
C
''')
[abc] = list(self.parser)
self.assertEqual(abc.key, 'abc')
- self.assertEqual(abc.val, '\n A\n B\n C')
+ self.assertEqual(abc.val, ' A\n B\n C')
self.assertEqual(abc.all, 'abc =\n A\n B\n C')
def test_message_with_attribute(self):
self.parser.readContents('''\
abc = ABC
.attr = Attr
''')
@@ -119,15 +120,86 @@ abc = ABC
def test_message_with_attribute_and_no_value(self):
self.parser.readContents('''\
abc
.attr = Attr
''')
[abc] = list(self.parser)
self.assertEqual(abc.key, 'abc')
- self.assertEqual(abc.val, '')
+ self.assertEqual(abc.val, None)
self.assertEqual(abc.all, 'abc\n .attr = Attr')
attributes = list(abc.attributes)
self.assertEqual(len(attributes), 1)
attr = attributes[0]
self.assertEqual(attr.key, 'attr')
self.assertEqual(attr.val, 'Attr')
+
+ def test_non_localizable(self):
+ self.parser.readContents('''\
+// Resource Comment
+
+foo = Foo
+
+// Section Comment
+[[ Section Header ]]
+
+bar = Bar
+
+// Standalone Comment
+
+// Baz Comment
+baz = Baz
+''')
+ entities = self.parser.walk()
+
+ entity = next(entities)
+ self.assertTrue(isinstance(entity, parser.Comment))
+ self.assertEqual(entity.all, '// Resource Comment')
+
+ entity = next(entities)
+ self.assertTrue(isinstance(entity, parser.Whitespace))
+ self.assertEqual(entity.all, '\n\n')
+
+ entity = next(entities)
+ self.assertTrue(isinstance(entity, parser.FluentEntity))
+ self.assertEqual(entity.val, 'Foo')
+
+ entity = next(entities)
+ self.assertTrue(isinstance(entity, parser.Whitespace))
+ self.assertEqual(entity.all, '\n\n')
+
+ entity = next(entities)
+ self.assertTrue(isinstance(entity, parser.FluentSection))
+ self.assertEqual(
+ entity.all, '// Section Comment\n[[ Section Header ]]')
+ self.assertEqual(entity.val, 'Section Header ')
+ self.assertEqual(
+ entity.entry.comment.content, 'Section Comment')
+
+ entity = next(entities)
+ self.assertTrue(isinstance(entity, parser.Whitespace))
+ self.assertEqual(entity.all, '\n\n')
+
+ entity = next(entities)
+ self.assertTrue(isinstance(entity, parser.FluentEntity))
+ self.assertEqual(entity.val, 'Bar')
+
+ entity = next(entities)
+ self.assertTrue(isinstance(entity, parser.Whitespace))
+ self.assertEqual(entity.all, '\n\n')
+
+ entity = next(entities)
+ self.assertTrue(isinstance(entity, parser.Comment))
+ self.assertEqual(entity.all, '// Standalone Comment')
+
+ entity = next(entities)
+ self.assertTrue(isinstance(entity, parser.Whitespace))
+ self.assertEqual(entity.all, '\n\n')
+
+ entity = next(entities)
+ self.assertTrue(isinstance(entity, parser.FluentEntity))
+ self.assertEqual(entity.val, 'Baz')
+ self.assertEqual(entity.entry.comment.content, 'Baz Comment')
+
+ entity = next(entities)
+ self.assertTrue(isinstance(entity, parser.Whitespace))
+ self.assertEqual(entity.all, '\n')
--- a/third_party/python/compare-locales/compare_locales/tests/test_merge.py
+++ b/third_party/python/compare-locales/compare_locales/tests/test_merge.py
@@ -288,19 +288,19 @@ class TestDTD(unittest.TestCase, Content
'missing': 1,
'missing_w': 1,
'unchanged': 2,
'unchanged_w': 2
}},
'details': {
'l10n.dtd': [
{'error': u'Unparsed content "<!ENTY bar '
- u'\'gimmick\'>" '
+ u'\'gimmick\'>\n" '
u'from line 2 column 1 to '
- u'line 2 column 22'},
+ u'line 3 column 1'},
{'missingEntity': u'bar'}]
}
})
mergefile = mozpath.join(self.tmp, "merge", "l10n.dtd")
self.assertTrue(os.path.isfile(mergefile))
p = getParser(mergefile)
p.readFile(mergefile)
[m, n] = p.parse()
new file mode 100644
--- /dev/null
+++ b/third_party/python/compare-locales/compare_locales/tests/test_merge_comments.py
@@ -0,0 +1,186 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import unittest
+from compare_locales.merge import merge_channels
+
+
+class TestMergeComments(unittest.TestCase):
+ name = "foo.properties"
+
+ def test_comment_added_in_first(self):
+ channels = (b"""
+foo = Foo 1
+# Bar Comment 1
+bar = Bar 1
+""", b"""
+foo = Foo 2
+bar = Bar 2
+""")
+ self.assertMultiLineEqual(
+ merge_channels(self.name, *channels), b"""
+foo = Foo 1
+# Bar Comment 1
+bar = Bar 1
+""")
+
+ def test_comment_still_in_last(self):
+ channels = (b"""
+foo = Foo 1
+bar = Bar 1
+""", b"""
+foo = Foo 2
+# Bar Comment 2
+bar = Bar 2
+""")
+ self.assertMultiLineEqual(
+ merge_channels(self.name, *channels), b"""
+foo = Foo 1
+# Bar Comment 2
+bar = Bar 1
+""")
+
+ def test_comment_changed(self):
+ channels = (b"""
+foo = Foo 1
+# Bar Comment 1
+bar = Bar 1
+""", b"""
+foo = Foo 2
+# Bar Comment 2
+bar = Bar 2
+""")
+ self.assertMultiLineEqual(
+ merge_channels(self.name, *channels), b"""
+foo = Foo 1
+# Bar Comment 1
+bar = Bar 1
+""")
+
+
+class TestMergeStandaloneComments(unittest.TestCase):
+ name = "foo.properties"
+
+ def test_comment_added_in_first(self):
+ channels = (b"""
+# Standalone Comment 1
+
+# Foo Comment 1
+foo = Foo 1
+""", b"""
+# Foo Comment 2
+foo = Foo 2
+""")
+ self.assertMultiLineEqual(
+ merge_channels(self.name, *channels), b"""
+# Standalone Comment 1
+
+# Foo Comment 1
+foo = Foo 1
+""")
+
+ def test_comment_still_in_last(self):
+ channels = (b"""
+# Foo Comment 1
+foo = Foo 1
+""", b"""
+# Standalone Comment 2
+
+# Foo Comment 2
+foo = Foo 2
+""")
+ self.assertMultiLineEqual(
+ merge_channels(self.name, *channels), b"""
+# Standalone Comment 2
+
+# Foo Comment 1
+foo = Foo 1
+""")
+
+ def test_comments_in_both(self):
+ channels = (b"""
+# Standalone Comment 1
+
+# Foo Comment 1
+foo = Foo 1
+""", b"""
+# Standalone Comment 2
+
+# Foo Comment 2
+foo = Foo 2
+""")
+ self.assertMultiLineEqual(
+ merge_channels(self.name, *channels), b"""
+# Standalone Comment 2
+
+# Standalone Comment 1
+
+# Foo Comment 1
+foo = Foo 1
+""")
+
+ def test_identical_comments_in_both(self):
+ channels = (b"""
+# Standalone Comment
+
+# Foo Comment 1
+foo = Foo 1
+""", b"""
+# Standalone Comment
+
+# Foo Comment 2
+foo = Foo 2
+""")
+ self.assertMultiLineEqual(
+ merge_channels(self.name, *channels), b"""
+# Standalone Comment
+
+# Foo Comment 1
+foo = Foo 1
+""")
+
+ def test_standalone_which_is_attached_in_first(self):
+ channels = (b"""
+# Ambiguous Comment
+foo = Foo 1
+
+# Bar Comment 1
+bar = Bar 1
+""", b"""
+# Ambiguous Comment
+
+# Bar Comment 2
+bar = Bar 2
+""")
+ self.assertMultiLineEqual(
+ merge_channels(self.name, *channels), b"""
+# Ambiguous Comment
+
+foo = Foo 1
+
+# Bar Comment 1
+bar = Bar 1
+""")
+
+ def test_standalone_which_is_attached_in_second(self):
+ channels = (b"""
+# Ambiguous Comment
+
+# Bar Comment 1
+bar = Bar 1
+""", b"""
+# Ambiguous Comment
+foo = Foo 1
+
+# Bar Comment 2
+bar = Bar 2
+""")
+ self.assertMultiLineEqual(
+ merge_channels(self.name, *channels), b"""
+# Ambiguous Comment
+foo = Foo 1
+
+# Bar Comment 1
+bar = Bar 1
+""")
new file mode 100644
--- /dev/null
+++ b/third_party/python/compare-locales/compare_locales/tests/test_merge_dtd.py
@@ -0,0 +1,133 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import unittest
+from compare_locales.merge import merge_channels
+
+
+class TestMergeDTD(unittest.TestCase):
+ name = "foo.dtd"
+ maxDiff = None
+
+ def test_no_changes(self):
+ channels = (b"""
+<!ENTITY foo "Foo 1">
+""", b"""
+<!ENTITY foo "Foo 2">
+""")
+ self.assertEqual(
+ merge_channels(self.name, *channels), b"""
+<!ENTITY foo "Foo 1">
+""")
+
+ def test_trailing_whitespace(self):
+ channels = (b"""
+<!ENTITY foo "Foo 1">
+""", b"""
+<!ENTITY foo "Foo 2"> \n""")
+ self.assertEqual(
+ merge_channels(self.name, *channels), b"""
+<!ENTITY foo "Foo 1"> \n""")
+
+ def test_browser_dtd(self):
+ channels = (b"""\
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!-- LOCALIZATION NOTE : FILE This file contains the browser main menu ... -->
+<!-- LOCALIZATION NOTE : FILE Do not translate commandkeys -->
+
+<!-- LOCALIZATION NOTE (mainWindow.titlemodifier) : DONT_TRANSLATE -->
+<!ENTITY mainWindow.titlemodifier "&brandFullName;">
+<!-- LOCALIZATION NOTE (mainWindow.separator): DONT_TRANSLATE -->
+<!ENTITY mainWindow.separator " - ">
+<!-- LOCALIZATION NOTE (mainWindow.privatebrowsing2): This will be appended ...
+ inside the ... -->
+<!ENTITY mainWindow.privatebrowsing2 "(Private Browsing)">
+""", b"""\
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!-- LOCALIZATION NOTE : FILE This file contains the browser main menu ... -->
+<!-- LOCALIZATION NOTE : FILE Do not translate commandkeys -->
+
+<!-- LOCALIZATION NOTE (mainWindow.title): DONT_TRANSLATE -->
+<!ENTITY mainWindow.title "&brandFullName;">
+<!-- LOCALIZATION NOTE (mainWindow.titlemodifier) : DONT_TRANSLATE -->
+<!ENTITY mainWindow.titlemodifier "&brandFullName;">
+<!-- LOCALIZATION NOTE (mainWindow.privatebrowsing): This will be appended ...
+ inside the ... -->
+<!ENTITY mainWindow.privatebrowsing "(Private Browsing)">
+""")
+
+ self.assertMultiLineEqual(
+ merge_channels(self.name, *channels), b"""\
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!-- LOCALIZATION NOTE : FILE This file contains the browser main menu ... -->
+<!-- LOCALIZATION NOTE : FILE Do not translate commandkeys -->
+
+<!-- LOCALIZATION NOTE (mainWindow.title): DONT_TRANSLATE -->
+<!ENTITY mainWindow.title "&brandFullName;">
+
+<!-- LOCALIZATION NOTE (mainWindow.titlemodifier) : DONT_TRANSLATE -->
+<!ENTITY mainWindow.titlemodifier "&brandFullName;">
+<!-- LOCALIZATION NOTE (mainWindow.privatebrowsing): This will be appended ...
+ inside the ... -->
+<!ENTITY mainWindow.privatebrowsing "(Private Browsing)">
+<!-- LOCALIZATION NOTE (mainWindow.separator): DONT_TRANSLATE -->
+<!ENTITY mainWindow.separator " - ">
+<!-- LOCALIZATION NOTE (mainWindow.privatebrowsing2): This will be appended ...
+ inside the ... -->
+<!ENTITY mainWindow.privatebrowsing2 "(Private Browsing)">
+""")
+
+ def test_aboutServiceWorkers_dtd(self):
+ channels = (b"""\
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!-- LOCALIZATION NOTE the term "Service Workers" should not be translated. -->
+<!ENTITY title "About Service Workers">
+<!-- LOCALIZATION NOTE the term "Service Workers" should not be translated. -->
+<!ENTITY maintitle "Registered Service Workers">
+<!-- LOCALIZATION NOTE the term "Service Workers" should not be translated. -->
+<!ENTITY warning_not_enabled "Service Workers are not enabled.">
+<!-- LOCALIZATION NOTE the term "Service Workers" should not be translated. -->
+<!ENTITY warning_no_serviceworkers "No Service Workers registered.">
+""", b"""\
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!-- LOCALIZATION NOTE the term "Service Workers" should not be translated. -->
+<!ENTITY title "About Service Workers">
+<!-- LOCALIZATION NOTE the term "Service Workers" should not be translated. -->
+<!ENTITY maintitle "Registered Service Workers">
+<!-- LOCALIZATION NOTE the term "Service Workers" should not be translated. -->
+<!ENTITY warning_not_enabled "Service Workers are not enabled.">
+<!-- LOCALIZATION NOTE the term "Service Workers" should not be translated. -->
+<!ENTITY warning_no_serviceworkers "No Service Workers registered.">
+""")
+
+ self.assertMultiLineEqual(
+ merge_channels(self.name, *channels), b"""\
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!-- LOCALIZATION NOTE the term "Service Workers" should not be translated. -->
+<!ENTITY title "About Service Workers">
+<!-- LOCALIZATION NOTE the term "Service Workers" should not be translated. -->
+<!ENTITY maintitle "Registered Service Workers">
+<!-- LOCALIZATION NOTE the term "Service Workers" should not be translated. -->
+<!ENTITY warning_not_enabled "Service Workers are not enabled.">
+<!-- LOCALIZATION NOTE the term "Service Workers" should not be translated. -->
+<!ENTITY warning_no_serviceworkers "No Service Workers registered.">
+""")
new file mode 100644
--- /dev/null
+++ b/third_party/python/compare-locales/compare_locales/tests/test_merge_ftl.py
@@ -0,0 +1,316 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import unittest
+from compare_locales.merge import merge_channels
+
+
+class TestMergeFluent(unittest.TestCase):
+ name = "foo.ftl"
+
+ def test_no_changes(self):
+ channels = (b"""
+foo = Foo 1
+""", b"""
+foo = Foo 2
+""")
+ self.assertEqual(
+ merge_channels(self.name, *channels), b"""
+foo = Foo 1
+""")
+
+ def test_attribute_in_first(self):
+ channels = (b"""
+foo = Foo 1
+ .attr = Attr 1
+""", b"""
+foo = Foo 2
+""")
+ self.assertEqual(
+ merge_channels(self.name, *channels), b"""
+foo = Foo 1
+ .attr = Attr 1
+""")
+
+ def test_attribute_in_last(self):
+ channels = (b"""
+foo = Foo 1
+""", b"""
+foo = Foo 2
+ .attr = Attr 2
+""")
+ self.assertEqual(
+ merge_channels(self.name, *channels), b"""
+foo = Foo 1
+""")
+
+ def test_attribute_changed(self):
+ channels = (b"""
+foo = Foo 1
+ .attr = Attr 1
+""", b"""
+foo = Foo 2
+ .attr = Attr 2
+""")
+ self.assertEqual(
+ merge_channels(self.name, *channels), b"""
+foo = Foo 1
+ .attr = Attr 1
+""")
+
+ def test_tag_in_first(self):
+ channels = (b"""
+foo = Foo 1
+ #tag
+""", b"""
+foo = Foo 2
+""")
+ self.assertEqual(
+ merge_channels(self.name, *channels), b"""
+foo = Foo 1
+ #tag
+""")
+
+ def test_tag_in_last(self):
+ channels = (b"""
+foo = Foo 1
+""", b"""
+foo = Foo 2
+ #tag
+""")
+ self.assertEqual(
+ merge_channels(self.name, *channels), b"""
+foo = Foo 1
+""")
+
+ def test_tag_changed(self):
+ channels = (b"""
+foo = Foo 1
+ #tag1
+""", b"""
+foo = Foo 2
+ #tag2
+""")
+ self.assertEqual(
+ merge_channels(self.name, *channels), b"""
+foo = Foo 1
+ #tag1
+""")
+
+ def test_section_in_first(self):
+ channels = (b"""
+[[ Section 1 ]]
+foo = Foo 1
+""", b"""
+foo = Foo 2
+""")
+ self.assertEqual(
+ merge_channels(self.name, *channels), b"""
+[[ Section 1 ]]
+foo = Foo 1
+""")
+
+ def test_section_in_last(self):
+ channels = (b"""
+foo = Foo 1
+""", b"""
+[[ Section 2 ]]
+foo = Foo 2
+""")
+ self.assertEqual(
+ merge_channels(self.name, *channels), b"""
+[[ Section 2 ]]
+foo = Foo 1
+""")
+
+ def test_section_changed(self):
+ channels = (b"""
+[[ Section 1 ]]
+foo = Foo 1
+""", b"""
+[[ Section 2 ]]
+foo = Foo 2
+""")
+ self.assertEqual(
+ merge_channels(self.name, *channels), b"""
+[[ Section 2 ]]
+[[ Section 1 ]]
+foo = Foo 1
+""")
+
+ def test_message_comment_in_first(self):
+ channels = (b"""
+// Comment 1
+foo = Foo 1
+""", b"""
+foo = Foo 2
+""")
+ self.assertEqual(
+ merge_channels(self.name, *channels), b"""
+// Comment 1
+foo = Foo 1
+""")
+
+ def test_message_comment_in_last(self):
+ channels = (b"""
+foo = Foo 1
+""", b"""
+// Comment 2
+foo = Foo 2
+""")
+ self.assertEqual(
+ merge_channels(self.name, *channels), b"""
+foo = Foo 1
+""")
+
+ def test_message_comment_changed(self):
+ channels = (b"""
+// Comment 1
+foo = Foo 1
+""", b"""
+// Comment 2
+foo = Foo 2
+""")
+ self.assertEqual(
+ merge_channels(self.name, *channels), b"""
+// Comment 1
+foo = Foo 1
+""")
+
+ def test_section_comment_in_first(self):
+ channels = (b"""
+// Comment 1
+[[ Section ]]
+""", b"""
+[[ Section ]]
+""")
+ self.assertEqual(
+ merge_channels(self.name, *channels), b"""
+// Comment 1
+[[ Section ]]
+""")
+
+ def test_section_comment_in_last(self):
+ channels = (b"""
+[[ Section ]]
+""", b"""
+// Comment 2
+[[ Section ]]
+""")
+ self.assertEqual(
+ merge_channels(self.name, *channels), b"""
+[[ Section ]]
+""")
+
+ def test_section_comment_changed(self):
+ channels = (b"""
+// Comment 1
+[[ Section ]]
+""", b"""
+// Comment 2
+[[ Section ]]
+""")
+ self.assertEqual(
+ merge_channels(self.name, *channels), b"""
+// Comment 1
+[[ Section ]]
+""")
+
+ def test_standalone_comment_in_first(self):
+ channels = (b"""
+foo = Foo 1
+
+// Comment 1
+""", b"""
+foo = Foo 2
+""")
+ self.assertEqual(
+ merge_channels(self.name, *channels), b"""
+foo = Foo 1
+
+// Comment 1
+""")
+
+ def test_standalone_comment_in_last(self):
+ channels = (b"""
+foo = Foo 1
+""", b"""
+foo = Foo 2
+
+// Comment 2
+""")
+ self.assertEqual(
+ merge_channels(self.name, *channels), b"""
+foo = Foo 1
+
+// Comment 2
+""")
+
+ def test_standalone_comment_changed(self):
+ channels = (b"""
+foo = Foo 1
+
+// Comment 1
+""", b"""
+foo = Foo 2
+
+// Comment 2
+""")
+ self.assertEqual(
+ merge_channels(self.name, *channels), b"""
+foo = Foo 1
+
+// Comment 2
+
+// Comment 1
+""")
+
+ def test_resource_comment_in_first(self):
+ channels = (b"""
+// Resource Comment 1
+
+foo = Foo 1
+""", b"""
+foo = Foo 2
+""")
+ self.assertEqual(
+ merge_channels(self.name, *channels), b"""
+// Resource Comment 1
+
+foo = Foo 1
+""")
+
+ def test_resource_comment_in_last(self):
+ channels = (b"""
+foo = Foo 1
+""", b"""
+// Resource Comment 1
+
+foo = Foo 2
+""")
+ self.assertEqual(
+ merge_channels(self.name, *channels), b"""
+// Resource Comment 1
+
+foo = Foo 1
+""")
+
+ def test_resource_comment_changed(self):
+ channels = (b"""
+// Resource Comment 1
+
+foo = Foo 1
+""", b"""
+// Resource Comment 2
+
+foo = Foo 2
+""")
+ self.assertEqual(
+ merge_channels(self.name, *channels), b"""
+// Resource Comment 2
+
+// Resource Comment 1
+
+foo = Foo 1
+""")
new file mode 100644
--- /dev/null
+++ b/third_party/python/compare-locales/compare_locales/tests/test_merge_messages.py
@@ -0,0 +1,93 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import unittest
+from compare_locales.merge import merge_channels
+
+
+class TestMergeTwo(unittest.TestCase):
+ name = "foo.properties"
+
+ def test_no_changes(self):
+ channels = (b"""
+foo = Foo 1
+""", b"""
+foo = Foo 2
+""")
+ self.assertEqual(
+ merge_channels(self.name, *channels), b"""
+foo = Foo 1
+""")
+
+ def test_message_added_in_first(self):
+ channels = (b"""
+foo = Foo 1
+bar = Bar 1
+""", b"""
+foo = Foo 2
+""")
+ self.assertEqual(
+ merge_channels(self.name, *channels), b"""
+foo = Foo 1
+bar = Bar 1
+""")
+
+ def test_message_still_in_last(self):
+ channels = (b"""
+foo = Foo 1
+""", b"""
+foo = Foo 2
+bar = Bar 2
+""")
+ self.assertEqual(
+ merge_channels(self.name, *channels), b"""
+foo = Foo 1
+bar = Bar 2
+""")
+
+ def test_message_reordered(self):
+ channels = (b"""
+foo = Foo 1
+bar = Bar 1
+""", b"""
+bar = Bar 2
+foo = Foo 2
+""")
+ self.assertEqual(
+ merge_channels(self.name, *channels), b"""
+foo = Foo 1
+bar = Bar 1
+""")
+
+
+class TestMergeThree(unittest.TestCase):
+ name = "foo.properties"
+
+ def test_no_changes(self):
+ channels = (b"""
+foo = Foo 1
+""", b"""
+foo = Foo 2
+""", b"""
+foo = Foo 3
+""")
+ self.assertEqual(
+ merge_channels(self.name, *channels), b"""
+foo = Foo 1
+""")
+
+ def test_message_still_in_last(self):
+ channels = (b"""
+foo = Foo 1
+""", b"""
+foo = Foo 2
+""", b"""
+foo = Foo 3
+bar = Bar 3
+""")
+ self.assertEqual(
+ merge_channels(self.name, *channels), b"""
+foo = Foo 1
+bar = Bar 3
+""")
new file mode 100644
--- /dev/null
+++ b/third_party/python/compare-locales/compare_locales/tests/test_merge_properties.py
@@ -0,0 +1,41 @@
+# coding=utf8
+
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+from codecs import decode, encode
+import unittest
+
+from compare_locales.merge import merge_channels
+
+
+class TestMergeProperties(unittest.TestCase):
+ name = "foo.properties"
+
+ def test_no_changes(self):
+ channels = (b"""
+foo = Foo 1
+""", b"""
+foo = Foo 2
+""")
+ self.assertEqual(
+ merge_channels(self.name, *channels), b"""
+foo = Foo 1
+""")
+
+ def test_encoding(self):
+ channels = (encode(u"""
+foo = Foo 1…
+""", "utf8"), encode(u"""
+foo = Foo 2…
+""", "utf8"))
+ output = merge_channels(self.name, *channels)
+ self.assertEqual(output, encode(u"""
+foo = Foo 1…
+""", "utf8"))
+
+ u_output = decode(output, "utf8")
+ self.assertEqual(u_output, u"""
+foo = Foo 1…
+""")
new file mode 100644
--- /dev/null
+++ b/third_party/python/compare-locales/compare_locales/tests/test_merge_unknown.py
@@ -0,0 +1,21 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import unittest
+
+from compare_locales.merge import merge_channels, MergeNotSupportedError
+
+
+class TestMergeUnknown(unittest.TestCase):
+ name = "foo.unknown"
+
+ def test_not_supported_error(self):
+ channels = (b"""
+foo = Foo 1
+""", b"""
+foo = Foo 2
+""")
+ pattern = "Unsupported file format \(foo\.unknown\)\."
+ with self.assertRaisesRegexp(MergeNotSupportedError, pattern):
+ merge_channels(self.name, *channels)
new file mode 100644
--- /dev/null
+++ b/third_party/python/compare-locales/compare_locales/tests/test_merge_whitespace.py
@@ -0,0 +1,76 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import unittest
+from compare_locales.merge import merge_channels
+
+
+class TestMergeWhitespace(unittest.TestCase):
+ name = "foo.properties"
+
+ def test_trailing_spaces(self):
+ channels = (b"""
+foo = Foo 1
+ """, b"""
+foo = Foo 2
+""")
+ self.assertEqual(
+ merge_channels(self.name, *channels), b"""
+foo = Foo 1
+ """)
+
+ def test_blank_lines_between_messages(self):
+ channels = (b"""
+foo = Foo 1
+
+bar = Bar 1
+""", b"""
+foo = Foo 2
+bar = Bar 2
+""")
+ self.assertEqual(
+ merge_channels(self.name, *channels), b"""
+foo = Foo 1
+
+bar = Bar 1
+""")
+
+ def test_no_eol(self):
+ channels = (b"""
+foo = Foo 1""", b"""
+foo = Foo 2
+bar = Bar 2
+""")
+ self.assertEqual(
+ merge_channels(self.name, *channels), b"""
+foo = Foo 1
+bar = Bar 2
+""")
+
+ def test_still_in_last_with_blank(self):
+ channels = (b"""
+
+foo = Foo 1
+
+baz = Baz 1
+
+""", b"""
+
+foo = Foo 2
+
+bar = Bar 2
+
+baz = Baz 2
+
+""")
+ self.assertEqual(
+ merge_channels(self.name, *channels), b"""
+
+foo = Foo 1
+
+bar = Bar 2
+
+baz = Baz 1
+
+""")
--- a/third_party/python/compare-locales/compare_locales/tests/test_parser.py
+++ b/third_party/python/compare-locales/compare_locales/tests/test_parser.py
@@ -3,37 +3,41 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
import unittest
from compare_locales import parser
class TestParserContext(unittest.TestCase):
- def test_lines(self):
- "Test that Parser.Context.lines returns 1-based tuples"
+ def test_linecol(self):
+ "Should return 1-based line and column numbers."
ctx = parser.Parser.Context('''first line
second line
third line
''')
self.assertEqual(
- ctx.lines(0, 1),
- [(1, 1), (1, 2)]
+ ctx.linecol(0),
+ (1, 1)
+ )
+ self.assertEqual(
+ ctx.linecol(1),
+ (1, 2)
)
self.assertEqual(
- ctx.lines(len('first line')),
- [(1, len('first line') + 1)]
+ ctx.linecol(len('first line')),
+ (1, len('first line') + 1)
)
self.assertEqual(
- ctx.lines(len('first line') + 1),
- [(2, 1)]
+ ctx.linecol(len('first line') + 1),
+ (2, 1)
)
self.assertEqual(
- ctx.lines(len(ctx.contents)),
- [(4, 1)]
+ ctx.linecol(len(ctx.contents)),
+ (4, 1)
)
def test_empty_parser(self):
p = parser.Parser()
entities, _map = p.parse()
self.assertListEqual(
entities,
[]
--- a/third_party/python/compare-locales/compare_locales/tests/test_properties.py
+++ b/third_party/python/compare-locales/compare_locales/tests/test_properties.py
@@ -17,21 +17,25 @@ class TestPropertiesParser(ParserTestMix
two_line = This is the first \
of two lines
one_line_trailing = This line ends in \\
and has junk
two_lines_triple = This line is one of two and ends in \\\
and still has another line coming
''', (
('one_line', 'This is one line'),
+ ('Whitespace', '\n'),
('two_line', u'This is the first of two lines'),
+ ('Whitespace', '\n'),
('one_line_trailing', u'This line ends in \\'),
+ ('Whitespace', '\n'),
('Junk', 'and has junk\n'),
('two_lines_triple', 'This line is one of two and ends in \\'
- 'and still has another line coming')))
+ 'and still has another line coming'),
+ ('Whitespace', '\n')))
def testProperties(self):
# port of netwerk/test/PropertiesTest.cpp
self.parser.readContents(self.resource('test.properties'))
ref = ['1', '2', '3', '4', '5', '6', '7', '8',
'this is the first part of a continued line '
'and here is the 2nd part']
i = iter(self.parser)
@@ -58,17 +62,21 @@ and an end''', (('bar', 'one line with a
def test_license_header(self):
self._test('''\
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
foo=value
-''', (('Comment', 'MPL'), ('foo', 'value')))
+''', (
+ ('Comment', 'MPL'),
+ ('Whitespace', '\n\n'),
+ ('foo', 'value'),
+ ('Whitespace', '\n')))
def test_escapes(self):
self.parser.readContents(r'''
# unicode escapes
zero = some \unicode
one = \u0
two = \u41
three = \u042
@@ -82,70 +90,91 @@ seven = \n\r\t\\
self.assertEqual(e.val, r)
def test_trailing_comment(self):
self._test('''first = string
second = string
#
#commented out
-''', (('first', 'string'), ('second', 'string'),
- ('Comment', 'commented out')))
+''', (
+ ('first', 'string'),
+ ('Whitespace', '\n'),
+ ('second', 'string'),
+ ('Whitespace', '\n\n'),
+ ('Comment', 'commented out'),
+ ('Whitespace', '\n')))
def test_trailing_newlines(self):
self._test('''\
foo = bar
\x20\x20
- ''', (('foo', 'bar'),))
+ ''', (('foo', 'bar'), ('Whitespace', '\n\n\x20\x20\n ')))
def test_just_comments(self):
self._test('''\
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
# LOCALIZATION NOTE These strings are used inside the Promise debugger
# which is available as a panel in the Debugger.
-''', (('Comment', 'MPL'), ('Comment', 'LOCALIZATION NOTE')))
+''', (
+ ('Comment', 'MPL'),
+ ('Whitespace', '\n\n'),
+ ('Comment', 'LOCALIZATION NOTE'),
+ ('Whitespace', '\n')))
def test_just_comments_without_trailing_newline(self):
self._test('''\
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
# LOCALIZATION NOTE These strings are used inside the Promise debugger
# which is available as a panel in the Debugger.''', (
- ('Comment', 'MPL'), ('Comment', 'LOCALIZATION NOTE')))
+ ('Comment', 'MPL'),
+ ('Whitespace', '\n\n'),
+ ('Comment', 'LOCALIZATION NOTE')))
def test_trailing_comment_and_newlines(self):
self._test('''\
# LOCALIZATION NOTE These strings are used inside the Promise debugger
# which is available as a panel in the Debugger.
-''', (('Comment', 'LOCALIZATION NOTE'),))
+''', (
+ ('Comment', 'LOCALIZATION NOTE'),
+ ('Whitespace', '\n\n\n')))
def test_empty_file(self):
self._test('', tuple())
self._test('\n', (('Whitespace', '\n'),))
self._test('\n\n', (('Whitespace', '\n\n'),))
- self._test(' \n\n', (('Whitespace', ' \n\n'),))
+ self._test(' \n\n', (('Whitespace', '\n\n'),))
def test_positions(self):
self.parser.readContents('''\
one = value
two = other \\
escaped value
''')
one, two = list(self.parser)
self.assertEqual(one.position(), (1, 1))
self.assertEqual(one.value_position(), (1, 7))
self.assertEqual(two.position(), (2, 1))
self.assertEqual(two.value_position(), (2, 7))
self.assertEqual(two.value_position(-1), (3, 14))
self.assertEqual(two.value_position(10), (3, 3))
+ # Bug 1399059 comment 18
+ def test_z(self):
+ self.parser.readContents('''\
+one = XYZ ABC
+''')
+ one, = list(self.parser)
+ self.assertEqual(one.val, 'XYZ ABC')
+
if __name__ == '__main__':
unittest.main()
deleted file mode 100644
--- a/third_party/python/fluent/PKG-INFO
+++ /dev/null
@@ -1,16 +0,0 @@
-Metadata-Version: 1.1
-Name: fluent
-Version: 0.4.2
-Summary: Localization library for expressive translations.
-Home-page: https://github.com/projectfluent/python-fluent
-Author: Mozilla
-Author-email: l10n-drivers@mozilla.org
-License: APL 2
-Description: UNKNOWN
-Keywords: fluent,localization,l10n
-Platform: UNKNOWN
-Classifier: Development Status :: 3 - Alpha
-Classifier: Intended Audience :: Developers
-Classifier: License :: OSI Approved :: Apache Software License
-Classifier: Programming Language :: Python :: 2.7
-Classifier: Programming Language :: Python :: 3.5
--- a/third_party/python/fluent/fluent/migrate/__init__.py
+++ b/third_party/python/fluent/fluent/migrate/__init__.py
@@ -1,10 +1,13 @@
# coding=utf8
from .context import MergeContext # noqa: F401
+from .errors import ( # noqa: F401
+ MigrationError, NotSupportedError, UnreadableReferenceError
+)
from .transforms import ( # noqa: F401
Source, COPY, REPLACE_IN_TEXT, REPLACE, PLURALS, CONCAT
)
from .helpers import ( # noqa: F401
- LITERAL, EXTERNAL_ARGUMENT, MESSAGE_REFERENCE
+ EXTERNAL_ARGUMENT, MESSAGE_REFERENCE
)
from .changesets import convert_blame_to_changesets # noqa: F401
--- a/third_party/python/fluent/fluent/migrate/context.py
+++ b/third_party/python/fluent/fluent/migrate/context.py
@@ -1,29 +1,34 @@
# coding=utf8
from __future__ import unicode_literals
import os
import codecs
import logging
+try:
+ from itertools import zip_longest
+except ImportError:
+ from itertools import izip_longest as zip_longest
+
import fluent.syntax.ast as FTL
from fluent.syntax.parser import FluentParser
from fluent.syntax.serializer import FluentSerializer
from fluent.util import fold
try:
from compare_locales.parser import getParser
except ImportError:
def getParser(path):
raise RuntimeError('compare-locales required')
from .cldr import get_plural_categories
from .transforms import Source
from .merge import merge_resource
-from .util import get_message
+from .errors import NotSupportedError, UnreadableReferenceError
class MergeContext(object):
"""Stateful context for merging translation resources.
`MergeContext` must be configured with the target language and the
directory locations of the input data.
@@ -36,17 +41,17 @@ class MergeContext(object):
- The legacy (DTD, properties) translation files for the given
language. The translations from these files will be transformed
into FTL and merged into the existing FTL files for this language.
- A list of `FTL.Message` objects some of whose nodes are special
helper or transform nodes:
- helpers: LITERAL, EXTERNAL_ARGUMENT, MESSAGE_REFERENCE
+ helpers: EXTERNAL_ARGUMENT, MESSAGE_REFERENCE
transforms: COPY, REPLACE_IN_TEXT, REPLACE, PLURALS, CONCAT
"""
def __init__(self, lang, reference_dir, localization_dir):
self.fluent_parser = FluentParser(with_spans=False)
self.fluent_serializer = FluentSerializer()
# An iterable of plural category names relevant to the context's
@@ -74,141 +79,187 @@ class MergeContext(object):
# corresponding to localized entities which will be migrated.
self.dependencies = {}
def read_ftl_resource(self, path):
"""Read an FTL resource and parse it into an AST."""
f = codecs.open(path, 'r', 'utf8')
try:
contents = f.read()
+ except UnicodeDecodeError as err:
+ logger = logging.getLogger('migrate')
+ logger.warn('Unable to read file {}: {}'.format(path, err))
+ raise err
finally:
f.close()
ast = self.fluent_parser.parse(contents)
annots = [
annot
for entry in ast.body
for annot in entry.annotations
]
if len(annots):
logger = logging.getLogger('migrate')
for annot in annots:
msg = annot.message
- logger.warn(u'Syntax error in {}: {}'.format(path, msg))
+ logger.warn('Syntax error in {}: {}'.format(path, msg))
return ast
def read_legacy_resource(self, path):
"""Read a legacy resource and parse it into a dict."""
parser = getParser(path)
parser.readFile(path)
# Transform the parsed result which is an iterator into a dict.
return {entity.key: entity.val for entity in parser}
- def add_reference(self, path, realpath=None):
- """Add an FTL AST to this context's reference resources."""
- fullpath = os.path.join(self.reference_dir, realpath or path)
- try:
- ast = self.read_ftl_resource(fullpath)
- except IOError as err:
- logger = logging.getLogger('migrate')
- logger.error(u'Missing reference file: {}'.format(path))
- raise err
- except UnicodeDecodeError as err:
- logger = logging.getLogger('migrate')
- logger.error(u'Error reading file {}: {}'.format(path, err))
- raise err
- else:
- self.reference_resources[path] = ast
+ def maybe_add_localization(self, path):
+ """Add a localization resource to migrate translations from.
- def add_localization(self, path):
- """Add an existing localization resource.
+ Only legacy resources can be added as migration sources. The resource
+ may be missing on disk.
- If it's an FTL resource, add an FTL AST. Otherwise, it's a legacy
- resource. Use a compare-locales parser to create a dict of (key,
- string value) tuples.
+ Uses a compare-locales parser to create a dict of (key, string value)
+ tuples.
"""
- fullpath = os.path.join(self.localization_dir, path)
- if fullpath.endswith('.ftl'):
- try:
- ast = self.read_ftl_resource(fullpath)
- except IOError:
- logger = logging.getLogger('migrate')
- logger.warn(u'Missing localization file: {}'.format(path))
- except UnicodeDecodeError as err:
- logger = logging.getLogger('migrate')
- logger.warn(u'Error reading file {}: {}'.format(path, err))
- else:
- self.localization_resources[path] = ast
+ if path.endswith('.ftl'):
+ error_message = (
+ 'Migrating translations from Fluent files is not supported '
+ '({})'.format(path))
+ logging.getLogger('migrate').error(error_message)
+ raise NotSupportedError(error_message)
+
+ try:
+ fullpath = os.path.join(self.localization_dir, path)
+ collection = self.read_legacy_resource(fullpath)
+ except IOError:
+ logger = logging.getLogger('migrate')
+ logger.warn('Missing localization file: {}'.format(path))
else:
- try:
- collection = self.read_legacy_resource(fullpath)
- except IOError:
- logger = logging.getLogger('migrate')
- logger.warn(u'Missing localization file: {}'.format(path))
- else:
- self.localization_resources[path] = collection
+ self.localization_resources[path] = collection
- def add_transforms(self, path, transforms):
- """Define transforms for path.
+ def add_transforms(self, target, reference, transforms):
+ """Define transforms for target using reference as template.
+
+ `target` is a path of the destination FTL file relative to the
+ localization directory. `reference` is a path to the template FTL
+ file relative to the reference directory.
Each transform is an extended FTL node with `Transform` nodes as some
values. Transforms are stored in their lazy AST form until
`merge_changeset` is called, at which point they are evaluated to real
FTL nodes with migrated translations.
Each transform is scanned for `Source` nodes which will be used to
build the list of dependencies for the transformed message.
"""
def get_sources(acc, cur):
if isinstance(cur, Source):
acc.add((cur.path, cur.key))
return acc
+ refpath = os.path.join(self.reference_dir, reference)
+ try:
+ ast = self.read_ftl_resource(refpath)
+ except IOError as err:
+ error_message = 'Missing reference file: {}'.format(refpath)
+ logging.getLogger('migrate').error(error_message)
+ raise UnreadableReferenceError(error_message)
+ except UnicodeDecodeError as err:
+ error_message = 'Error reading file {}: {}'.format(refpath, err)
+ logging.getLogger('migrate').error(error_message)
+ raise UnreadableReferenceError(error_message)
+ else:
+ # The reference file will be used by the merge function as
+ # a template for serializing the merge results.
+ self.reference_resources[target] = ast
+
for node in transforms:
# Scan `node` for `Source` nodes and collect the information they
# store into a set of dependencies.
dependencies = fold(get_sources, node, set())
# Set these sources as dependencies for the current transform.
- self.dependencies[(path, node.id.name)] = dependencies
+ self.dependencies[(target, node.id.name)] = dependencies
- path_transforms = self.transforms.setdefault(path, [])
+ # Read all legacy translation files defined in Source transforms.
+ for path in set(path for path, _ in dependencies):
+ self.maybe_add_localization(path)
+
+ path_transforms = self.transforms.setdefault(target, [])
path_transforms += transforms
+ if target not in self.localization_resources:
+ fullpath = os.path.join(self.localization_dir, target)
+ try:
+ ast = self.read_ftl_resource(fullpath)
+ except IOError:
+ logger = logging.getLogger('migrate')
+ logger.info(
+ 'Localization file {} does not exist and '
+ 'it will be created'.format(target))
+ except UnicodeDecodeError:
+ logger = logging.getLogger('migrate')
+ logger.warn(
+ 'Localization file {} will be re-created and some '
+ 'translations might be lost'.format(target))
+ else:
+ self.localization_resources[target] = ast
+
def get_source(self, path, key):
- """Get an entity value from the localized source.
+ """Get an entity value from a localized legacy source.
Used by the `Source` transform.
"""
- if path.endswith('.ftl'):
- resource = self.localization_resources[path]
- return get_message(resource.body, key)
- else:
- resource = self.localization_resources[path]
- return resource.get(key, None)
+ resource = self.localization_resources[path]
+ return resource.get(key, None)
+
+ def messages_equal(self, res1, res2):
+ """Compare messages of two FTL resources.
+
+ Uses FTL.BaseNode.equals to compare all messages in two FTL resources.
+ If the order or number of messages differ, the result is also False.
+ """
+ def message_id(message):
+ "Return the message's identifer name for sorting purposes."
+ return message.id.name
+
+ messages1 = sorted(
+ (entry for entry in res1.body if isinstance(entry, FTL.Message)),
+ key=message_id)
+ messages2 = sorted(
+ (entry for entry in res2.body if isinstance(entry, FTL.Message)),
+ key=message_id)
+ for msg1, msg2 in zip_longest(messages1, messages2):
+ if msg1 is None or msg2 is None:
+ return False
+ if not msg1.equals(msg2):
+ return False
+ return True
def merge_changeset(self, changeset=None):
"""Return a generator of FTL ASTs for the changeset.
The input data must be configured earlier using the `add_*` methods.
if given, `changeset` must be a set of (path, key) tuples describing
which legacy translations are to be merged.
Given `changeset`, return a dict whose keys are resource paths and
values are `FTL.Resource` instances. The values will also be used to
update this context's existing localization resources.
"""
if changeset is None:
- # Merge all known legacy translations.
+ # Merge all known legacy translations. Used in tests.
changeset = {
(path, key)
for path, strings in self.localization_resources.iteritems()
+ if not path.endswith('.ftl')
for key in strings.iterkeys()
}
for path, reference in self.reference_resources.iteritems():
current = self.localization_resources.get(path, FTL.Resource())
transforms = self.transforms.get(path, [])
def in_changeset(ident):
@@ -235,21 +286,24 @@ class MergeContext(object):
return message_deps & changeset
# Merge legacy translations with the existing ones using the
# reference as a template.
snapshot = merge_resource(
self, reference, current, transforms, in_changeset
)
- # If none of the transforms is in the given changeset, the merged
- # snapshot is identical to the current translation. We compare
- # JSON trees rather then use filtering by `in_changeset` to account
- # for translations removed from `reference`.
- if snapshot.to_json() == current.to_json():
+ # Skip this path if the messages in the merged snapshot are
+ # identical to those in the current state of the localization file.
+ # This may happen when:
+ #
+ # - none of the transforms is in the changset, or
+ # - all messages which would be migrated by the context's
+ # transforms already exist in the current state.
+ if self.messages_equal(current, snapshot):
continue
# Store the merged snapshot on the context so that the next merge
# already takes it into account as the existing localization.
self.localization_resources[path] = snapshot
# The result for this path is a complete `FTL.Resource`.
yield path, snapshot
new file mode 100644
--- /dev/null
+++ b/third_party/python/fluent/fluent/migrate/errors.py
@@ -0,0 +1,10 @@
+class MigrationError(ValueError):
+ pass
+
+
+class NotSupportedError(MigrationError):
+ pass
+
+
+class UnreadableReferenceError(MigrationError):
+ pass
--- a/third_party/python/fluent/fluent/migrate/helpers.py
+++ b/third_party/python/fluent/fluent/migrate/helpers.py
@@ -8,22 +8,16 @@ They take a string argument and immediat
(As opposed to Transforms which are AST nodes on their own and only return the
migrated AST nodes when they are evaluated by a MergeContext.) """
from __future__ import unicode_literals
import fluent.syntax.ast as FTL
-def LITERAL(value):
- """Create a Pattern with a single TextElement."""
- elements = [FTL.TextElement(value)]
- return FTL.Pattern(elements)
-
-
def EXTERNAL_ARGUMENT(name):
"""Create an ExternalArgument expression."""
return FTL.ExternalArgument(
id=FTL.Identifier(name)
)
--- a/third_party/python/fluent/fluent/migrate/merge.py
+++ b/third_party/python/fluent/fluent/migrate/merge.py
@@ -6,19 +6,20 @@ import fluent.syntax.ast as FTL
from .transforms import evaluate
from .util import get_message, get_transform
def merge_resource(ctx, reference, current, transforms, in_changeset):
"""Transform legacy translations into FTL.
Use the `reference` FTL AST as a template. For each en-US string in the
- reference, first check if it's in the currently processed changeset with
- `in_changeset`; then check for an existing translation in the current FTL
- `localization` or for a migration specification in `transforms`.
+ reference, first check for an existing translation in the current FTL
+ `localization` and use it if it's present; then if the string has
+ a transform defined in the migration specification and if it's in the
+ currently processed changeset, evaluate the transform.
"""
def merge_body(body):
return [
entry
for entry in map(merge_entry, body)
if entry is not None
]
--- a/third_party/python/fluent/fluent/migrate/transforms.py
+++ b/third_party/python/fluent/fluent/migrate/transforms.py
@@ -3,54 +3,54 @@
Transforms are AST nodes which describe how legacy translations should be
migrated. They are created inert and only return the migrated AST nodes when
they are evaluated by a MergeContext.
All Transforms evaluate to Fluent Patterns. This makes them suitable for
defining migrations of values of message, attributes and variants. The special
CONCAT Transform is capable of joining multiple Patterns returned by evaluating
-other Transforms into a single Pattern. It can also concatenate Fluent
-Expressions, like MessageReferences and ExternalArguments.
+other Transforms into a single Pattern. It can also concatenate Pattern
+elements: TextElements and Placeables.
The COPY, REPLACE and PLURALS Transforms inherit from Source which is a special
AST Node defining the location (the file path and the id) of the legacy
translation. During the migration, the current MergeContext scans the
migration spec for Source nodes and extracts the information about all legacy
-translations being migrated. Thus,
+translations being migrated. For instance,
COPY('file.dtd', 'hello')
is equivalent to:
- LITERAL(Source('file.dtd', 'hello'))
-
-where LITERAL is a helper defined in the helpers.py module for creating Fluent
-Patterns from the text passed as the argument.
+ FTL.Pattern([
+ FTL.TextElement(Source('file.dtd', 'hello'))
+ ])
-The LITERAL helper and the special REPLACE_IN_TEXT Transforms are useful for
-working with text rather than (path, key) source definitions. This is the case
-when the migrated translation requires some hardcoded text, e.g. <a> and </a>
-when multiple translations become a single one with a DOM overlay.
+Sometimes it's useful to work with text rather than (path, key) source
+definitions. This is the case when the migrated translation requires some
+hardcoded text, e.g. <a> and </a> when multiple translations become a single
+one with a DOM overlay. In such cases it's best to use the AST nodes:
FTL.Message(
id=FTL.Identifier('update-failed'),
value=CONCAT(
COPY('aboutDialog.dtd', 'update.failed.start'),
- LITERAL('<a>'),
+ FTL.TextElement('<a>'),
COPY('aboutDialog.dtd', 'update.failed.linkText'),
- LITERAL('</a>'),
+ FTL.TextElement('</a>'),
COPY('aboutDialog.dtd', 'update.failed.end'),
)
)
The REPLACE_IN_TEXT Transform also takes text as input, making in possible to
pass it as the foreach function of the PLURALS Transform. In this case, each
slice of the plural string will be run through a REPLACE_IN_TEXT operation.
-Those slices are strings, so a REPLACE(path, key, …) isn't suitable for them.
+Those slices are strings, so a REPLACE(path, key, …) wouldn't be suitable for
+them.
FTL.Message(
FTL.Identifier('delete-all'),
value=PLURALS(
'aboutDownloads.dtd',
'deleteAll',
EXTERNAL_ARGUMENT('num'),
lambda text: REPLACE_IN_TEXT(
@@ -61,17 +61,23 @@ Those slices are strings, so a REPLACE(path, key, …) isn't suitable for them.
)
)
)
"""
from __future__ import unicode_literals
import fluent.syntax.ast as FTL
-from .helpers import LITERAL
+from .errors import NotSupportedError
+
+
+def pattern_from_text(value):
+ return FTL.Pattern([
+ FTL.TextElement(value)
+ ])
def evaluate(ctx, node):
def eval_node(subnode):
if isinstance(subnode, Transform):
return subnode(ctx)
else:
return subnode
@@ -82,50 +88,48 @@ def evaluate(ctx, node):
class Transform(FTL.BaseNode):
def __call__(self, ctx):
raise NotImplementedError
class Source(Transform):
"""Declare the source translation to be migrated with other transforms.
- When evaluated `Source` returns a simple string value. All \\uXXXX from
- the original translations are converted beforehand to the literal
- characters they encode.
+ When evaluated, `Source` returns a simple string value. Escaped characters
+ are unescaped by the compare-locales parser according to the file format:
- HTML entities are left unchanged for now because we can't know if they
- should be converted to the characters they represent or not. Consider the
- following example in which `&` could be replaced with the literal `&`:
+ - in properties files: \\uXXXX,
+ - in DTD files: known named, decimal, and hexadecimal HTML entities.
- Privacy & History
+ Consult the following files for the list of known named HTML entities:
- vs. these two examples where the HTML encoding should be preserved:
-
- Erreur !
- Use /help <command> for more information.
+ https://github.com/python/cpython/blob/2.7/Lib/htmlentitydefs.py
+ https://github.com/python/cpython/blob/3.6/Lib/html/entities.py
"""
- # XXX Perhaps there's a strict subset of HTML entities which must or must
- # not be replaced?
+ def __init__(self, path, key):
+ if path.endswith('.ftl'):
+ raise NotSupportedError(
+ 'Migrating translations from Fluent files is not supported '
+ '({})'.format(path))
- def __init__(self, path, key):
self.path = path
self.key = key
def __call__(self, ctx):
return ctx.get_source(self.path, self.key)
class COPY(Source):
"""Create a Pattern with the translation value from the given source."""
def __call__(self, ctx):
source = super(self.__class__, self).__call__(ctx)
- return LITERAL(source)
+ return pattern_from_text(source)
class REPLACE_IN_TEXT(Transform):
"""Replace various placeables in the translation with FTL placeables.
The original placeables are defined as keys on the `replacements` dict.
For each key the value is defined as a list of FTL Expressions to be
interpolated.
@@ -205,29 +209,38 @@ class REPLACE(Source):
class PLURALS(Source):
"""Create a Pattern with plurals from given source.
Build an `FTL.SelectExpression` with the supplied `selector` and variants
extracted from the source. The source needs to be a semicolon-separated
list of variants. Each variant will be run through the `foreach` function,
- which should return an `FTL.Node` or a `Transform`.
+ which should return an `FTL.Node` or a `Transform`. By default, the
+ `foreach` function transforms the source text into a Pattern with a single
+ TextElement.
"""
- def __init__(self, path, key, selector, foreach=LITERAL):
+ def __init__(self, path, key, selector, foreach=pattern_from_text):
super(self.__class__, self).__init__(path, key)
self.selector = selector
self.foreach = foreach
def __call__(self, ctx):
value = super(self.__class__, self).__call__(ctx)
selector = evaluate(ctx, self.selector)
variants = value.split(';')
keys = ctx.plural_categories
+
+ # A special case for languages with one plural category. We don't need
+ # to insert a SelectExpression at all for them.
+ if len(keys) == len(variants) == 1:
+ variant, = variants
+ return evaluate(ctx, self.foreach(variant))
+
last_index = min(len(variants), len(keys)) - 1
def createVariant(zipped_enum):
index, (key, variant) = zipped_enum
# Run the legacy variant through `foreach` which returns an
# `FTL.Node` describing the transformation required for each
# variant. Then evaluate it to a migrated FTL node.
value = evaluate(ctx, self.foreach(variant))
@@ -259,17 +272,17 @@ class CONCAT(Transform):
acc.extend(cur.elements)
return acc
elif (isinstance(cur, FTL.TextElement) or
isinstance(cur, FTL.Placeable)):
acc.append(cur)
return acc
raise RuntimeError(
- 'CONCAT accepts FTL Patterns and Expressions.'
+ 'CONCAT accepts FTL Patterns, TextElements and Placeables.'
)
# Merge adjecent `FTL.TextElement` nodes.
def merge_adjecent_text(acc, cur):
if type(cur) == FTL.TextElement and len(acc):
last = acc[-1]
if type(last) == FTL.TextElement:
last.value += cur.value
deleted file mode 100644
--- a/third_party/python/fluent/setup.cfg
+++ /dev/null
@@ -1,4 +0,0 @@
-[egg_info]
-tag_build =
-tag_date = 0
-
deleted file mode 100644
--- a/third_party/python/fluent/setup.py
+++ /dev/null
@@ -1,24 +0,0 @@
-#!/usr/bin/env python
-
-from setuptools import setup
-
-setup(name='fluent',
- version='0.4.2',
- description='Localization library for expressive translations.',
- author='Mozilla',
- author_email='l10n-drivers@mozilla.org',
- license='APL 2',
- url='https://github.com/projectfluent/python-fluent',
- keywords=['fluent', 'localization', 'l10n'],
- classifiers=[
- 'Development Status :: 3 - Alpha',
- 'Intended Audience :: Developers',
- 'License :: OSI Approved :: Apache Software License',
- 'Programming Language :: Python :: 2.7',
- 'Programming Language :: Python :: 3.5',
- ],
- packages=['fluent', 'fluent.syntax', 'fluent.migrate'],
- package_data={
- 'fluent.migrate': ['cldr_data/*']
- }
- )
new file mode 100755
--- /dev/null
+++ b/third_party/python/fluent/tools/fluentfmt.py
@@ -0,0 +1,23 @@
+#!/usr/bin/python
+
+import sys
+
+sys.path.append('./')
+import codecs
+from fluent.syntax import parse, serialize
+
+
+def read_file(path):
+ with codecs.open(path, 'r', encoding='utf-8') as file:
+ text = file.read()
+ return text
+
+
+def pretty_print(fileType, data):
+ ast = parse(data)
+ print(serialize(ast))
+
+if __name__ == "__main__":
+ file_type = 'ftl'
+ f = read_file(sys.argv[1])
+ pretty_print(file_type, f)
new file mode 100644
--- /dev/null
+++ b/third_party/python/fluent/tools/migrate/README.md
@@ -0,0 +1,39 @@
+# Migration Tools
+
+`migrate-l10n.py` is a CLI script which uses the `fluent.migrate` module under
+the hood to run migrations on existing translations.
+
+## Examples
+
+The `examples/` directory contains a number of sample migrations. To run them
+you'll need at least one clone of a localization repository, e.g. from
+https://hg.mozilla.org/l10n-central/.
+
+Amend your `PYTHONPATH` to make sure that all `fluent.*` modules can be found:
+
+ $ export PYTHONPATH=$(pwd)/../..:$PYTHONPATH
+
+Then run migrations passing the `examples` directory as the reference:
+
+ $ ./migrate-l10n.py --lang it --reference-dir examples --localization-dir ~/moz/l10n-central/it examples.about_dialog examples.about_downloads examples.bug_1291693
+
+Here's what the output should look like:
+
+ Annotating /home/stas/moz/l10n-central/it
+ Running migration examples.bug_1291693
+ Writing to /home/stas/moz/l10n-central/it/browser/branding/official/brand.ftl
+ Committing changeset: Bug 1291693 - Migrate the menubar to FTL, part 1
+ Writing to /home/stas/moz/l10n-central/it/browser/menubar.ftl
+ Writing to /home/stas/moz/l10n-central/it/browser/toolbar.ftl
+ Writing to /home/stas/moz/l10n-central/it/browser/branding/official/brand.ftl
+ Committing changeset: Bug 1291693 - Migrate the menubar to FTL, part 2
+ Running migration examples.about_dialog
+ Writing to /home/stas/moz/l10n-central/it/browser/about_dialog.ftl
+ Committing changeset: Migrate about:dialog, part 1
+ Running migration examples.about_downloads
+ Writing to /home/stas/moz/l10n-central/it/mobile/about_downloads.ftl
+ Committing changeset: Migrate about:download in Firefox for Android, part 1
+ Writing to /home/stas/moz/l10n-central/it/mobile/about_downloads.ftl
+ Committing changeset: Migrate about:download in Firefox for Android, part 2
+ Writing to /home/stas/moz/l10n-central/it/mobile/about_downloads.ftl
+ Committing changeset: Migrate about:download in Firefox for Android, part 3
new file mode 100644
--- /dev/null
+++ b/third_party/python/fluent/tools/migrate/blame.py
@@ -0,0 +1,60 @@
+import argparse
+import json
+import hglib
+from hglib.util import b, cmdbuilder
+from compare_locales.parser import getParser, Junk
+
+
+class Blame(object):
+ def __init__(self, repopath):
+ self.client = hglib.open(repopath)
+ self.users = []
+ self.blame = {}
+
+ def main(self):
+ args = cmdbuilder(
+ b('annotate'), self.client.root(), d=True, u=True, T='json')
+ blame_json = ''.join(self.client.rawcommand(args))
+ file_blames = json.loads(blame_json)
+
+ for file_blame in file_blames:
+ self.handleFile(file_blame)
+
+ return {'authors': self.users,
+ 'blame': self.blame}
+
+ def handleFile(self, file_blame):
+ abspath = file_blame['abspath']
+
+ try:
+ parser = getParser(abspath)
+ except UserWarning:
+ return
+
+ self.blame[abspath] = {}
+
+ parser.readFile(file_blame['path'])
+ entities, emap = parser.parse()
+ for e in entities:
+ if isinstance(e, Junk):
+ continue
+ entity_lines = file_blame['lines'][
+ (e.value_position()[0] - 1):e.value_position(-1)[0]
+ ]
+ # ignore timezone
+ entity_lines.sort(key=lambda blame: -blame['date'][0])
+ line_blame = entity_lines[0]
+ user = line_blame['user']
+ timestamp = line_blame['date'][0] # ignore timezone
+ if user not in self.users:
+ self.users.append(user)
+ userid = self.users.index(user)
+ self.blame[abspath][e.key] = [userid, timestamp]
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser()
+ parser.add_argument("repopath")
+ args = parser.parse_args()
+ blame = Blame(args.repopath)
+ blimey = blame.main()
+ print(json.dumps(blimey, indent=4, separators=(',', ': ')))
new file mode 100644
--- /dev/null
+++ b/third_party/python/fluent/tools/migrate/examples/about_dialog.ftl
@@ -0,0 +1,12 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+update-failed =
+ Update failed. <label>Download the latest version</label>
+channel-description =
+ You are currently on the <label>{ $channelName }</label> update channel.
+community =
+ { brand-short-name } is designed by <label>{ vendor-short-name }</label>,
+ a <label>global community</label> working together to keep the Web
+ open, public and accessible to all.
new file mode 100644
--- /dev/null
+++ b/third_party/python/fluent/tools/migrate/examples/about_dialog.py
@@ -0,0 +1,85 @@
+# coding=utf8
+
+import fluent.syntax.ast as FTL
+from fluent.migrate import (
+ CONCAT, EXTERNAL_ARGUMENT, MESSAGE_REFERENCE, COPY, REPLACE
+)
+
+
+def migrate(ctx):
+ """Migrate about:dialog, part {index}"""
+
+ ctx.add_transforms('browser/about_dialog.ftl', 'about_dialog.ftl', [
+ FTL.Message(
+ id=FTL.Identifier('update-failed'),
+ value=CONCAT(
+ COPY(
+ 'browser/chrome/browser/aboutDialog.dtd',
+ 'update.failed.start'
+ ),
+ FTL.TextElement('<a>'),
+ COPY(
+ 'browser/chrome/browser/aboutDialog.dtd',
+ 'update.failed.linkText'
+ ),
+ FTL.TextElement('</a>'),
+ COPY(
+ 'browser/chrome/browser/aboutDialog.dtd',
+ 'update.failed.end'
+ )
+ )
+ ),
+ FTL.Message(
+ id=FTL.Identifier('channel-description'),
+ value=CONCAT(
+ COPY(
+ 'browser/chrome/browser/aboutDialog.dtd',
+ 'channel.description.start'
+ ),
+ FTL.Placeable(EXTERNAL_ARGUMENT('channelname')),
+ COPY(
+ 'browser/chrome/browser/aboutDialog.dtd',
+ 'channel.description.end'
+ )
+ )
+ ),
+ FTL.Message(
+ id=FTL.Identifier('community'),
+ value=CONCAT(
+ REPLACE(
+ 'browser/chrome/browser/aboutDialog.dtd',
+ 'community.start2',
+ {
+ '&brandShortName;': MESSAGE_REFERENCE(
+ 'brand-short-name'
+ )
+ }
+ ),
+ FTL.TextElement('<a>'),
+ REPLACE(
+ 'browser/chrome/browser/aboutDialog.dtd',
+ 'community.mozillaLink',
+ {
+ '&vendorShortName;': MESSAGE_REFERENCE(
+ 'vendor-short-name'
+ )
+ }
+ ),
+ FTL.TextElement('</a>'),
+ COPY(
+ 'browser/chrome/browser/aboutDialog.dtd',
+ 'community.middle2'
+ ),
+ FTL.TextElement('<a>'),
+ COPY(
+ 'browser/chrome/browser/aboutDialog.dtd',
+ 'community.creditsLink'
+ ),
+ FTL.TextElement('</a>'),
+ COPY(
+ 'browser/chrome/browser/aboutDialog.dtd',
+ 'community.end3'
+ )
+ )
+ ),
+ ])
new file mode 100644
--- /dev/null
+++ b/third_party/python/fluent/tools/migrate/examples/about_downloads.ftl
@@ -0,0 +1,38 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+title = Downloads
+header = Your Downloads
+empty = No Downloads
+
+open-menuitem
+ .label = Open
+retry-menuitem
+ .label = Retry
+remove-menuitem
+ .label = Delete
+pause-menuitem
+ .label = Pause
+resume-menuitem
+ .label = Resume
+cancel-menuitem
+ .label = Cancel
+remove-all-menuitem
+ .label = Delete All
+
+delete-all-title = Delete All
+delete-all-message =
+ { $num ->
+ [1] Delete this download?
+ *[other] Delete { $num } downloads?
+ }
+
+download-state-downloading = Downloading…
+download-state-canceled = Canceled
+download-state-failed = Failed
+download-state-paused = Paused
+download-state-starting = Starting…
+download-state-unknown = Unknown
+
+download-size-unknown = Unknown size
new file mode 100644
--- /dev/null
+++ b/third_party/python/fluent/tools/migrate/examples/about_downloads.py
@@ -0,0 +1,179 @@
+# coding=utf8
+
+import fluent.syntax.ast as FTL
+from fluent.migrate import EXTERNAL_ARGUMENT, COPY, PLURALS, REPLACE_IN_TEXT
+
+
+def migrate(ctx):
+ """Migrate about:download in Firefox for Android, part {index}"""
+
+ ctx.add_transforms('mobile/about_downloads.ftl', 'about_downloads.ftl', [
+ FTL.Message(
+ id=FTL.Identifier('title'),
+ value=COPY(
+ 'mobile/android/chrome/aboutDownloads.dtd',
+ 'aboutDownloads.title'
+ )
+ ),
+ FTL.Message(
+ id=FTL.Identifier('header'),
+ value=COPY(
+ 'mobile/android/chrome/aboutDownloads.dtd',
+ 'aboutDownloads.header'
+ )
+ ),
+ FTL.Message(
+ id=FTL.Identifier('empty'),
+ value=COPY(
+ 'mobile/android/chrome/aboutDownloads.dtd',
+ 'aboutDownloads.empty'
+ )
+ ),
+ FTL.Message(
+ id=FTL.Identifier('open-menuitem'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'mobile/android/chrome/aboutDownloads.dtd',
+ 'aboutDownloads.open'
+ )
+ )
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('retry-menuitem'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'mobile/android/chrome/aboutDownloads.dtd',
+ 'aboutDownloads.retry'
+ )
+ )
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('remove-menuitem'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'mobile/android/chrome/aboutDownloads.dtd',
+ 'aboutDownloads.remove'
+ )
+ )
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('pause-menuitem'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'mobile/android/chrome/aboutDownloads.dtd',
+ 'aboutDownloads.pause'
+ )
+ )
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('resume-menuitem'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'mobile/android/chrome/aboutDownloads.dtd',
+ 'aboutDownloads.resume'
+ )
+ )
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('cancel-menuitem'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'mobile/android/chrome/aboutDownloads.dtd',
+ 'aboutDownloads.cancel'
+ )
+ )
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('remove-all-menuitem'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'mobile/android/chrome/aboutDownloads.dtd',
+ 'aboutDownloads.removeAll'
+ )
+ )
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('delete-all-title'),
+ value=COPY(
+ 'mobile/android/chrome/aboutDownloads.properties',
+ 'downloadAction.deleteAll'
+ )
+ ),
+ FTL.Message(
+ id=FTL.Identifier('delete-all-message'),
+ value=PLURALS(
+ 'mobile/android/chrome/aboutDownloads.properties',
+ 'downloadMessage.deleteAll',
+ EXTERNAL_ARGUMENT('num'),
+ lambda text: REPLACE_IN_TEXT(
+ text,
+ {
+ '#1': EXTERNAL_ARGUMENT('num')
+ }
+ )
+ )
+ ),
+ FTL.Message(
+ id=FTL.Identifier('download-state-downloading'),
+ value=COPY(
+ 'mobile/android/chrome/aboutDownloads.properties',
+ 'downloadState.downloading'
+ )
+ ),
+ FTL.Message(
+ id=FTL.Identifier('download-state-canceled'),
+ value=COPY(
+ 'mobile/android/chrome/aboutDownloads.properties',
+ 'downloadState.canceled'
+ )
+ ),
+ FTL.Message(
+ id=FTL.Identifier('download-state-failed'),
+ value=COPY(
+ 'mobile/android/chrome/aboutDownloads.properties',
+ 'downloadState.failed'
+ )
+ ),
+ FTL.Message(
+ id=FTL.Identifier('download-state-paused'),
+ value=COPY(
+ 'mobile/android/chrome/aboutDownloads.properties',
+ 'downloadState.paused'
+ )
+ ),
+ FTL.Message(
+ id=FTL.Identifier('download-state-starting'),
+ value=COPY(
+ 'mobile/android/chrome/aboutDownloads.properties',
+ 'downloadState.starting'
+ )
+ ),
+ FTL.Message(
+ id=FTL.Identifier('download-size-unknown'),
+ value=COPY(
+ 'mobile/android/chrome/aboutDownloads.properties',
+ 'downloadState.unknownSize'
+ )
+ ),
+ ])
new file mode 100644
--- /dev/null
+++ b/third_party/python/fluent/tools/migrate/examples/brand.ftl
@@ -0,0 +1,13 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+brand-shorter-name = Firefox
+brand-short-name = Firefox
+brand-full-name = Mozilla Firefox
+vendor-short-name = Mozilla
+
+trademark-info =
+ Firefox and the Firefox logos are trademarks of the Mozilla Foundation.
+
+sync-brand-short-name = Sync
new file mode 100644
--- /dev/null
+++ b/third_party/python/fluent/tools/migrate/examples/bug_1291693.py
@@ -0,0 +1,1917 @@
+# coding=utf8
+
+import fluent.syntax.ast as FTL
+from fluent.migrate import MESSAGE_REFERENCE, COPY, REPLACE
+
+
+def migrate(ctx):
+ """Bug 1291693 - Migrate the menubar to FTL, part {index}"""
+
+ ctx.add_transforms('browser/menubar.ftl', 'menubar.ftl', [
+ FTL.Message(
+ id=FTL.Identifier('file-menu'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'fileMenu.label',
+ )
+ ),
+ FTL.Attribute(
+ FTL.Identifier('accesskey'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'fileMenu.accesskey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('tab-menuitem'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'tabCmd.label',
+ )
+ ),
+ FTL.Attribute(
+ FTL.Identifier('accesskey'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'tabCmd.accesskey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('tab-command'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('key'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'tabCmd.commandkey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('new-user-context-menu'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'newUserContext.label',
+ )
+ ),
+ FTL.Attribute(
+ FTL.Identifier('accesskey'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'newUserContext.accesskey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('new-navigator-menuitem'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'newNavigatorCmd.label',
+ )
+ ),
+ FTL.Attribute(
+ FTL.Identifier('accesskey'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'newNavigatorCmd.accesskey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('new-navigator-command'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('key'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'newNavigatorCmd.key',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('new-private-window-menuitem'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'newPrivateWindow.label',
+ )
+ ),
+ FTL.Attribute(
+ FTL.Identifier('accesskey'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'newPrivateWindow.accesskey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('new-non-remote-window-menuitem'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'newNonRemoteWindow.label',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('open-location-menuitem'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'openLocationCmd.label',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('open-file-menuitem'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'openFileCmd.label',
+ )
+ ),
+ FTL.Attribute(
+ FTL.Identifier('accesskey'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'openFileCmd.accesskey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('open-file-command'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('key'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'openFileCmd.commandkey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('close-menuitem'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'closeCmd.label',
+ )
+ ),
+ FTL.Attribute(
+ FTL.Identifier('accesskey'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'closeCmd.accesskey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('close-command'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('key'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'closeCmd.key',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('close-window-menuitem'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'closeWindow.label',
+ )
+ ),
+ FTL.Attribute(
+ FTL.Identifier('accesskey'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'closeWindow.accesskey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('save-page-menuitem'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'savePageCmd.label',
+ )
+ ),
+ FTL.Attribute(
+ FTL.Identifier('accesskey'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'savePageCmd.accesskey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('save-page-command'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('key'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'savePageCmd.commandkey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('email-page-menuitem'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'emailPageCmd.label',
+ )
+ ),
+ FTL.Attribute(
+ FTL.Identifier('accesskey'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'emailPageCmd.accesskey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('print-setup-menuitem'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'printSetupCmd.label',
+ )
+ ),
+ FTL.Attribute(
+ FTL.Identifier('accesskey'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'printSetupCmd.accesskey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('print-preview-menuitem'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'printPreviewCmd.label',
+ )
+ ),
+ FTL.Attribute(
+ FTL.Identifier('accesskey'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'printPreviewCmd.accesskey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('print-menuitem'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'printCmd.label',
+ )
+ ),
+ FTL.Attribute(
+ FTL.Identifier('accesskey'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'printCmd.accesskey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('print-command'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('key'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'printCmd.commandkey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('go-offline-menuitem'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'goOfflineCmd.label',
+ )
+ ),
+ FTL.Attribute(
+ FTL.Identifier('accesskey'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'goOfflineCmd.accesskey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('quit-application-menuitem'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'quitApplicationCmd.label',
+ )
+ ),
+ FTL.Attribute(
+ FTL.Identifier('accesskey'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'quitApplicationCmd.accesskey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('quit-application-menuitem-win'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'quitApplicationCmdWin2.label',
+ )
+ ),
+ FTL.Attribute(
+ FTL.Identifier('accesskey'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'quitApplicationCmdWin2.accesskey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('quit-application-menuitem-mac'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'quitApplicationCmdMac2.label',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('quit-application-command-unix'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('key'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'quitApplicationCmdUnix.key',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('edit-menu'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'editMenu.label',
+ )
+ ),
+ FTL.Attribute(
+ FTL.Identifier('accesskey'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'editMenu.accesskey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('undo-menuitem'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'undoCmd.label',
+ )
+ ),
+ FTL.Attribute(
+ FTL.Identifier('accesskey'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'undoCmd.accesskey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('undo-command'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('key'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'undoCmd.key',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('redo-menuitem'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'redoCmd.label',
+ )
+ ),
+ FTL.Attribute(
+ FTL.Identifier('accesskey'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'redoCmd.accesskey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('redo-command'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('key'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'redoCmd.key',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('cut-menuitem'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'cutCmd.label',
+ )
+ ),
+ FTL.Attribute(
+ FTL.Identifier('accesskey'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'cutCmd.accesskey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('cut-command'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('key'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'cutCmd.key',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('copy-menuitem'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'copyCmd.label',
+ )
+ ),
+ FTL.Attribute(
+ FTL.Identifier('accesskey'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'copyCmd.accesskey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('copy-command'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('key'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'copyCmd.key',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('paste-menuitem'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'pasteCmd.label',
+ )
+ ),
+ FTL.Attribute(
+ FTL.Identifier('accesskey'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'pasteCmd.accesskey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('paste-command'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('key'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'pasteCmd.key',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('delete-menuitem'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'deleteCmd.label',
+ )
+ ),
+ FTL.Attribute(
+ FTL.Identifier('accesskey'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'deleteCmd.accesskey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('select-all-menuitem'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'selectAllCmd.label',
+ )
+ ),
+ FTL.Attribute(
+ FTL.Identifier('accesskey'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'selectAllCmd.accesskey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('select-all-command'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('key'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'selectAllCmd.key',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('find-on-menuitem'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'findOnCmd.label',
+ )
+ ),
+ FTL.Attribute(
+ FTL.Identifier('accesskey'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'findOnCmd.accesskey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('find-on-command'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('key'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'findOnCmd.commandkey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('find-again-menuitem'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'findAgainCmd.label',
+ )
+ ),
+ FTL.Attribute(
+ FTL.Identifier('accesskey'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'findAgainCmd.accesskey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('find-again-command1'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('key'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'findAgainCmd.commandkey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('find-again-command2'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('key'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'findAgainCmd.commandkey2',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('find-selection-command'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('key'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'findSelectionCmd.commandkey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('bidi-switch-text-direction-menuitem'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'bidiSwitchTextDirectionItem.label',
+ )
+ ),
+ FTL.Attribute(
+ FTL.Identifier('accesskey'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'bidiSwitchTextDirectionItem.accesskey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('bidi-switch-text-direction-command'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('key'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'bidiSwitchTextDirectionItem.commandkey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('preferences-menuitem'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'preferencesCmd2.label',
+ )
+ ),
+ FTL.Attribute(
+ FTL.Identifier('accesskey'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'preferencesCmd2.accesskey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('preferences-menuitem-unix'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'preferencesCmdUnix.label',
+ )
+ ),
+ FTL.Attribute(
+ FTL.Identifier('accesskey'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'preferencesCmdUnix.accesskey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('view-menu'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'viewMenu.label',
+ )
+ ),
+ FTL.Attribute(
+ FTL.Identifier('accesskey'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'viewMenu.accesskey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('view-toolbar-menu'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'viewToolbarsMenu.label',
+ )
+ ),
+ FTL.Attribute(
+ FTL.Identifier('accesskey'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'viewToolbarsMenu.accesskey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('view-sidebar-menu'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'viewSidebarMenu.label',
+ )
+ ),
+ FTL.Attribute(
+ FTL.Identifier('accesskey'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'viewSidebarMenu.accesskey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('view-customize-toolbar-menuitem'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'viewCustomizeToolbar.label',
+ )
+ ),
+ FTL.Attribute(
+ FTL.Identifier('accesskey'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'viewCustomizeToolbar.accesskey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('full-zoom-menu'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'fullZoom.label',
+ )
+ ),
+ FTL.Attribute(
+ FTL.Identifier('accesskey'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'fullZoom.accesskey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('full-zoom-enlarge-menuitem'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'fullZoomEnlargeCmd.label',
+ )
+ ),
+ FTL.Attribute(
+ FTL.Identifier('accesskey'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'fullZoomEnlargeCmd.accesskey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('full-zoom-enlarge-command1'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('key'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'fullZoomEnlargeCmd.commandkey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('full-zoom-enlarge-command2'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('key'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'fullZoomEnlargeCmd.commandkey2',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('full-zoom-enlarge-command3'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('key'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'fullZoomEnlargeCmd.commandkey3',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('full-zoom-reduce-menuitem'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'fullZoomReduceCmd.label',
+ )
+ ),
+ FTL.Attribute(
+ FTL.Identifier('accesskey'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'fullZoomReduceCmd.accesskey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('full-zoom-reduce-command1'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('key'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'fullZoomReduceCmd.commandkey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('full-zoom-reduce-command2'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('key'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'fullZoomReduceCmd.commandkey2',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('full-zoom-reset-menuitem'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'fullZoomResetCmd.label',
+ )
+ ),
+ FTL.Attribute(
+ FTL.Identifier('accesskey'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'fullZoomResetCmd.accesskey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('full-zoom-reset-command1'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('key'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'fullZoomResetCmd.commandkey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('full-zoom-reset-command2'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('key'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'fullZoomResetCmd.commandkey2',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('full-zoom-toggle-menuitem'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'fullZoomToggleCmd.label',
+ )
+ ),
+ FTL.Attribute(
+ FTL.Identifier('accesskey'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'fullZoomToggleCmd.accesskey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('page-style-menu'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'pageStyleMenu.label',
+ )
+ ),
+ FTL.Attribute(
+ FTL.Identifier('accesskey'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'pageStyleMenu.accesskey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('page-style-no-style-menuitem'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'pageStyleNoStyle.label',
+ )
+ ),
+ FTL.Attribute(
+ FTL.Identifier('accesskey'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'pageStyleNoStyle.accesskey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('page-style-persistent-only-menuitem'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'pageStylePersistentOnly.label',
+ )
+ ),
+ FTL.Attribute(
+ FTL.Identifier('accesskey'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'pageStylePersistentOnly.accesskey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('show-all-tabs-menuitem'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'showAllTabsCmd.label',
+ )
+ ),
+ FTL.Attribute(
+ FTL.Identifier('accesskey'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'showAllTabsCmd.accesskey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('bidi-switch-page-direction-menuitem'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'bidiSwitchPageDirectionItem.label',
+ )
+ ),
+ FTL.Attribute(
+ FTL.Identifier('accesskey'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'bidiSwitchPageDirectionItem.accesskey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('enter-full-screen-menuitem'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'enterFullScreenCmd.label',
+ )
+ ),
+ FTL.Attribute(
+ FTL.Identifier('accesskey'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'enterFullScreenCmd.accesskey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('exit-full-screen-menuitem'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'exitFullScreenCmd.label',
+ )
+ ),
+ FTL.Attribute(
+ FTL.Identifier('accesskey'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'exitFullScreenCmd.accesskey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('full-screen-menuitem'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'fullScreenCmd.label',
+ )
+ ),
+ FTL.Attribute(
+ FTL.Identifier('accesskey'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'fullScreenCmd.accesskey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('full-screen-command'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('key'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'fullScreenCmd.macCommandKey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('history-menu'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'historyMenu.label',
+ )
+ ),
+ FTL.Attribute(
+ FTL.Identifier('accesskey'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'historyMenu.accesskey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('show-all-history-menuitem'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'showAllHistoryCmd2.label',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('show-all-history-command'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('key'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'showAllHistoryCmd.commandkey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('clear-recent-history-menuitem'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'clearRecentHistory.label',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('history-synced-tabs-menuitem'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'syncTabsMenu3.label',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('history-restore-last-session-menuitem'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'historyRestoreLastSession.label',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('history-undo-menu'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'historyUndoMenu.label',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('history-undo-window-menu'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'historyUndoWindowMenu.label',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('bookmarks-menu'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'bookmarksMenu.label',
+ )
+ ),
+ FTL.Attribute(
+ FTL.Identifier('accesskey'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'bookmarksMenu.accesskey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('show-all-bookmarks-menuitem'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'showAllBookmarks2.label',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('show-all-bookmarks-command'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('key'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'bookmarksCmd.commandkey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('show-all-bookmarks-command-gtk'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('key'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'bookmarksGtkCmd.commandkey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('bookmark-this-page-broadcaster'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'bookmarkThisPageCmd.label',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('edit-this-page-broadcaster'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'editThisBookmarkCmd.label',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('bookmark-this-page-command'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('key'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'bookmarkThisPageCmd.commandkey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('subscribe-to-page-menuitem'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'subscribeToPageMenupopup.label',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('subscribe-to-page-menupopup'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'subscribeToPageMenupopup.label',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('add-cur-pages-menuitem'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'addCurPagesCmd.label',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('recent-bookmarks-menuitem'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'recentBookmarks.label',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('other-bookmarks-menu'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'otherBookmarksCmd.label',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('personalbar-menu'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'personalbarCmd.label',
+ )
+ ),
+ FTL.Attribute(
+ FTL.Identifier('accesskey'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'personalbarCmd.accesskey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('tools-menu'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'toolsMenu.label',
+ )
+ ),
+ FTL.Attribute(
+ FTL.Identifier('accesskey'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'toolsMenu.accesskey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('downloads-menuitem'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'downloads.label',
+ )
+ ),
+ FTL.Attribute(
+ FTL.Identifier('accesskey'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'downloads.accesskey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('downloads-command'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('key'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'downloads.commandkey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('downloads-command-unix'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('key'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'downloadsUnix.commandkey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('addons-menuitem'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'addons.label',
+ )
+ ),
+ FTL.Attribute(
+ FTL.Identifier('accesskey'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'addons.accesskey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('addons-command'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('key'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'addons.commandkey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('sync-sign-in-menuitem'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ REPLACE(
+ 'browser/chrome/browser/browser.dtd',
+ 'syncSignIn.label',
+ {
+ '&syncBrand.shortName.label;': MESSAGE_REFERENCE(
+ 'sync-brand-short-name'
+ )
+ }
+ )
+ ),
+ FTL.Attribute(
+ FTL.Identifier('accesskey'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'syncSignIn.accesskey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('sync-sync-now-menuitem'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'syncSyncNowItem.label',
+ )
+ ),
+ FTL.Attribute(
+ FTL.Identifier('accesskey'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'syncSyncNowItem.accesskey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('sync-re-auth-menuitem'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ REPLACE(
+ 'browser/chrome/browser/browser.dtd',
+ 'syncReAuthItem.label',
+ {
+ '&syncBrand.shortName.label;': MESSAGE_REFERENCE(
+ 'sync-brand-short-name'
+ )
+ }
+ )
+ ),
+ FTL.Attribute(
+ FTL.Identifier('accesskey'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'syncReAuthItem.accesskey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('sync-toolbar-button'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'syncToolbarButton.label',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('web-developer-menu'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'webDeveloperMenu.label',
+ )
+ ),
+ FTL.Attribute(
+ FTL.Identifier('accesskey'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'webDeveloperMenu.accesskey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('page-source-broadcaster'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'pageSourceCmd.label',
+ )
+ ),
+ FTL.Attribute(
+ FTL.Identifier('accesskey'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'pageSourceCmd.accesskey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('page-source-command'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('key'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'pageSourceCmd.commandkey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('page-info-menuitem'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'pageInfoCmd.label',
+ )
+ ),
+ FTL.Attribute(
+ FTL.Identifier('accesskey'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'pageInfoCmd.accesskey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('page-info-command'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('key'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'pageInfoCmd.commandkey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('mirror-tab-menu'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'mirrorTabCmd.label',
+ )
+ ),
+ FTL.Attribute(
+ FTL.Identifier('accesskey'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'mirrorTabCmd.accesskey',
+ )
+ ),
+ ]
+ ),
+ ])
+
+ ctx.add_transforms('browser/toolbar.ftl', 'toolbar.ftl', [
+ FTL.Message(
+ id=FTL.Identifier('urlbar-textbox'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('placeholder'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'urlbar.placeholder2',
+ )
+ ),
+ FTL.Attribute(
+ FTL.Identifier('accesskey'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'urlbar.accesskey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('view-bookmarks-broadcaster'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'bookmarksButton.label',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('view-bookmarks-command'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('key'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'bookmarksCmd.commandkey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('view-bookmarks-command-win'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('key'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'bookmarksWinCmd.commandkey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('view-history-broadcaster'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'historyButton.label',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('view-history-command'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('key'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'historySidebarCmd.commandkey',
+ )
+ ),
+ ]
+ ),
+ FTL.Message(
+ id=FTL.Identifier('view-tabs-broadcaster'),
+ attributes=[
+ FTL.Attribute(
+ FTL.Identifier('label'),
+ COPY(
+ 'browser/chrome/browser/browser.dtd',
+ 'syncedTabs.sidebar.label',
+ )
+ ),
+ ]
+ ),
+ ])
+
+ ctx.add_transforms('browser/branding/official/brand.ftl', 'brand.ftl', [
+ FTL.Message(
+ id=FTL.Identifier('brand-shorter-name'),
+ value=COPY(
+ 'browser/branding/official/brand.dtd',
+ 'brandShorterName'
+ )
+ ),
+ FTL.Message(
+ id=FTL.Identifier('brand-short-name'),
+ value=COPY(
+ 'browser/branding/official/brand.dtd',
+ 'brandShortName'
+ )
+ ),
+ FTL.Message(
+ id=FTL.Identifier('brand-full-name'),
+ value=COPY(
+ 'browser/branding/official/brand.dtd',
+ 'brandFullName'
+ )
+ ),
+ FTL.Message(
+ id=FTL.Identifier('vendor-short-name'),
+ value=COPY(
+ 'browser/branding/official/brand.dtd',
+ 'vendorShortName'
+ )
+ ),
+ FTL.Message(
+ id=FTL.Identifier('trademark-info'),
+ value=COPY(
+ 'browser/branding/official/brand.dtd',
+ 'trademarkInfo.part1'
+ )
+ ),
+ FTL.Message(
+ id=FTL.Identifier('sync-brand-short-name'),
+ value=COPY(
+ 'browser/branding/official/brand.properties',
+ 'syncBrandShortName'
+ )
+ ),
+ ])
new file mode 100644
--- /dev/null
+++ b/third_party/python/fluent/tools/migrate/examples/menubar.ftl
@@ -0,0 +1,336 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+[[ File menu ]]
+
+file-menu
+ .label = File
+ .accesskey = F
+tab-menuitem
+ .label = New Tab
+ .accesskey = T
+tab-command
+ .key = t
+new-user-context-menu
+ .label = New Container Tab
+ .accesskey = C
+new-navigator-menuitem
+ .label = New Window
+ .accesskey = N
+new-navigator-command
+ .key = N
+new-private-window-menuitem
+ .label = New Private Window
+ .accesskey = W
+new-non-remote-window-menuitem
+ .label = New Non-e10s Window
+
+// Only displayed on OS X, and only on windows that aren't main browser windows,
+// or when there are no windows but Firefox is still running.
+open-location-menuitem
+ .label = Open Location…
+open-file-menuitem
+ .label = Open File…
+ .accesskey = O
+open-file-command
+ .key = o
+
+close-menuitem
+ .label = Close
+ .accesskey = C
+close-command
+ .key = W
+close-window-menuitem
+ .label = Close Window
+ .accesskey = d
+
+// .accesskey2 is for content area context menu
+save-page-menuitem
+ .label = Save Page As…
+ .accesskey = A
+ .accesskey2 = P
+save-page-command
+ .key = s
+
+email-page-menuitem
+ .label = Email Link…
+ .accesskey = E
+
+print-setup-menuitem
+ .label = Page Setup…
+ .accesskey = u
+print-preview-menuitem
+ .label = Print Preview…
+ .accesskey = v
+print-menuitem
+ .label = Print…
+ .accesskey = P
+print-command
+ .key = p
+
+go-offline-menuitem
+ .label = Work Offline
+ .accesskey = k
+
+quit-application-menuitem
+ .label = Quit
+ .accesskey = Q
+quit-application-menuitem-win
+ .label = Exit
+ .accesskey = x
+quit-application-menuitem-mac
+ .label = Quit { brand-shorter-name }
+// Used by both Linux and OSX builds
+quit-application-command-unix
+ .key = Q
+
+[[ Edit menu ]]
+
+edit-menu
+ .label = Edit
+ .accesskey = E
+undo-menuitem
+ .label = Undo
+ .accesskey = U
+undo-command
+ .key = Z
+redo-menuitem
+ .label = Redo
+ .accesskey = R
+redo-command
+ .key = Y
+cut-menuitem
+ .label = Cut
+ .accesskey = t
+cut-command
+ .key = X
+copy-menuitem
+ .label = Copy
+ .accesskey = C
+copy-command
+ .key = C
+paste-menuitem
+ .label = Paste
+ .accesskey = P
+paste-command
+ .key = V
+delete-menuitem
+ .label = Delete
+ .accesskey = D
+select-all-menuitem
+ .label = Select All
+ .accesskey = A
+select-all-command
+ .key = A
+
+find-on-menuitem
+ .label = Find in This Page…
+ .accesskey = F
+find-on-command
+ .key = f
+find-again-menuitem
+ .label = Find Again
+ .accesskey = g
+find-again-command1
+ .key = g
+find-again-command2
+ .keycode = VK_F3
+find-selection-command
+ .key = e
+
+bidi-switch-text-direction-menuitem
+ .label = Switch Text Direction
+ .accesskey = w
+bidi-switch-text-direction-command
+ .key = X
+
+preferences-menuitem
+ .label = Options
+ .accesskey = O
+preferences-menuitem-unix
+ .label = Preferences
+ .accesskey = n
+
+
+[[ View menu ]]
+
+view-menu
+ .label = View
+ .accesskey = V
+view-toolbars-menu
+ .label = Toolbars
+ .accesskey = T
+view-sidebar-menu
+ .label = Sidebar
+ .accesskey = e
+view-customize-toolbar-menuitem
+ .label = Customize…
+ .accesskey = C
+
+full-zoom-menu
+ .label = Zoom
+ .accesskey = Z
+full-zoom-enlarge-menuitem
+ .label = Zoom In
+ .accesskey = I
+full-zoom-enlarge-command1
+ .key = +
+full-zoom-enlarge-command2
+ .key =
+full-zoom-enlarge-command3
+ .key = ""
+full-zoom-reduce-menuitem
+ .label = Zoom Out
+ .accesskey = O
+full-zoom-reduce-command1
+ .key = -
+full-zoom-reduce-command2
+ .key = ""
+full-zoom-reset-menuitem
+ .label = Reset
+ .accesskey = R
+full-zoom-reset-command1
+ .key = 0
+full-zoom-reset-command2
+ .key = ""
+full-zoom-toggle-menuitem
+ .label = Zoom Text Only
+ .accesskey = T
+
+page-style-menu
+ .label = Page Style
+ .accesskey = y
+page-style-no-style-menuitem
+ .label = No Style
+ .accesskey = n
+page-style-persistent-only-menuitem
+ .label = Basic Page Style
+ .accesskey = b
+
+show-all-tabs-menuitem
+ .label = Show All Tabs
+ .accesskey = A
+bidi-switch-page-direction-menuitem
+ .label = Switch Page Direction
+ .accesskey = D
+
+// Match what Safari and other Apple applications use on OS X Lion.
+[[ Full Screen controls ]]
+
+enter-full-screen-menuitem
+ .label = Enter Full Screen
+ .accesskey = F
+exit-full-screen-menuitem
+ .label = Exit Full Screen
+ .accesskey = F
+full-screen-menuitem
+ .label = Full Screen
+ .accesskey = F
+full-screen-command
+ .key = f
+
+
+[[ History menu ]]
+
+history-menu
+ .label = History
+ .accesskey = s
+show-all-history-menuitem
+ .label = Show All History
+show-all-history-command
+ .key = H
+clear-recent-history-menuitem
+ .label = Clean Recent History…
+history-synced-tabs-menuitem
+ .label = Synced Tabs
+history-restore-last-session-menuitem
+ .label = Restore Previous Session
+history-undo-menu
+ .label = Recently Closed Tabs
+history-undo-window-menu
+ .label = Recently Closed Windows
+
+
+[[ Bookmarks menu ]]
+
+bookmarks-menu
+ .label = Bookmarks
+ .accesskey = B
+show-all-bookmarks-menuitem
+ .label = Show All Bookmarks
+show-all-bookmarks-command
+ .key = b
+// .key should not contain the letters A-F since the are reserved shortcut
+// keys on Linux.
+show-all-bookmarks-command-gtk
+ .key = o
+bookmark-this-page-broadcaster
+ .label = Bookmark This Page
+edit-this-page-broadcaster
+ .label = Edit This Page
+bookmark-this-page-command
+ .key = d
+subscribe-to-page-menuitem
+ .label = Subscribe to This Page…
+subscribe-to-page-menupopup
+ .label = Subscribe to This Page…
+add-cur-pages-menuitem
+ .label = Bookmark All Tabs…
+recent-bookmarks-menuitem
+ .label = Recently Bookmarked
+
+other-bookmarks-menu
+ .label = Other Bookmarks
+personalbar-menu
+ .label = Bookmarks Toolbar
+ .accesskey = B
+
+
+[[ Tools menu ]]
+
+tools-menu
+ .label = Tools
+ .accesskey = T
+downloads-menuitem
+ .label = Downloads
+ .accesskey = D
+downloads-command
+ .key = j
+downloads-command-unix
+ .key = y
+addons-menuitem
+ .label = Add-ons
+ .accesskey = A
+addons-command
+ .key = A
+
+sync-sign-in-menuitem
+ .label = Sign In To { sync-brand-short-name }…
+ .accesskey = Y
+sync-sync-now-menuitem
+ .label = Sync Now
+ .accesskey = S
+sync-re-auth-menuitem
+ .label = Reconnect to { sync-brand-short-name }…
+ .accesskey = R
+sync-toolbar-button
+ .label = Sync
+
+web-developer-menu
+ .label = Web Developer
+ .accesskey = W
+
+page-source-broadcaster
+ .label = Page Source
+ .accesskey = o
+page-source-command
+ .key = u
+page-info-menuitem
+ .label = Page Info
+ .accesskey = I
+page-info-command
+ .key = i
+mirror-tab-menu
+ .label = Mirror Tab
+ .accesskey = m
new file mode 100644
--- /dev/null
+++ b/third_party/python/fluent/tools/migrate/examples/toolbar.ftl
@@ -0,0 +1,24 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+urlbar-textbox
+ .placeholder = Search or enter address
+ .accesskey = d
+
+
+[[ Toolbar items ]]
+
+view-bookmarks-broadcaster
+ .label = Bookmarks
+view-bookmarks-command
+ .key = b
+view-bookmarks-command-win
+ .key = i
+
+view-history-broadcaster
+ .label = History
+view-history-command
+ .key = h
+view-tabs-broadcaster
+ .label = Synced Tabs
new file mode 100755
--- /dev/null
+++ b/third_party/python/fluent/tools/migrate/migrate-l10n.py
@@ -0,0 +1,124 @@
+#!/usr/bin/env python
+# coding=utf8
+
+import os
+import sys
+import json
+import logging
+import argparse
+import importlib
+
+import hglib
+from hglib.util import b
+
+from fluent.migrate import (
+ MergeContext, MigrationError, convert_blame_to_changesets
+)
+from blame import Blame
+
+
+def main(lang, reference_dir, localization_dir, blame, migrations, dry_run):
+ """Run migrations and commit files with the result."""
+ changesets = convert_blame_to_changesets(blame)
+ client = hglib.open(localization_dir)
+
+ for migration in migrations:
+
+ print('Running migration {}'.format(migration.__name__))
+
+ # For each migration create a new context.
+ ctx = MergeContext(lang, reference_dir, localization_dir)
+
+ try:
+ # Add the migration spec.
+ migration.migrate(ctx)
+ except MigrationError as err:
+ sys.exit(err.message)
+
+ # Keep track of how many changesets we're committing.
+ index = 0
+
+ for changeset in changesets:
+ # Run the migration for the changeset.
+ snapshot = ctx.serialize_changeset(changeset['changes'])
+
+ # Did it change any files?
+ if not snapshot:
+ continue
+
+ # Write serialized FTL files to disk.
+ for path, content in snapshot.iteritems():
+ fullpath = os.path.join(localization_dir, path)
+ print(' Writing to {}'.format(fullpath))
+ if not dry_run:
+ fulldir = os.path.dirname(fullpath)
+ if not os.path.isdir(fulldir):
+ os.makedirs(fulldir)
+ with open(fullpath, 'w') as f:
+ f.write(content.encode('utf8'))
+ f.close()
+
+ index += 1
+ author = changeset['author'].encode('utf8')
+ message = migration.migrate.__doc__.format(
+ index=index,
+ author=author
+ )
+
+ print(' Committing changeset: {}'.format(message))
+ if not dry_run:
+ client.commit(
+ b(message), user=b(author), addremove=True
+ )
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser(
+ description='Migrate translations to FTL.'
+ )
+ parser.add_argument(
+ 'migrations', metavar='MIGRATION', type=str, nargs='+',
+ help='migrations to run (Python modules)'
+ )
+ parser.add_argument(
+ '--lang', type=str,
+ help='target language code'
+ )
+ parser.add_argument(
+ '--reference-dir', type=str,
+ help='directory with reference FTL files'
+ )
+ parser.add_argument(
+ '--localization-dir', type=str,
+ help='directory for localization files'
+ )
+ parser.add_argument(
+ '--blame', type=argparse.FileType(), default=None,
+ help='path to a JSON with blame information'
+ )
+ parser.add_argument(
+ '--dry-run', action='store_true',
+ help='do not write to disk nor commit any changes'
+ )
+ parser.set_defaults(dry_run=False)
+
+ logger = logging.getLogger('migrate')
+ logger.setLevel(logging.INFO)
+
+ args = parser.parse_args()
+
+ if args.blame:
+ # Load pre-computed blame from a JSON file.
+ blame = json.load(args.blame)
+ else:
+ # Compute blame right now.
+ print('Annotating {}'.format(args.localization_dir))
+ blame = Blame(args.localization_dir).main()
+
+ main(
+ lang=args.lang,
+ reference_dir=args.reference_dir,
+ localization_dir=args.localization_dir,
+ blame=blame,
+ migrations=map(importlib.import_module, args.migrations),
+ dry_run=args.dry_run
+ )
new file mode 100755
--- /dev/null
+++ b/third_party/python/fluent/tools/parse.py
@@ -0,0 +1,25 @@
+#!/usr/bin/python
+
+import sys
+
+sys.path.append('./')
+import codecs
+from fluent.syntax import parse
+import json
+
+
+def read_file(path):
+ with codecs.open(path, 'r', encoding='utf-8') as file:
+ text = file.read()
+ return text
+
+
+def print_ast(fileType, data):
+ ast = parse(data)
+ print(json.dumps(ast.to_json(), indent=2, ensure_ascii=False))
+
+
+if __name__ == "__main__":
+ file_type = 'ftl'
+ f = read_file(sys.argv[1])
+ print_ast(file_type, f)
new file mode 100755
--- /dev/null
+++ b/third_party/python/fluent/tools/serialize.py
@@ -0,0 +1,23 @@
+#!/usr/bin/python
+
+import sys
+import json
+
+sys.path.append('./')
+import codecs
+from fluent.syntax import ast, serialize
+
+
+def read_json(path):
+ with codecs.open(path, 'r', encoding='utf-8') as file:
+ return json.load(file)
+
+
+def pretty_print(fileType, data):
+ resource = ast.from_json(data)
+ print(serialize(resource))
+
+if __name__ == "__main__":
+ file_type = 'ftl'
+ f = read_json(sys.argv[1])
+ pretty_print(file_type, f)