Explicación de como está implementado el bundle translations-apibundle que conecta un proyecto en Symfony2 con el servidor de traducciones centralizado tradukoj.com
3. www.tradukoj.com
Las traducciones en SF2 son gestionadas mediante unos
archivos en la carpeta Resources/translations de
cada uno de los distintos bundles, el nombre del archivo
tiene esta estructura:
!
{catalog}.{language}.{format}!
!
Los formatos disponibles actualmente son:!
!
!
!
Normalmente catalog toma alguno de estos valores:!
!
!
!
aunque puede ser otro cualquiera.
yml xml php
messages validators security
Traducciones en Symfony2
4. www.tradukoj.com
Los diferentes archivos al final lo que hacen
es relacionar una clave con un texto:!
!
!
!
!
!
!
Primer inconveniente:
hay que mantener un sistema en el que se
repiten las claves en varios archivos.
yml!
!
header:!
menu:!
label:
"Menu"
xml!
!
<trans-‐unit
id="1">!
<source>!
header.menu.label!
</source>!
<target>Menu</target>!
</trans-‐unit>
php!
!
//
messages.es.php
return
array(!
'header.menu.label'
=>
'Menu',!
);
Traducciones en Symfony2
5. www.tradukoj.com
Las claves se separan en catálogos!
(o espacios de nombres) por claridad.!
!
En realidad SF2 no trabaja directamente!
con esos archivos.!
!
Lo hace con la versión "compilada" que crea en app/cache/
{env}/translations/catalogue.{language}.php
!
Esta versión se "compila" en la!
regeneración de la cache.!
!
!
Veamos cómo es uno de esos archivos...
Traducciones en Symfony2
7. www.tradukoj.com
Traducciones en Symfony2
Ahora ya podemos usar esas claves en nuestro
código.!
!
En un twig:!
{{
"header.menu.label"|trans
}}!
!
En un controlador:!
$this-‐>container!
-‐>get("translator")!
-‐>trans("header.menu.label");
9. www.tradukoj.com
La manipulación de los archivos
fuentes de las traducciones!
(xml, yml o php) requiere ciertos
conocimientos técnicos que no
siempre el traductor, revisor o
colaborador posee.
El proyecto: motivación
10. www.tradukoj.com
Por tanto hay que ir con mil ojos
cuando se les pasa algún archivo
de éstos, porque a la vuelta es
fácil que SF2 se queje porque
haya tabulaciones, no coincidan
las claves, etc, etc, etc..
El proyecto: motivación
11. www.tradukoj.com
Incluso la edición por parte de técnicos
puede producir los mismos conflictos
que el resto del código fuente.!
!
Es fácil ponerse de acuerdo para
editar, pero hay que acordarse de
hacerlo. Os aseguro que revisar un yml
con conflictos es muy divertido!
;<)
El proyecto: motivación
13. www.tradukoj.com
Propongo …
Un servidor centralizado para gestionar
las traducciones de los desarrollos en
Symfony2.!
!
Un sistema con ciertas ventajas: !
edición colaborativa, !
permisos y roles, !
sin necesidad de ningún conocimiento
técnico para mantenerlo.
15. www.tradukoj.com
El proyecto
Empezó siendo translations.com.es!
Al intuir su posible difusión decidí cambiar a
un punto .com!
!
Al no quedar libre ninguno en los
principales idiomas me decidí por un idioma
menos conocido
(no es éste un proyecto de idiomas ;) ),!
!
En esperanto tradukoj
(leido TRADUCOI) significa traducciones.
18. www.tradukoj.com
A partir de la instalación
del bundle, los archivos de
traducciones fuentes
tienen que ser ignorados.
Veamos cómo…
19. www.tradukoj.com
El bundle instala dentro
de su código una clase que
intercepta las recreaciones
de los archivos de
traducciones en cache.
20. www.tradukoj.com
Jlaso/Translations/ApiBundle/Translations/Loader/PdoLoader.php
class
PdoLoader
implements
LoaderInterface,
ResourceInterface
{
//
Esta
es
la
que
se
invoca
para
regenerar
las
traducciones
public
function
load($resource,
$locale,
$domain
=
Translation::DEFAULT_DOMAIN)
!
//
Esta
genera
la
sentencia
que
recupera
las
keys
de
la
tabla
local
protected
function
getTranslationsStatement()
!
//
public
function
getTranslations($locale,
$criteria,
$hierarchicalArray
=
true)
!
//
public
function
registerResources(Translator
$translator)
!
//
protected
function
getResourcesStatement()
!
//
public
function
isFresh($timestamp)
!
//
protected
function
getFreshnessStatement($timestamp)
public
function
getResource()
public
function
getConnection()
}
21. www.tradukoj.com
Jlaso/Translations/ApiBundle/Translations/Loader/PdoLoader.php
class
PdoLoader
implements
LoaderInterface,
ResourceInterface
{
//..
!
public
function
load($resource,
$locale,
$domain
=
Translation::DEFAULT_DOMAIN)
{
if
($resource
!==
$this)
{
return
new
MessageCatalogue($locale);
}
$stmt
=
$this-‐>getTranslationsStatement();
$stmt-‐>bindValue(':locale',
$locale,
PDO::PARAM_STR);
$stmt-‐>bindValue(':domain',
$domain,
PDO::PARAM_STR);
!
$catalogue
=
new
MessageCatalogue($locale);
while
($row
=
$stmt-‐>fetch())
{
$catalogue-‐>set($row['key'],
$row['message'],
$domain);
}
!
return
$catalogue;
}
!
//..
} Se han condensado y/o
eliminado algunas partes
por claridad
23. www.tradukoj.com
/**
*
@ORMTable(name="jlaso_translations")
*
@UniqueEntity(fields="domain,locale,key")
*/
class
Translation
{
private
$id;
private
$domain;
private
$locale;
private
$key;
private
$message;
protected
$bundle;
protected
$file;
private
$createdAt;
private
$updatedAt;
//
getters
and
setters
..
}
CREATE
TABLE
`jlaso_translations`
(!
`id`
int(11)
NOT
NULL
AUTO_INCREMENT,!
`domain`
varchar(50)
COLLATE
utf8_unicode_ci
NOT
NULL,!
`locale`
varchar(10)
COLLATE
utf8_unicode_ci
NOT
NULL,!
`key`
varchar(255)
COLLATE
utf8_unicode_ci
NOT
NULL,!
`message`
longtext
COLLATE
utf8_unicode_ci,!
`bundle`
varchar(100)
COLLATE
utf8_unicode_ci
NOT
NULL,!
`file`
varchar(255)
COLLATE
utf8_unicode_ci
NOT
NULL,!
`created_at`
datetime
NOT
NULL,!
`updated_at`
datetime
NOT
NULL,!
PRIMARY
KEY
(`id`)!
)
ENGINE=InnoDB
AUTO_INCREMENT=1
DEFAULT
CHARSET=utf8
COLLATE=utf8_unicode_ci;
24. www.tradukoj.com
Y toda esta introducción es
para hablaros de ese conector,
y de cómo he optimizado la
ejecución del comando más
pesado:
la sincronización.
26. www.tradukoj.com
jlaso/translations-apibundle
• Veamos como ha ido evolucionando
la conexión.
!
• La primera versión en 45 minutos no
había terminado la sincronización.
!
• Actualmente en 30 segundos se
produce todo el proceso.
!
• Los datos de prueba son siempre los
mismos.
28. www.tradukoj.com
Empezando
• Un controlador para
cada acción.
!
• Una petición por key.
Ventajas:
Arquitectura REST conocida.
!
Inconvenientes:
A mayor número de claves, más
peticiones. Cada una de ellas tiene
que negociar de nuevo con el
servidor.
!
Resultado:
Deplorable, 45 minutos
29. www.tradukoj.com
Evolución de la conexión
• Ruta
convencional.
• ~ Api-REST.
• Una petición
por cada key.
• Una petición
por cada
catálogo e
idioma.
30. www.tradukoj.com
Mejorando
• Se concentran todos los
datos de un catálogo en
una petición.
Ventajas:
Mejora el rendimiento.
!
Inconvenientes:
problemas con el tamaño de los
datos enviados, en ocasiones se
pierden datos.
!
Resultado:
mejorable, 25 minutos
31. www.tradukoj.com
Evolución de la conexión
• Ruta
convencional.
• ~ Api-REST.
• Una petición
por cada key.
• Una petición
por cada
catálogo e
idioma.
• Socket
• Una petición
por cada
catálogo e
idioma.
32. www.tradukoj.com
La evolución
• Petición de socket libre.
!
• Se evoluciona el modelo
API-REST anterior tal cual.
Ventajas:
Mejora el rendimiento de manera
brutal.
!
Inconvenientes:
sigue habiendo problemas con la
pérdida de datos.
!
Resultado:
muy bueno, menos de 5 minutos.
33. www.tradukoj.com
Evolución de la conexión
• Ruta
convencional.
• ~ Api-REST.
• Una petición
por cada key.
• Una petición
por cada
catálogo e
idioma.
• Socket
• Una petición
por cada
catálogo e
idioma.
• Fraccionando
en bloques y
comprimido.
34. www.tradukoj.com
La revolución
• Comunicación mediante
bloques de tamaño fijo y
comprimiendo los datos
enviados por el canal.
!
• Reconocimiento de
cada paquete recibido.
Inconvenientes:
No se controla la pérdida de
paquetes aunque van numerados.
!
Resultado:
perfecto, medio minuto.
43. www.tradukoj.com
En el servidor este controlador atiende la ruta !
de petición de creación de un socket
@Route("/create-‐socket/{projectId}")
public
function
createSocketAction(…)
{
$host
=
php_uname('n');
$found
=
false;
for
($port
=
self::MIN_PORT;
$port
<
self::MAX_PORT;
$port++)
{
$connection
=
@fsockopen($host,
$port,
$errno,
$errtxt,
500);
if
(is_resource($connection)){
fclose($connection);
}else{
$found
=
true;
break;
}
}
if($found){
$srcDir
=
dirname($this-‐>get('kernel')-‐>getRootDir());
$cmd
=
"php
$srcDir
/app/console
".
self::COMMAND
.
"
$host
$port
>/dev/null
2>/dev/null
&";
exec($cmd);
}
!
return
$this-‐>resultOk(array('port'
=>
$port));
}
Se han condensado y/o
eliminado algunas partes
por claridad
44. www.tradukoj.com
Ahora la comunicación es por el socket
creado y no por http
MiProyectoEnSF2
TAB
TAB: TranslationsApiBundle
/create-socket
{“port”:”10000”}
send => read
45. www.tradukoj.com
Formato de los mensajes
block-len : block-num : num-blocks : info
• block-len: indica la longitud del último campo
(info)
• block-num: es el número de bloque que se está
enviando/recibiendo
• num-blocks: la cantidad total de bloques que se
quieren enviar y se van a recibir
• info: el bloque que se está enviando/recibiendo en
formato comprimido (lzf)
!
Ejemplo de mensaje:
000010:001:001:0123456789
46. www.tradukoj.com
Campo info en los mensajes
Una vez se ha recompuesto todo el campo info a base de
juntar todos los bloques, se descomprime (lzf_decompress) e
inmediatamente se decodifica con json_decode
!
Veamos una petición del índice de catálogos de un
proyecto:
!
{
"auth.key":"key1234",
"auth.secret":"secret1234",
"command":"catalog-‐index",
"project_id":1
}
!
Está claro que este campo no necesita ni comprimirse ni
enviarse en bloques, pero cuando empiezas a trabajar con
las keys y sus traducciones, os aseguro que la cosa se
complica por momentos, en términos de longitud.
48. www.tradukoj.com
En el bundle TAB enviamos los mensajes
protected
function
sendMessage($msg,
$compress
=
true)
{
$msg
=
lzf_compress($msg);
$len
=
strlen($msg);
$blocks
=
ceil($len
/
self::BLOCK_SIZE);
for($i=0;
$i<$blocks;
$i++){
//
get
Block
to
send
$block
=
substr($msg,
$i
*
self::BLOCK_SIZE,
($i
==
$blocks-‐1)
?
$len
-‐
($i-‐1)
*
self::BLOCK_SIZE
:
self::BLOCK_SIZE);
$prefix
=
sprintf("%06d:%03d:%03d:",
strlen($block),
$i+1,
$blocks);
$aux
=
$prefix
.
$block;
if(false
===
socket_write($this-‐>socket,
$aux,
strlen($aux))){
die('error');
};
//
Wait
for
ACK
do{
$read
=
socket_read($this-‐>socket,
10,
PHP_NORMAL_READ);
}while(strpos($read,
self::ACK)
!==
0);
}
!
return
true;
} Se han condensado y/o
eliminado algunas partes
por claridad
49. www.tradukoj.com
En el bundle TAB
protected
function
readSocket()
{
$buffer
=
'';
$overload
=
strlen('000000:000:000:');
do{
$buf
=
socket_read($this-‐>socket,
$overload
+
self::BLOCK_SIZE,
PHP_BINARY_READ);
if($buf
===
false){
echo
socket_strerror(socket_last_error($this-‐>socket));
return
-‐2;
}
list($size,
$block,
$blocks)
=
explode(":",
$buf);
$aux
=
substr($buf,
$overload);
if($size
==
strlen($aux)){
$this-‐>send(self::ACK);
}else{
$this-‐>send(self::NO_ACK);
die('error
in
size');
}
$buffer
.=
$aux;
}while($block
<
$blocks);
return
lzf_decompress($buffer);
}
Se han condensado y/o
eliminado algunas partes
por claridad
50. www.tradukoj.com
El servidor utiliza el canal
de la misma manera
MiProyectoEnSF2
TAB
TAB: TranslationsApiBundle
/create-socket
{“port”:”10000”}
send => read
read => send
51. www.tradukoj.com
En el servidor discriminamos por el comando solicitado
do{
$buf
=
$this-‐>readSocket();
$read
=
json_decode($buf,
true);
$command
=
isset($read['command'])
?
$read['command']
:
'';
//
..
switch($command){
case
self::CMD_CATALOG_INDEX:
…
case
self::CMD_TRANSDOC_INDEX:
…
case
self::CMD_TRANSDOC_SYNC:
…
case
self::CMD_TRANSDOC_GET:
…
case
self::CMD_UPLOAD_KEYS:
…
case
self::CMD_DOWNLOAD_KEYS:
…
case
self::CMD_SHUTDOWN:
$this-‐>resultOk();
sleep(1);
socket_close($this-‐>msgsock);
exit;
default:
$this-‐>exception(sprintf('command
'%s'
unknow',
$command));
break;
}
}
while
(true);
Se han condensado y/o
eliminado algunas partes
por claridad
53. www.tradukoj.com
¿Qué queda por hacer?
• Dejar el socket siempre abierto.
!
• Control completo de todas las excepciones.
!
• Control de envío de paquetes en orden o
repetir si fallo.
!
• Tratar archivos xml y php.
!
• Subir claves nuevas en bloque.
!
• En el editor: mejorar la experiencia de
usuario, chat, mailing, edición colaborativa
…
54. www.tradukoj.com
¿Qué más queda por hacer?
• Gestión de usuarios (invitar/añadir).
!
• Permitir traducciones abiertas (al
estilo de translate.whatsapp.com) en las
que colaboran o validan un público
más abierto.
!
• Poder reservar subdominios para lo
anterior o apuntar subdominios
externos (estos servicios
probablemente serán de pago)
55. www.tradukoj.com
Agradecimientos
A mi empresa: por hacer de "conejillo de
indias" con las traducciones de
!
!
!
A mis compañeros por prestarse a este
experimento y soportar los
inconvenientes iniciales de la
implantación, y sobre todo por aportar
las críticas que me han ayudado a
mejorarlo.