Com treballar amb les localitzacions
Pels qui comenceu segurament us haureu hagut de barallar amb com treballar amb els textos per traduir d'una forma eficient. Jo després de provar-ho de moltes formes he trobat un forma prou interessant per fer-ho que us presento a continuació.
El primer que cal fer és crear-se un, o més, fitxers on hi haurà tots els elements per traduir. Jo els acostumo a anomenar amb un nom indicatiu dels missatges seguit de "_Localized.h".
Normalment quan volem afegir un text traduït hem d'utilitzar un d'aquests mètodes (n'hi ha d'altres, però).
NSLocalizedString(@"BUTTON_OK", @"Button OK text")El primer codi ens obtindrà el text traduït del fitxer per defecte "Localizable.strings" de l'aplicació.
NSLocalizedStringFromTable(@"BUTTON_OK", @"Logs", @"Button OK text")
NSLocalizedStringFromTableInBundle(@"BUTTON_OK", @"Logs", [NSBundle mainBundle], @"Button OK text")
NSLocalizedStringFromTableInBundle(@"BUTTON_OK", @"Logs", [NSBundle bundleForClass:NSClassFromString(@"LogsPrefPane")], @"Button OK text")
NSLocalizedStringWithDefaultValue(@"BUTTON_OK", @"Logs", [NSBundle bundleForClass:NSClassFromString(@"LogsPrefPane")], @"Ok", @"Button OK text")
El segon serveix per obtenir el text traduït d'un fitxer específic, en aquest cas: "Logs.strings" també de l'aplicació que s'està executant.
El tercer i quart codi ens permet indicar en quina aplicació hi ha la cadena traduïda i s'ha d'especificar amb un bundle depenent de l'opció. Per exemple, si volem obtenir-la de l'aplicació actual només cal indicar "[NSBundle mainBundle]", però si per exemple tenim una framework o un panell de preferències això no ens servirà. Caldrà especificar el bundle a partir d'una classe coneguda de la framework o del panell: "[NSBundle bundleForClass:NSClassFromString(@"LogsPrefPane")]".
L'última opció a més permet especificar un text per defecte. És útil si el text conté paràmetres que no s'han utilitzat a la clau. Si no hi ha valor per defecte s'utilitza el text de la clau.
És a dir, la clau del text a obtenir s'utilitzarà com a valor per defecte si no hi ha cap traducció. Però això pot implicar que tingueu claus duplicades que podrien traduir-se de forma diferent en altres idiomes i portar problemes de confusió. Us recomano que utilitzeu claus descriptives i curtes, i que sempre afegiu les traduccions en anglès que és sempre l'idioma per defecte si no és troba el de l'usuari.
En la descripció, és molt interessant indicar quin text s'espera que hi hagi, per aconsellar al traductor i es vol permetre una llicència per fer la traducció més entenedora. Cal indicar quins paràmetres hi haurà, a que fan referència i si és el cas que depenen d'altres traduccions fer-ne referència. Així el traductor por ajustar la composició amb l'article, el gènere, el nombre, etc que potser en l'original en anglès no s'utilitza ni article, ni gènere ni nombre. A l'hora, ens elements que formen part de composicions cal indicar en quines composicions poden formar part, per fer-hi les mateixes comprovacions.
Si teniu textos que formen part de composicions i a més poden mostrar-se sols, creeu dues entrades per a evitar problemes en la composició o al mostrar-se sols. Que el traductor pugui ajustar la traducció a cada situació.
01 juliol 2009 09:08
Per evitar escriure a dins el codi aquestes funcions tant llargues que si el text és també llarg, i si hi afegim el comentari encara ho pot ser més, el resultat és un codi poc treballable, o directament intractable. Per això recomano afegir els textos per traduir en fitxer acabats amb "_Localizable.h".
Un exemple "LogMessages_Localizable.h":
/*!
@header LogMessages_Localizable.h
@created 2009-01-26
@copyright Copyright 2009 xin.cat All rights reserved.
*/
#pragma mark Default Localized
#define kFmwk_LocLogs_Button_OK NSLocalizedStringFromTableInBundle(@"BUTTON_OK",@"LogMessages",kBundleFmwk,@"Default OK button")
#define kFmwk_LocLogs_Button_Cancel NSLocalizedStringFromTableInBundle(@"BUTTON_CANCEL",@"LogMessages",kBundleFmwk,@"Default Cancel Button")
#pragma mark Search Sources Localizations
#define kFmwk_LocLogs_Search_AppAdded NSLocalizedStringFromTableInBundle(@"SEARCH_MSG_APPLICATION_ADDED",@"LogMessages",kBundleFmwk,@"Message when application added. First parameter is the app name, secont the path.")
#define kFmwk_LocLogs_Search_StartThread NSLocalizedStringFromTableInBundle(@"SEARCH_ALT_START_THREAD",@"LogMessages",kBundleFmwk,@"Alert when start search thread. No parameters.")
#define kFmwk_LocLogs_Search_FinishThread NSLocalizedStringFromTableInBundle(@"SEARCH_ALT_FINISH_THREAD",@"LogMessages",kBundleFmwk,@"Alert when finish search thread. No parameters.")
#define kFmwk_LocLogs_Search_StartSearch NSLocalizedStringFromTableInBundle(@"SEARCH_ALT_START_SEARCH",@"LogMessages",kBundleFmwk,@"Alert when start search launched. No parameters.")
#define kFmwk_LocLogs_Search_AutoSave NSLocalizedStringFromTableInBundle(@"SEARCH_ALT_AUTO_SAVE",@"LogMessages",kBundleFmwk,@"Alert when search autosave sources. No parameters.")
#define kFmwk_LocLogs_Search_NoSources NSLocalizedStringFromTableInBundle(@"SEARCH_ALT_SOURCES_EMPTY",@"LogMessages",kBundleFmwk,@"Alert when no sources to search. No parameters.")
#define kFmwk_LocLogs_Search_NextSources NSLocalizedStringFromTableInBundle(@"SEARCH_ALT_NEXT_SOURCE",@"LogMessages",kBundleFmwk,@"Alert when search to next sources. First parameter contain source path.")
#define kFmwk_LocLogs_Search_FinishSearch NSLocalizedStringFromTableInBundle(@"SEARCH_ALT_FINISH_SEARCH",@"LogMessages",kBundleFmwk,@"Alert when all sources searched and this finish. No parameters.")
#define kFmwk_LocLogs_Search_ContinueFrom NSLocalizedStringFromTableInBundle(@"SEARCH_ALT_CONTINUE_FROM",@"LogMessages",kBundleFmwk,@"Alert when search continue from a specific patg. First parameter is the path.")
Si us hi fixeu, estem definint unes constants "kFmwk_LocLogs_*" que se substituiran en el codi pel text que li segueix. D'aquesta manera tenim tots els textos a traduir agrupats i ordenats i els podem utilitzar en qualsevol part del codi, els cops que calgui sense haver de re-escriure el text, la descripció i la resta de coses. Així:
NSRunAlertPanelRelativeToWindow(kLocPref_Grl_AlertSetuid_Title,
kLocPref_Grl_AlertSetuid_Message,
kLocPref_Grl_AlertSetuid_ButtonOK, nil, nil,
[[self mainView] window] );
Si el codi del text a mostrar depèn d'una variable és millor utilitzar els mètodes específics i oblidar-nos d'utilitzar les constants. Sense oblidar-nos d'afegir-los al fitxer de constants per a tenir-los ben organitzats:
NSString *key = [NSString stringWithFormat:@"ERROR_%d", errCode];
NSBundle *bundle = [NSBundle mainBundle];
NSString *text = [bundle localizedStringForKey:key value:nil table:@"LogMessages"];
NSString *message = [NSString stringWithFormat:text array:params];
01 juliol 2009 09:28
Aquests fitxers poden utilitzar-se en diferents aplicacions, frameworks o bundles, només cal definir el bundle definint el #define kBundleFmwk. En aquest cas, us recomano definir-lo en el fitxers "*_Prefix.pch". Així aquí podeu veure que aquesta aplicació puc utilitzar els missatges de la framework, simplement definint la macro kBundleFmwk per a que l'agafi de la framework:
#define kBundleFmwk [NSBundle bundleForClass:NSClassFromString(@"LogObject")]
#define kBundleAgent [NSBundle mainBundle]
Si volgués afegir el llistat de textos traduïts dins la mateixa aplicació, només caldria ficar-ho així, i automàticament agafaria els textos del propi programa. Així amb un sol fitxer de textos poden aprofitar-se en diverses aplicacions o frameworks:
#define kBundleFmwk [NSBundle mainBundle]
#define kBundleAgent [NSBundle mainBundle]
Per utilitzar els textos dins el codi, no podem oblidar d'afegir l'import on hi ha les constants:
#import "LogMessages_Localizable.h"
01 juliol 2009 09:33
Però això no és tot. Ens queda saber com se generen els fitxers "Localizable.strings" i "LogMessages.strings". Per a poder posteriorment fer les traduccions.
Per generar els fitxer ".strings" s'utilitza l'aplicació de línia de comandes "genstrings" i s'executa normalment així:
genstrings -o English.lproj *_Localizable.h"El primer paràmetre indica el directori on es crearan els fitxers ".strings" que tindran el nom de la taula que haguem definit: "LogMessages" en l'exemple. L'últim paràmetre indica els fitxers on voleu que es cerquin les mètodes: NSLocalizedString, NSLocalizedStringFromTable, NSLocalizedStringFromTableInBundle, NSLocalizedStringWithDefaultValue. A partir dels quals generarà els fitxers amb els textos:
LogMessages.strings:
/* Default Cancel Button */
"BUTTON_CANCEL" = "BUTTON_CANCEL";
/* Default OK button */
"BUTTON_OK" = "BUTTON_CANCEL";
/* Message when application added. First parameter is the app name, secont the path. */
"SEARCH_MSG_APPLICATION_ADDED" = "SEARCH_MSG_APPLICATION_ADDED";
/* Alert when start search thread. No parameters. */
"SEARCH_ALT_START_THREAD" = "SEARCH_ALT_START_THREAD";
/* Alert when finish search thread. No parameters. */
"SEARCH_ALT_FINISH_THREAD" = "SEARCH_ALT_FINISH_THREAD";
/* Alert when search autosave sources. No parameters. */
"SEARCH_ALT_AUTO_SAVE" = "SEARCH_ALT_AUTO_SAVE";
/* Alert when start search launched. No parameters. */
"SEARCH_ALT_START_SEARCH" = "SEARCH_ALT_START_SEARCH";
/* Alert when search continue from a specific patg. First parameter is the path. */
"SEARCH_ALT_CONTINUE_FROM" = "SEARCH_ALT_CONTINUE_FROM";
/* Alert when search to next sources. First parameter contain source path. */
"SEARCH_ALT_NEXT_SOURCE" = "SEARCH_ALT_NEXT_SOURCE";
/* Alert when no sources to search. No parameters. */
"SEARCH_ALT_SOURCES_EMPTY" = "SEARCH_ALT_SOURCES_EMPTY";
/* Alert when all sources searched and this finish. No parameters. */
"SEARCH_ALT_FINISH_SEARCH" = "SEARCH_ALT_FINISH_SEARCH";
Ara podeu agafar aquest fitxer i substituir la part de la dreta de l'igual (=) ficant el text que voleu que aparegui per l'idioma indicat (English.lproj) i afegint-hi els comentaris addicionals que cregueu adients. Així:
//
// BUTTONS
//
/* Default Cancel Button */
"BUTTON_CANCEL" = "Cancel";
/* Default OK button */
"BUTTON_OK" = "OK";
//
// SEARCHING LOGS MESSAGES
//
/* Message when application added. First parameter is the app name, secont the path. */
"SEARCH_MSG_APPLICATION_ADDED" = "Application '%1$@' added at path:\n\t%2$@";
/* Alert when start search thread. No parameters. */
"SEARCH_ALT_START_THREAD" = "Search thread had start.";
/* Alert when finish search thread. No parameters. */
"SEARCH_ALT_FINISH_THREAD" = "Search thread had finished";
/* Alert when search autosave sources. No parameters. */
"SEARCH_ALT_AUTO_SAVE" = "Search thread had autosaved application sources.";
/* Alert when start search launched. No parameters. */
"SEARCH_ALT_START_SEARCH" = "Searching had start searching.";
/* Alert when search continue from a specific patg. First parameter is the path. */
"SEARCH_ALT_CONTINUE_FROM" = "Search thread continue searching from: %@";
/* Alert when search to next sources. First parameter contain source path. */
"SEARCH_ALT_NEXT_SOURCE" = "Search thread had search new source: %@";
/* Alert when no sources to search. No parameters. */
"SEARCH_ALT_SOURCES_EMPTY" = "Search thread do not search because no sources found.";
/* Alert when all sources searched and this finish. No parameters. */
"SEARCH_ALT_FINISH_SEARCH" = "Searching had finished. ";
01 juliol 2009 09:45
Tingueu en compte dues coses:
- El format del fitxer .strings és UTF-16
- Cada cop que executeu aquesta aplicació us generarà el fitxer de zero, esborrant la traducció que haguéssiu fet anteriorment, els comentaris o l'ordre dels textos.
Bé, per evitar això jo no utilitzo directament aquesta aplicació, sinó que m'he creat un script amb python que em conserva les traduccions ja realitzades, els comentaris afegits (els laterals també) i l'ordre de les traduccions.
Quan s'executa, s'actualitzen els comentaris que han canviat, s'esborren els textos eliminats, però manté els textos ja traduïts. I a sota de tot, amb un comentari amb la data i hora, hi afegeix els nous textos a traduir, que l'usuari ja traduirà i col·locarà al seu lloc. Així:
//
// BUTTONS
//
/* Default Cancel Button */
"BUTTON_CANCEL" = "Cancel";
/* Default OK button */
"BUTTON_OK" = "OK";
//
// SEARCHING LOGS MESSAGES
//
/* Message when application added. First parameter is the app name, secont the path. */
"SEARCH_MSG_APPLICATION_ADDED" = "Application '%1$@' added at path:\n\t%2$@";
/* Alert when start search thread. No parameters. */
"SEARCH_ALT_START_THREAD" = "Search thread had start.";
/* Alert when finish search thread. No parameters. */
"SEARCH_ALT_FINISH_THREAD" = "Search thread had finished";
/* Alert when search autosave sources. No parameters. */
"SEARCH_ALT_AUTO_SAVE" = "Search thread had autosaved application sources.";
/* Alert when start search launched. No parameters. */
"SEARCH_ALT_START_SEARCH" = "Searching had start searching.";
/* Alert when search continue from a specific patg. First parameter is the path. */
"SEARCH_ALT_CONTINUE_FROM" = "Search thread continue searching from: %@";
/* Alert when search to next sources. First parameter contain source path. */
"SEARCH_ALT_NEXT_SOURCE" = "Search thread had search new source: %@";
/* Alert when no sources to search. No parameters. */
"SEARCH_ALT_SOURCES_EMPTY" = "Search thread do not search because no sources found.";
/* Alert when all sources searched and this finish. No parameters. */
"SEARCH_ALT_FINISH_SEARCH" = "Searching had finished. ";
// New entries: 2009-07-01 09:50:35
/* Message when application added. First parameter is the app name, secont the path. */
"SEARCH_MSG_APPLICATION_ADDED1" = "SEARCH_MSG_APPLICATION_ADDED1";
/* Message when application added. First parameter is the app name, secont the path. */
"SEARCH_MSG_APPLICATION_ADDED2" = "SEARCH_MSG_APPLICATION_ADDED2";
/* Message when application added. First parameter is the app name, secont the path. */
"SEARCH_MSG_APPLICATION_ADDED3" = "SEARCH_MSG_APPLICATION_ADDED3";
01 juliol 2009 09:53
L'escript (pyGenStrings.py) us l'enganxo aquí i s'utilitza així:
usage: pyGenStrings pathOut files ...
On "pathOut" serà el directori on hi ha els fitxers ".strings" i els "files" són els fitxers (amb comodins si es vol) d'on es generaran les cadenes.
L'escript bàsicament, llegeix tots els fitxers ".strings" del directori "pathOut" i en desa les dades. Posterioment executa l'aplicació "genstrgins -o <pathOut> <files>" per generar totes les traduccions. Posteriorment torna a llegir els nous fitxers per trobar els textos nous i conservar els antics.
Tingueu en compte que si canvieu la clau d'una traducció, se us eliminarà l'anterior i se us afegirà la nova. Per evitar això, el millor és anar al fitxer ".strings" i modificar allí també la clau.
pyGenString.py:
#! /usr/bin/env python
import os
import re
import sys
import time
import codecs
def combine_strings(previous,path_to):
# Charge new string files
string_files = {}
for f in os.listdir(path_to):
file_path = os.path.join(path_to,f)
if os.path.isfile(file_path) and f.endswith('.strings'):
try:
content = codecs.open(file_path,'r','utf-16').read()
entries = re.findall(ur'(.*?\n"(.*?)"\s*?=\s*?".*?"\s*?;[^\n]*)',content,re.S)
byname = {}
for e in entries:
byname[e[1]] = e
string_files[f] ={'content':content, 'entries':entries, 'byname':byname}
except:
pass
# Print old entries and replace comment if exists
for f,d in previous.items():
# Discard files with same content
if d['content'] == string_files[f]['content']: continue
old_entries = d['byname'].keys()
new_entries = string_files[f]['byname'].keys()
new_file_content = u''
# Add previous entries, and change comment if needed
for e in d['entries']:
if e[1] not in new_entries: continue;
new = string_files[f]['byname'][e[1]]
new_comment = re.findall(ur'\s*?/\*(.*?)\*/\s*?\n"',new[0],re.S)
old_comment = re.findall(ur'\s*?/\*(.*?)\*/\s*?\n"',e[0],re.S)
if len(old_comment) and new_comment[0] != old_comment[0]:
# re.sub delete extra '\'. Must duplicate
new_comment[0] = new_comment[0].replace('\\','\\\\')
new_file_content += re.sub(ur'(\s*?/\*).*?(\*/\s*?\n")',ur'\1%s\2'%new_comment[0],e[0],re.S)
else:
new_file_content += e[0]
new_file_content += '\n'*4
# Check for new entries
new_entries = set(new_entries).difference(old_entries)
if new_entries:
new_file_content += '// New entries: %s\n\n' % time.strftime('%F %T',time.localtime())
for k in string_files[f]['entries']:
if k[1] not in new_entries: continue;
new_file_content += k[0]
new_file_content += '\n'*2
file_path = os.path.join(path_to,f)
codecs.open(file_path,'w','utf-16').write(new_file_content)
def gen_strings(list_from,path_to):
os.system('genstrings -o %s %s' % (path_to,' '.join(list_from)))
def load_string_from(path):
string_files = {}
for f in os.listdir(path):
file_path = os.path.join(path,f)
if os.path.isfile(file_path) and f.endswith('.strings'):
try:
content = codecs.open(file_path,'r','utf-16').read()
entries = re.findall(ur'(.*?\n"(.*?)"\s*?=\s*?".*?"\s*?;[^\n]*)',content,re.S)
byname = {}
for e in entries:
byname[e[1]] = e
string_files[f] ={'content':content, 'entries':entries, 'byname':byname}
except Exception, e:
pass
return string_files
if __name__ == '__main__':
if len(sys.argv) < 3:
print "pyGenStrings usage: pyGenStrings pathOut files ..."
sys.exit(1)
load = load_string_from(sys.argv[1])
gen_strings(sys.argv[2:],sys.argv[1])
combine_strings(load,sys.argv[1])
01 juliol 2009 10:00
Però això no és tot, ja que fer això manualment és una feinada. Lo ideal seria fer-ho de forma automatizada cada cop que es compila.
És pot fer. Només cal que seguiu aquestes instruccions.
1.- Afegir el fitxer pyGenStrings.py en un directori conegut i doneu-li permisos d'execució.
chmod +x pyGenStrings.py
2.- A l'XCode aneu al target on voleu que es compilin els textos i hi afegiu un target de tipus "New Script Build Phase".

3.- Afegiu l'execució de l'script per tants idiomes com vulgueu. Recomano que només ho feu per l'anglès i utilitzeu programes de traducció per als altres idiomes. Però ho podeu fer directament. Recordeu que un servidor té el fitxer "pyGenString.py" en el directori "~/.bin/", vosaltres escriviu on l'hageu ficat vosaltres.
~/.bin/pyGenStrings.py English.lproj *_Localized.h
~/.bin/pyGenStrings.py Catalan.lproj *_Localized.h
~/.bin/pyGenStrings.py Spanish.lproj *_Localized.h
4.- Per últim cal ficar la nova phase a l'inici per que compili els missatges abans d'afegir-los al programa final. I també en podeu canviar el nom, per a saber que és el que fa.

Ara, cada cop que compileu, se us actualitzaran els fitxers ".strings" que posteriorment podreu anar traduint sense preocupar-vos que us desapareguin els textos ja traduïts.
01 juliol 2009 10:15
Espero que s'entengui, i que ho pogueu aprofitar. 
01 juliol 2009 10:17
