forked from upstream-mirrors/OpenTracks
Improve state handling, specially in cases where SendActivity gets destroyed.
This commit is contained in:
Executable
+56
@@ -0,0 +1,56 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Copyright 2010 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# This script retrieves the value of a given named string from all indicated
|
||||
# strings.xml files. If invoked in a project directory (a directory with a
|
||||
# 'res' subdirectory), and if no strings.xml files are provided, the script
|
||||
# will automatically analyze res/values*/strings.xml
|
||||
|
||||
PROGNAME=$(basename "$0")
|
||||
|
||||
function usage() {
|
||||
echo "Usage: ${PROGNAME} string_name [file file..]" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
function die() {
|
||||
echo "${PROGNAME}: $@" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
if [[ "$#" -lt 1 ]] ; then
|
||||
usage
|
||||
fi
|
||||
|
||||
name=$1
|
||||
shift
|
||||
|
||||
files=
|
||||
if [[ $# -eq 0 ]] ; then
|
||||
if [[ -d res ]] ; then
|
||||
files=res/values*/strings.xml
|
||||
else
|
||||
die "invoked outside of project root with no file arguments"
|
||||
fi
|
||||
else
|
||||
files="$@"
|
||||
fi
|
||||
|
||||
for file in $files ; do
|
||||
echo === $file
|
||||
xmllint --xpath /resources/string[@name=\"$name\"] $file
|
||||
echo
|
||||
done
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<projectDescription>
|
||||
<name>i18n</name>
|
||||
<comment></comment>
|
||||
<projects>
|
||||
</projects>
|
||||
<buildSpec>
|
||||
<buildCommand>
|
||||
<name>org.python.pydev.PyDevBuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
</buildSpec>
|
||||
<natures>
|
||||
<nature>org.python.pydev.pythonNature</nature>
|
||||
</natures>
|
||||
</projectDescription>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<?eclipse-pydev version="1.0"?>
|
||||
|
||||
<pydev_project>
|
||||
<pydev_property name="org.python.pydev.PYTHON_PROJECT_INTERPRETER">Default</pydev_property>
|
||||
<pydev_property name="org.python.pydev.PYTHON_PROJECT_VERSION">python 2.6</pydev_property>
|
||||
<pydev_pathproperty name="org.python.pydev.PROJECT_SOURCE_PATH">
|
||||
<path>/i18n/src</path>
|
||||
</pydev_pathproperty>
|
||||
</pydev_project>
|
||||
@@ -0,0 +1,45 @@
|
||||
'''
|
||||
Module for dealing with resource files (but not their contents).
|
||||
|
||||
@author: Rodrigo Damazio
|
||||
'''
|
||||
import os.path
|
||||
from glob import glob
|
||||
import re
|
||||
|
||||
MYTRACKS_RES_DIR = 'MyTracks/res'
|
||||
ANDROID_MASTER_VALUES = 'values'
|
||||
ANDROID_VALUES_MASK = 'values-*'
|
||||
|
||||
|
||||
def GetMyTracksDir():
|
||||
'''
|
||||
Returns the directory in which the MyTracks directory is located.
|
||||
'''
|
||||
path = os.getcwd()
|
||||
while not os.path.isdir(os.path.join(path, MYTRACKS_RES_DIR)):
|
||||
if path == '/':
|
||||
raise 'Not in My Tracks project'
|
||||
|
||||
# Go up one level
|
||||
path = os.path.split(path)[0]
|
||||
|
||||
return path
|
||||
|
||||
|
||||
def GetAllLanguageFiles():
|
||||
'''
|
||||
Returns a mapping from all found languages to their respective directories.
|
||||
'''
|
||||
mytracks_path = GetMyTracksDir()
|
||||
res_dir = os.path.join(mytracks_path, MYTRACKS_RES_DIR, ANDROID_VALUES_MASK)
|
||||
language_dirs = glob(res_dir)
|
||||
master_dir = os.path.join(mytracks_path, MYTRACKS_RES_DIR, ANDROID_MASTER_VALUES)
|
||||
if len(language_dirs) == 0:
|
||||
raise 'No languages found!'
|
||||
if not os.path.isdir(master_dir):
|
||||
raise 'Couldn\'t find master file'
|
||||
|
||||
language_tuples = [(re.findall(r'.*values-([A-Za-z-]+)', dir)[0],dir) for dir in language_dirs]
|
||||
language_tuples.append(('en', master_dir))
|
||||
return dict(language_tuples)
|
||||
@@ -0,0 +1,94 @@
|
||||
'''
|
||||
Module which brings history information about files from Mercurial.
|
||||
|
||||
@author: Rodrigo Damazio
|
||||
'''
|
||||
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
REVISION_REGEX = re.compile(r'(?P<hash>[0-9a-f]{12}):.*')
|
||||
|
||||
def _GetOutputLines(args):
|
||||
'''
|
||||
Runs an external process and returns its output as a list of lines.
|
||||
|
||||
@param args: the arguments to run
|
||||
'''
|
||||
process = subprocess.Popen(args,
|
||||
stdout=subprocess.PIPE,
|
||||
universal_newlines = True,
|
||||
shell = False)
|
||||
output = process.communicate()[0]
|
||||
return output.splitlines()
|
||||
|
||||
|
||||
def FillMercurialRevisions(filename, parsed_file):
|
||||
'''
|
||||
Fills the revs attribute of all strings in the given parsed file with
|
||||
a list of revisions that touched the lines corresponding to that string.
|
||||
|
||||
@param filename: the name of the file to get history for
|
||||
@param parsed_file: the parsed file to modify
|
||||
'''
|
||||
# Take output of hg annotate to get revision of each line
|
||||
output_lines = _GetOutputLines(['hg', 'annotate', '-c', filename])
|
||||
|
||||
# Create a map of line -> revision (key is list index, line 0 doesn't exist)
|
||||
line_revs = ['dummy']
|
||||
for line in output_lines:
|
||||
rev_match = REVISION_REGEX.match(line)
|
||||
if not rev_match:
|
||||
raise 'Unexpected line of output from hg: %s' % line
|
||||
rev_hash = rev_match.group('hash')
|
||||
line_revs.append(rev_hash)
|
||||
|
||||
for str in parsed_file.itervalues():
|
||||
# Get the lines that correspond to each string
|
||||
start_line = str['startLine']
|
||||
end_line = str['endLine']
|
||||
|
||||
# Get the revisions that touched those lines
|
||||
revs = []
|
||||
for line_number in range(start_line, end_line + 1):
|
||||
revs.append(line_revs[line_number])
|
||||
|
||||
# Merge with any revisions that were already there
|
||||
# (for explict revision specification)
|
||||
if 'revs' in str:
|
||||
revs += str['revs']
|
||||
|
||||
# Assign the revisions to the string
|
||||
str['revs'] = frozenset(revs)
|
||||
|
||||
def DoesRevisionSuperceed(filename, rev1, rev2):
|
||||
'''
|
||||
Tells whether a revision superceeds another.
|
||||
This essentially means that the older revision is an ancestor of the newer
|
||||
one.
|
||||
This also returns True if the two revisions are the same.
|
||||
|
||||
@param rev1: the revision that may be superceeding the other
|
||||
@param rev2: the revision that may be superceeded
|
||||
@return: True if rev1 superceeds rev2 or they're the same
|
||||
'''
|
||||
if rev1 == rev2:
|
||||
return True
|
||||
|
||||
# TODO: Add filename
|
||||
args = ['hg', 'log', '-r', 'ancestors(%s)' % rev1, '--template', '{node|short}\n', filename]
|
||||
output_lines = _GetOutputLines(args)
|
||||
|
||||
return rev2 in output_lines
|
||||
|
||||
def NewestRevision(filename, rev1, rev2):
|
||||
'''
|
||||
Returns which of two revisions is closest to the head of the repository.
|
||||
If none of them is the ancestor of the other, then we return either one.
|
||||
|
||||
@param rev1: the first revision
|
||||
@param rev2: the second revision
|
||||
'''
|
||||
if DoesRevisionSuperceed(filename, rev1, rev2):
|
||||
return rev1
|
||||
return rev2
|
||||
Executable
+96
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/python
|
||||
'''
|
||||
Entry point for My Tracks i18n tool.
|
||||
|
||||
@author: Rodrigo Damazio
|
||||
'''
|
||||
|
||||
import mytracks.files
|
||||
import mytracks.translate
|
||||
import mytracks.validate
|
||||
import sys
|
||||
|
||||
def Usage():
|
||||
print 'Usage: %s <command> [<language> ...]\n' % sys.argv[0]
|
||||
print 'Commands are:'
|
||||
print ' cleanup'
|
||||
print ' translate'
|
||||
print ' validate'
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def Translate(languages):
|
||||
'''
|
||||
Asks the user to interactively translate any missing or oudated strings from
|
||||
the files for the given languages.
|
||||
|
||||
@param languages: the languages to translate
|
||||
'''
|
||||
validator = mytracks.validate.Validator(languages)
|
||||
validator.Validate()
|
||||
missing = validator.missing_in_lang()
|
||||
outdated = validator.outdated_in_lang()
|
||||
|
||||
for lang in languages:
|
||||
untranslated = missing[lang] + outdated[lang]
|
||||
|
||||
if len(untranslated) == 0:
|
||||
continue
|
||||
|
||||
translator = mytracks.translate.Translator(lang)
|
||||
translator.Translate(untranslated)
|
||||
|
||||
|
||||
def Validate(languages):
|
||||
'''
|
||||
Computes and displays errors in the string files for the given languages.
|
||||
|
||||
@param languages: the languages to compute for
|
||||
'''
|
||||
validator = mytracks.validate.Validator(languages)
|
||||
validator.Validate()
|
||||
|
||||
error_count = 0
|
||||
if (validator.valid()):
|
||||
print 'All files OK'
|
||||
else:
|
||||
for lang, missing in validator.missing_in_master().iteritems():
|
||||
print 'Missing in master, present in %s: %s:' % (lang, str(missing))
|
||||
error_count = error_count + len(missing)
|
||||
for lang, missing in validator.missing_in_lang().iteritems():
|
||||
print 'Missing in %s, present in master: %s:' % (lang, str(missing))
|
||||
error_count = error_count + len(missing)
|
||||
for lang, outdated in validator.outdated_in_lang().iteritems():
|
||||
print 'Outdated in %s: %s:' % (lang, str(outdated))
|
||||
error_count = error_count + len(outdated)
|
||||
|
||||
return error_count
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
argv = sys.argv
|
||||
argc = len(argv)
|
||||
if argc < 2:
|
||||
Usage()
|
||||
|
||||
languages = mytracks.files.GetAllLanguageFiles()
|
||||
if argc == 3:
|
||||
langs = set(argv[2:])
|
||||
if not langs.issubset(languages):
|
||||
raise 'Language(s) not found'
|
||||
|
||||
# Filter just to the languages specified
|
||||
languages = dict((lang, lang_file)
|
||||
for lang, lang_file in languages.iteritems()
|
||||
if lang in langs or lang == 'en' )
|
||||
|
||||
cmd = argv[1]
|
||||
if cmd == 'translate':
|
||||
Translate(languages)
|
||||
elif cmd == 'validate':
|
||||
error_count = Validate(languages)
|
||||
else:
|
||||
Usage()
|
||||
error_count = 0
|
||||
|
||||
print '%d errors found.' % error_count
|
||||
@@ -0,0 +1,115 @@
|
||||
'''
|
||||
Module which parses a string XML file.
|
||||
|
||||
@author: Rodrigo Damazio
|
||||
'''
|
||||
|
||||
from xml.parsers.expat import ParserCreate
|
||||
import re
|
||||
#import xml.etree.ElementTree as ET
|
||||
|
||||
class StringsParser(object):
|
||||
'''
|
||||
Parser for string XML files.
|
||||
|
||||
This object is not thread-safe and should be used for parsing a single file at
|
||||
a time, only.
|
||||
'''
|
||||
|
||||
def Parse(self, file):
|
||||
'''
|
||||
Parses the given file and returns a dictionary mapping keys to an object
|
||||
with attributes for that key, such as the value, start/end line and explicit
|
||||
revisions.
|
||||
|
||||
In addition to the standard XML format of the strings file, this parser
|
||||
supports an annotation inside comments, in one of these formats:
|
||||
|
||||
<!-- KEEP_PARENT name="bla" -->
|
||||
<!-- KEEP_PARENT name="bla" rev="123456789012" -->
|
||||
|
||||
Such an annotation indicates that we're explicitly inheriting form the
|
||||
master file (and the optional revision says that this decision is compatible
|
||||
with the master file up to that revision).
|
||||
|
||||
@param file: the name of the file to parse
|
||||
'''
|
||||
self._Reset()
|
||||
|
||||
# Unfortunately expat is the only parser that will give us line numbers
|
||||
self._xml_parser = ParserCreate()
|
||||
self._xml_parser.StartElementHandler = self._StartElementHandler
|
||||
self._xml_parser.EndElementHandler = self._EndElementHandler
|
||||
self._xml_parser.CharacterDataHandler = self._CharacterDataHandler
|
||||
self._xml_parser.CommentHandler = self._CommentHandler
|
||||
|
||||
file_obj = open(file)
|
||||
self._xml_parser.ParseFile(file_obj)
|
||||
file_obj.close()
|
||||
|
||||
return self._all_strings
|
||||
|
||||
def _Reset(self):
|
||||
self._currentString = None
|
||||
self._currentStringName = None
|
||||
self._currentStringValue = None
|
||||
self._all_strings = {}
|
||||
|
||||
def _StartElementHandler(self, name, attrs):
|
||||
if name != 'string':
|
||||
return
|
||||
|
||||
if 'name' not in attrs:
|
||||
return
|
||||
|
||||
assert not self._currentString
|
||||
assert not self._currentStringName
|
||||
self._currentString = {
|
||||
'startLine' : self._xml_parser.CurrentLineNumber,
|
||||
}
|
||||
|
||||
if 'rev' in attrs:
|
||||
self._currentString['revs'] = [attrs['rev']]
|
||||
|
||||
self._currentStringName = attrs['name']
|
||||
self._currentStringValue = ''
|
||||
|
||||
def _EndElementHandler(self, name):
|
||||
if name != 'string':
|
||||
return
|
||||
|
||||
assert self._currentString
|
||||
assert self._currentStringName
|
||||
self._currentString['value'] = self._currentStringValue
|
||||
self._currentString['endLine'] = self._xml_parser.CurrentLineNumber
|
||||
self._all_strings[self._currentStringName] = self._currentString
|
||||
|
||||
self._currentString = None
|
||||
self._currentStringName = None
|
||||
self._currentStringValue = None
|
||||
|
||||
def _CharacterDataHandler(self, data):
|
||||
if not self._currentString:
|
||||
return
|
||||
|
||||
self._currentStringValue += data
|
||||
|
||||
_KEEP_PARENT_REGEX = re.compile(r'\s*KEEP_PARENT\s+'
|
||||
r'name\s*=\s*[\'"]?(?P<name>[a-z0-9_]+)[\'"]?'
|
||||
r'(?:\s+rev=[\'"]?(?P<rev>[0-9a-f]{12})[\'"]?)?\s*',
|
||||
re.MULTILINE | re.DOTALL)
|
||||
|
||||
def _CommentHandler(self, data):
|
||||
keep_parent_match = self._KEEP_PARENT_REGEX.match(data)
|
||||
if not keep_parent_match:
|
||||
return
|
||||
|
||||
name = keep_parent_match.group('name')
|
||||
self._all_strings[name] = {
|
||||
'keepParent' : True,
|
||||
'startLine' : self._xml_parser.CurrentLineNumber,
|
||||
'endLine' : self._xml_parser.CurrentLineNumber
|
||||
}
|
||||
rev = keep_parent_match.group('rev')
|
||||
if rev:
|
||||
self._all_strings[name]['revs'] = [rev]
|
||||
@@ -0,0 +1,21 @@
|
||||
'''
|
||||
Module which prompts the user for translations and saves them.
|
||||
|
||||
TODO: implement
|
||||
|
||||
@author: Rodrigo Damazio
|
||||
'''
|
||||
|
||||
class Translator(object):
|
||||
'''
|
||||
classdocs
|
||||
'''
|
||||
|
||||
def __init__(self, language):
|
||||
'''
|
||||
Constructor
|
||||
'''
|
||||
self._language = language
|
||||
|
||||
def Translate(self, string_names):
|
||||
print string_names
|
||||
@@ -0,0 +1,115 @@
|
||||
'''
|
||||
Module which compares languague files to the master file and detects
|
||||
issues.
|
||||
|
||||
@author: Rodrigo Damazio
|
||||
'''
|
||||
|
||||
import os
|
||||
from mytracks.parser import StringsParser
|
||||
import mytracks.history
|
||||
|
||||
class Validator(object):
|
||||
|
||||
def __init__(self, languages):
|
||||
'''
|
||||
Builds a strings file validator.
|
||||
|
||||
Params:
|
||||
@param languages: a dictionary mapping each language to its corresponding directory
|
||||
'''
|
||||
self._langs = {}
|
||||
self._master = None
|
||||
self._language_paths = languages
|
||||
|
||||
parser = StringsParser()
|
||||
for lang, lang_dir in languages.iteritems():
|
||||
filename = os.path.join(lang_dir, 'strings.xml')
|
||||
parsed_file = parser.Parse(filename)
|
||||
mytracks.history.FillMercurialRevisions(filename, parsed_file)
|
||||
|
||||
if lang == 'en':
|
||||
self._master = parsed_file
|
||||
else:
|
||||
self._langs[lang] = parsed_file
|
||||
|
||||
self._Reset()
|
||||
|
||||
def Validate(self):
|
||||
'''
|
||||
Computes whether all the data in the files for the given languages is valid.
|
||||
'''
|
||||
self._Reset()
|
||||
self._ValidateMissingKeys()
|
||||
self._ValidateOutdatedKeys()
|
||||
|
||||
def valid(self):
|
||||
return (len(self._missing_in_master) == 0 and
|
||||
len(self._missing_in_lang) == 0 and
|
||||
len(self._outdated_in_lang) == 0)
|
||||
|
||||
def missing_in_master(self):
|
||||
return self._missing_in_master
|
||||
|
||||
def missing_in_lang(self):
|
||||
return self._missing_in_lang
|
||||
|
||||
def outdated_in_lang(self):
|
||||
return self._outdated_in_lang
|
||||
|
||||
def _Reset(self):
|
||||
# These are maps from language to string name list
|
||||
self._missing_in_master = {}
|
||||
self._missing_in_lang = {}
|
||||
self._outdated_in_lang = {}
|
||||
|
||||
def _ValidateMissingKeys(self):
|
||||
'''
|
||||
Computes whether there are missing keys on either side.
|
||||
'''
|
||||
master_keys = frozenset(self._master.iterkeys())
|
||||
for lang, file in self._langs.iteritems():
|
||||
keys = frozenset(file.iterkeys())
|
||||
missing_in_master = keys - master_keys
|
||||
missing_in_lang = master_keys - keys
|
||||
|
||||
if len(missing_in_master) > 0:
|
||||
self._missing_in_master[lang] = missing_in_master
|
||||
if len(missing_in_lang) > 0:
|
||||
self._missing_in_lang[lang] = missing_in_lang
|
||||
|
||||
def _ValidateOutdatedKeys(self):
|
||||
'''
|
||||
Computers whether any of the language keys are outdated with relation to the
|
||||
master keys.
|
||||
'''
|
||||
for lang, file in self._langs.iteritems():
|
||||
outdated = []
|
||||
for key, str in file.iteritems():
|
||||
# Get all revisions that touched master and language files for this
|
||||
# string.
|
||||
master_str = self._master[key]
|
||||
master_revs = master_str['revs']
|
||||
lang_revs = str['revs']
|
||||
if not master_revs or not lang_revs:
|
||||
print 'WARNING: No revision for %s in %s' % (key, lang)
|
||||
continue
|
||||
|
||||
master_file = os.path.join(self._language_paths['en'], 'strings.xml')
|
||||
lang_file = os.path.join(self._language_paths[lang], 'strings.xml')
|
||||
|
||||
# Assume that the repository has a single head (TODO: check that),
|
||||
# and as such there is always one revision which superceeds all others.
|
||||
master_rev = reduce(
|
||||
lambda r1, r2: mytracks.history.NewestRevision(master_file, r1, r2),
|
||||
master_revs)
|
||||
lang_rev = reduce(
|
||||
lambda r1, r2: mytracks.history.NewestRevision(lang_file, r1, r2),
|
||||
lang_revs)
|
||||
|
||||
# If the master version is newer than the lang version
|
||||
if mytracks.history.DoesRevisionSuperceed(lang_file, master_rev, lang_rev):
|
||||
outdated.append(key)
|
||||
|
||||
if len(outdated) > 0:
|
||||
self._outdated_in_lang[lang] = outdated
|
||||
Reference in New Issue
Block a user