Fullmenu null

 

03 March 2018

La mayoría de las aplicaciones Java utilizan la funcionalidad que ofrece para el manejo de mensajes internacionalizados el cual se basa en una serie de ficheros properties que comparten un nombre común más un sufijo que indica el idioma al que corresponden las traducciones incluidas en el mismo.

Por ejemplo, supongamos que nuestra aplicación va a mostrar por defecto los mensajes en inglés pero necesitamos poder mostrarlos también en español y francés. Este caso en Java se corresponde con estos ficheros:

theapp.properties
login=Login
welcome=Welcome
theapp_es.properties
login=Identificacion
welcome=Bienvenido
theapp_fr.properties
login=Identifier
welcome=Bienvenue

Cuando la aplicación crece, el número de mensajes a mostrar suele hacerlo también y nos vamos centrando en el fichero por defecto hasta tener la mayor cantidad posible de identificadores. Si en este momento queremos realizar la traducción (o encargarsela a alguien) nos encontramos con una serie de ficheros incompletos e inconexos y aunque existen herramientas para facilitar la edición, estas no suelen ser del agrado de quien tiene que traducirlos.

Mediante este script vamos a poder realizar dos procesos diferentes aunque relacionados:

  • Partiendo de un conjunto de ficheros properties de traducciones incompletas crearemos un fichero Excel donde cada columna corresponderá a un idioma y cada fila a un elemento a traducir en cada idioma

  • Partiendo de un excel con las traducciones completas crearemos un conjunto de ficheros properties cada uno con las traducciones que le correspondan.

Note
Vamos a usar Apache POI para ambas situaciones pero para la escritura vamos a usar el DSL Groovy Excel Builder de James Kleeh para demostrar lo fácil que es escribir un excel con Groovy.

Dependencias

De forma general vamos a usar las librerías de Apache POI para leer y escribir el Excel, pero como el DSL que vamos a usar las incluye en sus dependencias podemos indicar simplemente este en Grappe:

@Grab('com.jameskleeh:excel-builder:0.4.2')

import org.apache.poi.ss.usermodel.*
import org.apache.poi.hssf.usermodel.*
import org.apache.poi.xssf.usermodel.*
import org.apache.poi.ss.util.*
import org.apache.poi.ss.usermodel.*
import com.jameskleeh.excel.ExcelBuilder

Argumentos

Como hemos dicho el script podrá ejecutar dos acciones diferentes por lo que preparamos un CliBuilder que nos permita interpretar la línea de comandos y los argumentos proporcionados por el usuario:

def cli = new CliBuilder(usage: 'groovy ExcelI18n.groovy -[h] -action generateProperties/generateExcel filename.xls ')

cli.with { // (1)
    h(longOpt: 'help', 'Import/Export excel file to i18n properties files.', required: false)
    action('generateProperties generateExcel', required: true, args:1, argName:'action')
}

def options = cli.parse(args)
if (!options){
    return
}
if(options.h || options.arguments().size()==0) {
    cli.usage()
    return
}

if( options.action == 'generateProperties'){    //(2)
    return generateProperties(options.arguments()[0])
}

if( options.action == 'generateExcel'){ //(3)
    return generateExcel(options.arguments()[0])
}

cli.usage() //(4)
  1. preparamos las opciones disponibles

  2. de Excel a properties

  3. de properties a Excel

  4. si no hay acción correcta mostramos el uso

De i18n a Excel

Partiendo de una situación en la que tenemos un conjunto de traducciones incompletas lo que pretendemos hacer es cargar todos estos ficheros en un Excel organizando por filas y columnas los códigos y los idiomas respectivamente.

Para determinar los idiomas que queremos manejar en nuestra aplicación hay que fijarse en que todos ellos siguen el patrón:

filename (_ i18code)? .properties , es decir un nombre de fichero común, un guión bajo y un código de idioma (por ejemplo es, fr, ca, etc) opcionales y una extension fija .properties

void generateExcel(filename){
    File excelFile = new File(filename)

    String dir = excelFile.absolutePath.split(File.separator).dropRight(1).join(File.separator)
    String name = excelFile.name.split('\\.').dropRight(1).join('.')

    Properties defaultProperties = new Properties()
    defaultProperties.load( new File("${name}.properties").newInputStream() ) //(1)

    Map<String,Properties> propertiesMap = [:]  //(2)

    new File(dir).eachFileMatch ~"${name}(_[A-Za-z]+)\\.properties", {  //(3)
        def matcher = (it.name =~ "${name}(_[A-Za-z]+)\\.properties" )
        String lang = matcher[0][1]?.substring(1)
        Properties prp = new Properties()
        prp.load( it.newInputStream() )
        propertiesMap[ lang ] = prp //(4)
    }

    ExcelBuilder.output(new FileOutputStream(new File("${filename}"))) {
        sheet {
            row{
                cell("Code")
                cell("Default")
                propertiesMap.keySet().each { lang ->
                    cell(lang.toUpperCase())
                }
            }
            defaultProperties.propertyNames().each{ String property->   //(5)
                row{
                    cell(property)
                    cell(defaultProperties[property])
                    propertiesMap.keySet().each { lang ->
                        cell(propertiesMap[lang][property])
                    }
                }
            }
        }
    }
}
  1. cargar en un Properties el fichero por defecto (el cual contienen todos las keys a traducir)

  2. preparar un Map<String,Properties>

  3. buscar los ficheros de traducciones particulares que cumplen el patrón explicado

  4. cargar en el mapa el properties identificado por su código de idioma

  5. crear un Excel donde cada key será una fila y cada idioma volcará su texto

De Excel a i18n

Una vez completado el Excel con las traducciones adecuadas necesitarremos volver a reescribir los ficheros properties

void generateProperties(filename){
    File excelFile = new File(filename)

    String dir = excelFile.absolutePath.split(File.separator).dropRight(1).join(File.separator)
    String name = excelFile.name.split('\\.').dropRight(1).join('.')
    InputStream inp = new FileInputStream(excelFile)

    //(1)
    new File(dir).eachFileMatch ~"${name}(_[A-Za-z]+)?\\.properties", {
        it.delete()
    }

    List<String> languages = []

    Workbook wb = WorkbookFactory.create(inp)
    Sheet sheet = wb.getSheetAt(0)

    sheet.iterator().eachWithIndex{ Row row, int idx-> //(2)

        if( idx == 0){  //(3)
            languages = ['']+row.cellIterator().collect{ '_'+it.stringCellValue }.drop(2)*.toLowerCase()
            return
        }

        String code = row.getCell(0)
        languages.eachWithIndex{ String lang, int i ->  //(4)
            String txt = row.getCell(i+1)?.stringCellValue
            if(txt)
                new File("${name}${lang}.properties") << "$code=$txt\n" //(5)
        }
    }
}
  1. limpiamos ficheros de traduccion si los hubiera

  2. iteramos por las filas del Excel

  3. la primera fila nos indica los lenguajes que se contemplan además del por defecto

  4. para cada lenguaje indicado en la primera fila buscamos si hay traducción en el excel

  5. si tenemos traducción la concatenamos al fichero correspondiente al idioma


Script
//tag::dependencies[]
@Grab('com.jameskleeh:excel-builder:0.4.2')

import org.apache.poi.ss.usermodel.*
import org.apache.poi.hssf.usermodel.*
import org.apache.poi.xssf.usermodel.*
import org.apache.poi.ss.util.*
import org.apache.poi.ss.usermodel.*
import com.jameskleeh.excel.ExcelBuilder
//end::dependencies[]

//tag::arguments[]
def cli = new CliBuilder(usage: 'groovy ExcelI18n.groovy -[h] -action generateProperties/generateExcel filename.xls ')

cli.with { // (1)
    h(longOpt: 'help', 'Import/Export excel file to i18n properties files.', required: false)
    action('generateProperties generateExcel', required: true, args:1, argName:'action')
}

def options = cli.parse(args)
if (!options){
    return
}
if(options.h || options.arguments().size()==0) {
    cli.usage()
    return
}

if( options.action == 'generateProperties'){    //(2)
    return generateProperties(options.arguments()[0])
}

if( options.action == 'generateExcel'){ //(3)
    return generateExcel(options.arguments()[0])
}

cli.usage() //(4)
//end::arguments[]

//tag::generateProperties[]
void generateProperties(filename){
    File excelFile = new File(filename)

    String dir = excelFile.absolutePath.split(File.separator).dropRight(1).join(File.separator)
    String name = excelFile.name.split('\\.').dropRight(1).join('.')
    InputStream inp = new FileInputStream(excelFile)

    //(1)
    new File(dir).eachFileMatch ~"${name}(_[A-Za-z]+)?\\.properties", {
        it.delete()
    }

    List<String> languages = []

    Workbook wb = WorkbookFactory.create(inp)
    Sheet sheet = wb.getSheetAt(0)

    sheet.iterator().eachWithIndex{ Row row, int idx-> //(2)

        if( idx == 0){  //(3)
            languages = ['']+row.cellIterator().collect{ '_'+it.stringCellValue }.drop(2)*.toLowerCase()
            return
        }

        String code = row.getCell(0)
        languages.eachWithIndex{ String lang, int i ->  //(4)
            String txt = row.getCell(i+1)?.stringCellValue
            if(txt)
                new File("${name}${lang}.properties") << "$code=$txt\n" //(5)
        }
    }
}
//end::generateProperties[]

//tag::generateExcel[]
void generateExcel(filename){
    File excelFile = new File(filename)

    String dir = excelFile.absolutePath.split(File.separator).dropRight(1).join(File.separator)
    String name = excelFile.name.split('\\.').dropRight(1).join('.')

    Properties defaultProperties = new Properties()
    defaultProperties.load( new File("${name}.properties").newInputStream() ) //(1)

    Map<String,Properties> propertiesMap = [:]  //(2)

    new File(dir).eachFileMatch ~"${name}(_[A-Za-z]+)\\.properties", {  //(3)
        def matcher = (it.name =~ "${name}(_[A-Za-z]+)\\.properties" )
        String lang = matcher[0][1]?.substring(1)
        Properties prp = new Properties()
        prp.load( it.newInputStream() )
        propertiesMap[ lang ] = prp //(4)
    }

    ExcelBuilder.output(new FileOutputStream(new File("${filename}"))) {
        sheet {
            row{
                cell("Code")
                cell("Default")
                propertiesMap.keySet().each { lang ->
                    cell(lang.toUpperCase())
                }
            }
            defaultProperties.propertyNames().each{ String property->   //(5)
                row{
                    cell(property)
                    cell(defaultProperties[property])
                    propertiesMap.keySet().each { lang ->
                        cell(propertiesMap[lang][property])
                    }
                }
            }
        }
    }
}
//end::generateExcel[]