pl.droidsonroids.gradle.localization.ParserEngine.groovy Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of android-gradle-localization-plugin Show documentation
Show all versions of android-gradle-localization-plugin Show documentation
Gradle plugin for generating localized string resources
package pl.droidsonroids.gradle.localization
import groovy.xml.MarkupBuilderHelper
import java.text.Normalizer
import java.util.regex.Pattern
import static pl.droidsonroids.gradle.localization.ResourceType.ARRAY
import static pl.droidsonroids.gradle.localization.ResourceType.PLURAL
import static pl.droidsonroids.gradle.localization.TagEscapingStrategy.ALWAYS
import static pl.droidsonroids.gradle.localization.TagEscapingStrategy.IF_TAGS_ABSENT
class ParserEngine {
private static
final Pattern JAVA_IDENTIFIER_REGEX = Pattern.compile("\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*")
private final Parser mParser
private final ConfigExtension mConfig
private final File mResDir
private final Closeable mCloseableInput
ParserEngine(ConfigExtension config, File resDir) {
def csvSources = [config.csvFileURI, config.csvFile, config.csvGenerationCommand,
config.xlsFile, config.xlsFileURI] as Set
csvSources.remove(null)
if (csvSources.size() != 1) {
throw new IllegalArgumentException("Exactly one source must be defined")
}
mResDir = resDir
mConfig = config
final SourceType sourceType
if (config.csvGenerationCommand != null) {
def shellCommand = config.csvGenerationCommand.split('\\s+')
def redirect = ProcessBuilder.Redirect.INHERIT
def process = new ProcessBuilder(shellCommand).redirectError(redirect).start()
mCloseableInput = Utils.wrapReader(new InputStreamReader(process.inputStream))
sourceType = SourceType.CSV
} else if (config.csvFile != null) {
mCloseableInput = Utils.wrapReader(new FileReader(config.csvFile))
sourceType = SourceType.CSV
} else if (config.csvFileURI != null) {
mCloseableInput = Utils.wrapReader(new InputStreamReader(new URL(config.csvFileURI).openStream()))
sourceType = SourceType.CSV
} else if (config.xlsFileURI != null) {
final url = new URL(config.xlsFileURI)
mCloseableInput = Utils.wrapInputStream(url.openStream())
sourceType = url.path.endsWith("xls") ? SourceType.XLS : SourceType.XLSX
} else if (config.xlsFile != null) {
mCloseableInput = Utils.wrapInputStream(new FileInputStream(config.xlsFile))
sourceType = config.xlsFile.absolutePath.endsWith("xls") ? SourceType.XLS : SourceType.XLSX
} else {
throw new IllegalStateException()
}
if (sourceType == SourceType.CSV) {
def reader = (Reader) mCloseableInput
mParser = config.csvStrategy ? new CSVInnerParser(reader, config.csvStrategy) : new CSVInnerParser(reader)
} else {
def isXLS = sourceType == SourceType.XLS
mParser = new XLSXParser((InputStream) mCloseableInput, isXLS, config.sheetName, config.useAllSheets)
}
}
void parseSpreadsheet() {
mCloseableInput.withCloseable {
Map sheets = mParser.getResult()
for (String sheetName: sheets.keySet()) {
String[][] allCells = sheets.get(sheetName)
String outputFileName
if (sheetName != null) {
outputFileName = sheetName + ".xml"
} else {
outputFileName = mConfig.outputFileName
}
def header = new SourceInfo(allCells[0], mConfig, mResDir, outputFileName)
parseCells(header, allCells)
}
}
}
private parseCells(final SourceInfo sourceInfo, String[][] cells) {
HashMap translatableArrays = new HashMap()
for (j in 1..sourceInfo.mBuilders.length - 1) {
def builder = sourceInfo.mBuilders[j]
if (builder == null) {
continue
}
def keys = new HashSet(cells.length)
builder.addResource({
if (cells.length <= 1)
return
def stringAttrs = new LinkedHashMap<>(2)
def pluralsMap = new HashMap>()
def arrays = new HashMap>()
for (i in 1..cells.length - 1) {
String[] row = cells[i]
if (row == null) {
continue
}
if (row.length < sourceInfo.mColumnsCount) {
String[] extendedRow = new String[sourceInfo.mColumnsCount]
System.arraycopy(row, 0, extendedRow, 0, row.length)
for (int k in row.length..sourceInfo.mColumnsCount - 1)
extendedRow[k] = ''
row = extendedRow
}
String name = row[sourceInfo.mNameIdx]
def value = row[j]
String comment = null
if (sourceInfo.mCommentIndex >= 0 && !row[sourceInfo.mCommentIndex].empty) {
comment = row[sourceInfo.mCommentIndex]
}
def indexOfOpeningBrace = name.indexOf('[')
def indexOfClosingBrace = name.indexOf(']')
String indexValue
ResourceType resourceType
if (indexOfOpeningBrace > 0 && indexOfClosingBrace == name.length() - 1) {
indexValue = name.substring(indexOfOpeningBrace + 1, indexOfClosingBrace)
resourceType = indexValue.empty ? ARRAY : PLURAL
name = name.substring(0, indexOfOpeningBrace)
} else {
resourceType = ResourceType.STRING
indexValue = null
}
def translatable = true
stringAttrs['name'] = name
if (sourceInfo.mTranslatableIndex >= 0) {
translatable = !row[sourceInfo.mTranslatableIndex].equalsIgnoreCase('false')
if (resourceType == ARRAY) {
translatable &= translatableArrays.get(name, true)
translatableArrays[name] = translatable
} else
stringAttrs['translatable'] = translatable ? null : 'false'
}
if (sourceInfo.mFormattedIndex >= 0) {
def formatted = !row[sourceInfo.mFormattedIndex].equalsIgnoreCase('false')
stringAttrs['formatted'] = formatted ? null : 'false'
}
if (value.empty) {
if (!translatable && builder.mQualifier != mConfig.defaultColumnName)
continue
if (mConfig.handleEmptyTranslationsAsDefault && builder.mQualifier != mConfig.defaultColumnName)
continue
if (!mConfig.allowEmptyTranslations && resourceType != PLURAL)
throw new IllegalArgumentException("$name is not translated to locale $builder.mQualifier, row #${i + 1}")
} else {
if (!translatable && !mConfig.allowNonTranslatableTranslation && builder.mQualifier != mConfig.defaultColumnName)
throw new IllegalArgumentException("$name is translated but marked translatable='false', row #${i + 1}")
}
TagEscapingStrategy strategy = mConfig.tagEscapingStrategy
if (sourceInfo.mTagEscapingStrategyIndex >= 0) {
def strategyName = row[sourceInfo.mTagEscapingStrategyIndex]
if (strategyName) {
strategy = TagEscapingStrategy.valueOf(strategyName)
}
}
if (!JAVA_IDENTIFIER_REGEX.matcher(name).matches()) {
if (mConfig.skipInvalidName)
continue
throw new IllegalArgumentException("$name is not valid name, row #${i + 1}")
}
if (mConfig.escapeSlashes)
value = value.replace("\\", "\\\\")
if (mConfig.escapeApostrophes)
value = value.replace("'", "\\'")
if (mConfig.escapeQuotes && (strategy == ALWAYS || (strategy == IF_TAGS_ABSENT && !Utils.containsHTML(value))))
value = value.replace("\"", "\\\"")
if (mConfig.escapeNewLines)
value = value.replace("\n", "\\n")
if (value.startsWith(' ') || value.endsWith(' '))
value = '"' + value + '"'
if (mConfig.convertTripleDotsToHorizontalEllipsis)
value = value.replace("...", "…")
value = value.replace("?", "\\?")
if (mConfig.normalizationForm)
value = Normalizer.normalize(value, mConfig.normalizationForm)
if (resourceType == PLURAL || resourceType == ARRAY) {
//TODO require only one translatable value for all list?
if (resourceType == ARRAY) {
def stringList = arrays.get(name, [])
stringList += new StringArrayItem(value, comment)
arrays[name] = stringList
} else {
Quantity pluralQuantity = Quantity.valueOf(indexValue)
if (!Quantity.values().contains(pluralQuantity))
throw new IllegalArgumentException("${pluralQuantity.name()} is not valid quantity, row #${i + 1}")
Set quantitiesSet = pluralsMap.get(name, [] as TreeSet)
if (!value.empty) {
if (!quantitiesSet.add(new PluralItem(pluralQuantity, value, comment)))
throw new IllegalArgumentException("$name is duplicated in row #${i + 1}")
}
}
continue
}
if (!keys.add(name)) {
//TODO support case when first occurrence is marked non-translatable
if (mConfig.skipDuplicatedName)
continue
throw new IllegalArgumentException("$name is duplicated in row #${i + 1}")
}
string(stringAttrs) {
yieldValue(mkp, value, strategy)
}
if (comment) {
mkp.comment(comment)
}
}
pluralsMap.each { key, value ->
plurals([name: key]) {
if (value.empty)
throw new IllegalArgumentException("At least one quantity string must be defined for key: $key, qualifier $builder.mQualifier")
value.each { quantityEntry ->
item(quantity: quantityEntry.quantity) {
yieldValue(mkp, quantityEntry.value)
}
if (quantityEntry.comment)
mkp.comment(quantityEntry.comment)
}
}
}
arrays.each { key, value ->
boolean translatable = translatableArrays.getOrDefault(key, true)
'string-array'([name: key, translatable: translatable ? null : 'false']) {
value.each { stringArrayItem ->
item {
yieldValue(mkp, stringArrayItem.value)
}
if (stringArrayItem.comment)
mkp.comment(stringArrayItem.comment)
}
}
}
})
}
}
private void yieldValue(MarkupBuilderHelper mkp, String value, TagEscapingStrategy strategy = mConfig.tagEscapingStrategy) {
if (strategy == ALWAYS || (strategy == IF_TAGS_ABSENT && !Utils.containsHTML(value)))
mkp.yield(value)
else
mkp.yieldUnescaped(value)
}
}