Localización de aplicaciones en iOS. Parte 1. ¿Qué tenemos?

Localización de aplicaciones en iOS


Parte 1. ¿Qué tenemos?


Guía de recursos de cadena localizada


Introduccion


Hace unos años, me sumergí en el mundo mágico del desarrollo de iOS, que con toda su esencia me prometió un futuro feliz en el campo de las TI. Sin embargo, profundizando en las características de la plataforma y el entorno de desarrollo, me enfrenté a muchas dificultades e inconvenientes para resolver tareas aparentemente muy triviales: el "conservadurismo innovador" de Apple a veces hace que los desarrolladores sean muy sofisticados para satisfacer al cliente desenfrenado "WANT".


Uno de estos problemas es el problema de localizar los recursos de cadena de la aplicación. Me gustaría dedicar varias de mis primeras publicaciones sobre las extensiones de Habr a este problema.


Inicialmente, esperaba ajustar mis pensamientos en un artículo, pero la cantidad de información que me gustaría presentar era bastante grande. En este artículo intentaré descubrir la esencia de los mecanismos estándar para trabajar con recursos localizados con énfasis en algunos aspectos que la mayoría de las guías y tutoriales descuidan. El material está dirigido principalmente a desarrolladores principiantes (o aquellos que no se han encontrado con tales tareas). Para desarrolladores experimentados, esta información puede no ser particularmente valiosa. Pero sobre los inconvenientes y desventajas que se pueden encontrar en la práctica, lo diré en el futuro ...


Fuera de la caja Cómo se organiza el almacenamiento de recursos de cadena en aplicaciones iOS


Para empezar, notamos que la presencia de mecanismos de localización en la plataforma ya es una gran ventaja, porque salva al programador del desarrollo adicional y establece un formato único para trabajar con datos. Y a menudo, los mecanismos básicos son suficientes para la implementación de proyectos relativamente pequeños.


Y entonces, ¿qué oportunidades nos brinda Xcode "fuera de la caja"? Primero, veamos el estándar para almacenar recursos de cadena en un proyecto.


En proyectos con contenido estático, los datos de cadena se pueden almacenar directamente en la interfaz (archivos de marcado .storyboard y .xib , que a su vez son archivos XML representados con las herramientas de Interface Builder ) o en código. El primer enfoque nos permite simplificar y acelerar el proceso de marcado de pantallas y pantallas individuales, como el desarrollador puede observar la mayor parte del cambio sin compilar la aplicación. Sin embargo, en este caso no es difícil encontrarse con redundancia de datos (si varios elementos utilizan el mismo texto, las pantallas). El segundo enfoque simplemente elimina el problema de la redundancia de datos, pero lleva a la necesidad de llenar pantallas manualmente (configurando IBOutlet adicionales y asignándoles los valores de texto correspondientes), lo que a su vez conduce a la redundancia de código (por supuesto, excepto en los casos en que el texto debería ser instalado directamente por el código de la aplicación).


Además, Apple proporciona una extensión de archivo estándar .strings . Este estándar regula el formato para almacenar datos de cadena en forma de una matriz asociativa ( "-" ):


 "key" = "value"; 

La clave distingue entre mayúsculas y minúsculas, permite el uso de espacios, guiones bajos, signos de puntuación y caracteres especiales.


Es importante tener en cuenta que, a pesar de la sintaxis directa, los archivos de cadenas son fuentes regulares de errores durante la compilación, el ensamblaje o el funcionamiento de una aplicación. Hay varias razones para esto.


En primer lugar, los errores de sintaxis. La falta de punto y coma, signos de igual, comillas extra o sin escape conducirán inevitablemente a un error del compilador. Además, Xcode apuntará al archivo con el error, pero no resaltará la línea en la que algo está mal. Encontrar un error tipográfico puede llevar un tiempo considerable, especialmente si el archivo contiene una cantidad significativa de datos.


En segundo lugar, duplicación de claves. La aplicación, por supuesto, no se bloqueará por ello, pero se pueden mostrar datos incorrectos al usuario. La cuestión es que cuando se accede a una línea por clave, se extrae el valor correspondiente a la última aparición de la clave en el archivo.


Como resultado, un diseño simple requiere que el programador sea muy minucioso y atento al llenar archivos con datos.


Bien informado los desarrolladores pueden exclamar inmediatamente: "¿Pero qué pasa con JSON y PLIST? ¿Qué no les gustó?" Bueno, en primer lugar, JSON y PLIST (de hecho, XML ordinario) son estándares universales que permiten almacenar cadenas y datos numéricos, lógicos ( BOOL ), binarios, hora y fecha, así como colecciones indexadas ( Array ) y asociativas ( Dictionary ) matrices. En consecuencia, la sintaxis de estos estándares está más saturada y, por lo tanto, es más fácil mordisquearlos. En segundo lugar, la velocidad de procesamiento de dichos archivos es ligeramente inferior a la de los archivos de cadenas, nuevamente debido a la sintaxis más compleja. Esto sin mencionar el hecho de que para trabajar con ellos es necesario realizar una serie de manipulaciones en el código.


Localizado, localizado, pero no localizado. Localización de interfaz de usuario


Y así, con los estándares resueltos, ahora veamos cómo usarlo todo.


Vamos en orden. Primero, cree una aplicación de vista única simple y agregue algunos componentes de texto al Main.storyboard en el ViewController .




El contenido en este caso se almacena directamente en la interfaz. Para localizarlo, debe hacer lo siguiente:


1) Ir a la configuración del proyecto




2) Luego - del objetivo al proyecto




3) Abra la pestaña Información




En la sección de Localizaciones , vemos de inmediato que ya tenemos la entrada "Inglés - Idioma de desarrollo" . Esto significa que el inglés está configurado como el idioma de desarrollo (o predeterminado).


Agreguemos otro idioma ahora. Para hacer esto, haga clic en " + " y seleccione el idioma deseado (por ejemplo, elegí ruso). Caring Xcode nos ofrece de inmediato elegir qué archivos localizar para el idioma agregado.




Haga clic en Finalizar , vea lo que sucedió. En el navegador del proyecto, los botones para mostrar el anidamiento aparecieron cerca de los archivos seleccionados. Al hacer clic en ellos, vemos que los archivos seleccionados previamente contienen los archivos de localización creados.




Por ejemplo, Main.storyboard (Base) es el archivo de marcado de interfaz predeterminado en el lenguaje de desarrollo base, y al Main.strings (Russian) la localización, el Main.strings (Russian) asociado se creó en pares, un archivo de cadena para la localización rusa. Al abrirlo, puede ver lo siguiente:


 /* Class = "UILabel"; text = "Label"; ObjectID = "tQe-tG-eeo"; */ "tQe-tG-eeo.text" = "Label"; /* Class = "UITextField"; placeholder = "TextField"; ObjectID = "cpp-y2-Z0N"; */ "cpp-y2-Z0N.placeholder" = "TextField"; /* Class = "UIButton"; normalTitle = "Button"; ObjectID = "EKl-Rz-Dc2"; */ "EKl-Rz-Dc2.normalTitle" = "Button"; 

Aquí, en general, todo es simple, pero en aras de la claridad, consideraremos con más detalle, prestando atención a los comentarios generados por el cuidado Xcode:


 /* Class = "UILabel"; text = "Label"; ObjectID = "tQe-tG-eeo"; */ "tQe-tG-eeo.text" = "Label"; 

Aquí hay una instancia de la clase UILabel con el valor "Label" para el parámetro de text . ObjectID : el identificador del objeto en el archivo de marcado: esta es una línea única asignada a cualquier componente en el momento en que se coloca en el Storyboard/Xib . Es a partir del ObjectID y el nombre del parámetro del objeto (en este caso, text ) que se forma la clave, y el registro en sí puede interpretarse formalmente de la siguiente manera:


Establezca el parámetro de texto del objeto tQe-tG-eeo en Etiqueta.


En este registro, solo el " valor " está sujeto a cambios. Reemplace " Etiqueta " con " Etiqueta ". Haremos lo mismo con otros objetos.


 /* Class = "UILabel"; text = "Label"; ObjectID = "tQe-tG-eeo"; */ "tQe-tG-eeo.text" = ""; /* Class = "UITextField"; placeholder = "TextField"; ObjectID = "cpp-y2-Z0N"; */ "cpp-y2-Z0N.placeholder" = " "; /* Class = "UIButton"; normalTitle = "Button"; ObjectID = "EKl-Rz-Dc2"; */ "EKl-Rz-Dc2.normalTitle" = ""; 

Lanzamos nuestra aplicación.




Pero que vemos? La aplicación utiliza localización básica. ¿Cómo verificar si realizamos la transferencia correctamente?


Aquí vale la pena hacer una pequeña digresión y cavar un poco en la dirección de las características de la plataforma iOS y la estructura de la aplicación.


Para comenzar, considere cambiar la estructura del proyecto en el proceso de agregar localización. Así es como se ve el directorio del proyecto antes de agregar la localización rusa:




Y así después:




Como podemos ver, Xcode creó un nuevo directorio ru.lproj , en el que colocó las cadenas localizadas creadas.




¿Y dónde está la estructura del proyecto Xcode para la aplicación iOS terminada? Y a pesar del hecho de que esto ayuda a comprender mejor las características de la plataforma, así como los principios de distribución y almacenamiento de recursos directamente en la aplicación finalizada. La conclusión es que al ensamblar un proyecto Xcode, además de generar un archivo ejecutable, el entorno transfiere recursos (archivos de diseño de la interfaz Storyboard / Xib , imágenes, archivos de línea, etc.) a la aplicación terminada, preservando la jerarquía especificada en la etapa de desarrollo.


Para trabajar con esta jerarquía, Apple proporciona la clase Bundle(NSBundle) ( traducción gratuita ):


Apple usa el Bundle para proporcionar acceso a aplicaciones, marcos, complementos y muchos otros tipos de contenido. Los paquetes organizan los recursos en subdirectorios claramente definidos, y las estructuras de los paquetes varían según la plataforma y el tipo. Con el bundle , puede acceder a los recursos de un paquete sin conocer su estructura. Bundle es una interfaz única para buscar elementos, teniendo en cuenta la estructura del paquete, las necesidades del usuario, las localizaciones disponibles y otros factores relevantes.
Búsqueda y descubrimiento de un recurso.
Antes de comenzar a trabajar con un recurso, debe especificar su bundle . La clase Bundle tiene muchos constructores, pero main se usa con mayor frecuencia. Bundle.main proporciona una ruta a los directorios que contienen el código ejecutable actual. De esta manera, Bundle.main proporciona acceso a los recursos utilizados por la aplicación actual.

Considere la estructura Bundle.main usando la clase FileManager :




Con base en lo anterior, podemos concluir: cuando se carga la aplicación, se forma su Bundle.main se Bundle.main la localización actual del dispositivo (lenguaje del sistema), la localización de la aplicación y los recursos localizados. Luego, la aplicación selecciona de todas las localizaciones disponibles la que corresponde al idioma actual del sistema y extrae los recursos localizados correspondientes. Si no hay coincidencias, se utilizan los recursos del directorio predeterminado (en nuestro caso, la localización en inglés, ya que el inglés se definió como un lenguaje de desarrollo, y la necesidad de una localización adicional de recursos se puede descuidar). Si cambia el idioma del dispositivo a ruso y reinicia la aplicación, la interfaz ya corresponderá a la localización en ruso.




Pero antes de cerrar el tema de localizar la interfaz de usuario a través de Interface Builder , vale la pena señalar otra forma notable. Al crear archivos de localización (al agregar un nuevo idioma al proyecto o en el inspector de archivos localizado), es fácil notar que Xcode brinda la posibilidad de elegir el tipo de archivo a crear:




En lugar de un archivo de línea, puede crear fácilmente un Storyboard/Xib localizado que guardará todo el marcado del archivo base. Una gran ventaja de este enfoque es que el desarrollador puede ver de inmediato cómo se mostrará el contenido en un idioma en particular y corregir de inmediato el diseño de la pantalla, especialmente si la cantidad de texto difiere u otra dirección del texto (por ejemplo, en árabe, hebreo), etc. . Pero al mismo tiempo, la creación de archivos Storyboard / Xib adicionales aumenta significativamente el tamaño de la aplicación en sí misma (de todos modos, los archivos de cadena ocupan mucho menos espacio).


Por lo tanto, al elegir uno u otro método de localización de interfaz, vale la pena considerar qué enfoque será más apropiado y práctico en una situación particular.


Hágalo usted mismo Trabajando con recursos de cadena localizados en código


Esperemos que con contenido estático, todo esté más o menos claro. Pero, ¿qué pasa con el texto que se establece directamente en el código?


Los desarrolladores del sistema operativo iOS se encargaron de esto.


Para trabajar con recursos de texto localizados, el marco de Foundation proporciona la familia de métodos NSLocalizedStrings en Swift


 NSLocalizedString(_ key: String, comment: String) NSLocalizedString(_ key: String, tableName: String?, bundle: Bundle, value: String, comment: String) 

y macros en Objective-C


 NSLocalizedString(key, comment) NSLocalizedStringFromTable(key, tbl, comment) NSLocalizedStringFromTableInBundle(key, tbl, bundle, comment) NSLocalizedStringWithDefaultValue(key, tbl, bundle, val, comment) 

Comencemos con lo obvio. El parámetro key es la clave de cadena en el archivo de cadenas; val (valor predeterminado): el valor predeterminado que se utiliza si la clave especificada no está en el archivo; comment : (menos obvio) una breve descripción de la cadena localizada (de hecho, no tiene una funcionalidad útil y pretende explicar el propósito de usar una cadena específica).


En cuanto a los parámetros tableName ( tbl ) y bunble , deben considerarse con más detalle.


tableName ( tbl ) es el nombre del archivo String (para ser sincero, no sé por qué Apple lo llama una tabla), que contiene la fila que necesitamos por la clave especificada; cuando se transfiere, la extensión .string no se especifica. La capacidad de navegar entre tablas le permite no almacenar recursos de cadena en un archivo, sino distribuirlos a su propia discreción. Esto le permite deshacerse de la congestión de archivos, simplifica la edición y minimiza la posibilidad de errores.


El parámetro de bundle extiende la navegación de recursos aún más. Como se mencionó anteriormente, el paquete es un mecanismo para acceder a los recursos de la aplicación, es decir, podemos determinar de forma independiente la fuente de los recursos.


Un poco mas. Iremos directamente a la Fundación y consideraremos la declaración de métodos (macros) para obtener una imagen más clara, porque La gran mayoría de los tutoriales simplemente ignoran este punto. El marco Swift no es muy informativo:


 /// Returns a localized string, using the main bundle if one is not specified. public func NSLocalizedString(_ key: String, tableName: String? = default, bundle: Bundle = default, value: String = default, comment: String) -> String 

"El paquete principal devuelve una cadena localizada" , todo lo que tenemos. Objective-C es un poco diferente.


 #define NSLocalizedString(key, comment) \ [NSBundle.mainBundle localizedStringForKey:(key) value:@"" table:nil] #define NSLocalizedStringFromTable(key, tbl, comment) \ [NSBundle.mainBundle localizedStringForKey:(key) value:@"" table:(tbl)] #define NSLocalizedStringFromTableInBundle(key, tbl, bundle, comment) \ [bundle localizedStringForKey:(key) value:@"" table:(tbl)] #define NSLocalizedStringWithDefaultValue(key, tbl, bundle, val, comment) \ [bundle localizedStringForKey:(key) value:(val) table:(tbl)] 

Aquí ya puede ver claramente que nada menos que bundle (en los primeros dos casos mainBundle ) funciona con archivos de recursos de cadena, lo mismo que en el caso de la localización de la interfaz. Por supuesto, podría decir esto inmediatamente, considerando la clase Bundle ( NSBundle ) en el párrafo anterior, pero en ese momento esta información no tenía un valor práctico particular. Pero en el contexto de trabajar con líneas en el código, esto no se puede decir. De hecho, las funciones globales proporcionadas por la Fundación son solo envoltorios sobre los métodos de paquete estándar, cuya tarea principal es hacer que el código sea más conciso y seguro. Nadie prohíbe inicializar el bundle manual y acceder directamente a los recursos en su nombre, pero de esta manera parece (aunque muy, muy pequeña) la probabilidad de la formación de enlaces circulares y pérdidas de memoria.


Los siguientes ejemplos describirán cómo trabajar con funciones globales y macros.


Veamos cómo funciona todo.
Primero, cree un archivo de cadena que contendrá nuestros recursos de cadena. Llámelo Localizable.strings * y agréguelo.


 "testKey" = "testValue"; 

( Los archivos de cadena se localizan exactamente de la misma manera que Storyboard / Xib , por lo que no describiré este proceso. Reemplazaremos el "valor de prueba " en el archivo de localización ruso con " valor de prueba *").


Importante! En iOS, un archivo con este nombre es el archivo de recursos de cadena predeterminado, es decir si no especifica el nombre de la tabla tableName ( tbl ), la aplicación activará automáticamente Localizable.strings .


Agregue el siguiente código a nuestro proyecto


 //Swift print("String for 'testKey': " + NSLocalizedString("testKey", comment: "")) 

 //Objective-C NSLog(@"String for 'testKey': %@", NSLocalizedString(@"testKey", @"")); 

y ejecutar el proyecto Después de ejecutar el código, aparecerá una línea en la consola.


 String for 'testKey': testValue 

¡Todo funciona bien!


De manera similar con el ejemplo de localización de la interfaz, cambie la localización y ejecute la aplicación. El resultado de la ejecución del código será


 String for 'testKey':   

Ahora intentemos obtener el valor mediante la clave, que no está en el archivo Localizable.strings :


 //Swift print("String for 'unknownKey': " + NSLocalizedString("unknownKey", comment: "")) 

 //Objective-C NSLog(@"String for 'unknownKey': %@", NSLocalizedString(@"unknownKey", @"")); 

El resultado de la ejecución de dicho código será


 String for 'unknownKey': unknownKey 

Como no hay una clave en el archivo, el método devuelve la clave como resultado. Si tal resultado es inaceptable, entonces es mejor usar el método


 //Swift print("String for 'testKey': " + NSLocalizedString("unknownKey", tableName: nil, bundle: Bundle.main, value: "noValue", comment: "")) 

 //Objective-C NSLog(@"String for 'testKey': %@", NSLocalizedStringWithDefaultValue(@"unknownKey", nil, NSBundle.mainBundle, @"noValue", @"")); 

donde hay un parámetro de value ( valor predeterminado ). Pero en este caso, debe especificar la fuente de recursos: bundle .


Las cadenas localizadas admiten el mecanismo de interpolación, similar a las cadenas estándar de iOS. Para hacer esto, agregue un registro al archivo de cadena usando literales de cadena ( %@ , %li , %f , etc.), por ejemplo:


 "stringWithArgs" = "String with %@: %li, %f"; 

Para generar esa línea, debe agregar un código del formulario


 //Swift print(String(format: NSLocalizedString("stringWithArgs", comment: ""), "some", 123, 123.098 )) 

 //Objective-C NSLog(@"%@", [NSString stringWithFormat: NSLocalizedString(@"stringWithArgs", @""), @"some", 123, 123.098]); 

¡Pero cuando use tales diseños, debe tener mucho cuidado! El hecho es que iOS monitorea estrictamente el número, el orden de los argumentos, la correspondencia de sus tipos con los literales especificados. Entonces, por ejemplo, si sustituye la cadena como el segundo argumento en lugar del valor entero


 //Swift print(String(format: NSLocalizedString("stringWithArgs", comment: ""), "some", "123", 123.098 )) 

 //Objective-C NSLog(@"%@", [NSString stringWithFormat: NSLocalizedString(@"stringWithArgs", @""), @"some", @"123", 123.098]); 

entonces la aplicación sustituirá el código entero de la cadena "123" en lugar del desajuste


 "String with some: 4307341664, 123.089000" 

Si lo omites, obtenemos


 "String with some: 0, 123.089000" 

Pero si omite el objeto correspondiente a %@ en la lista de argumentos


 //Swift print(String(format: NSLocalizedString("stringWithArgs", comment: ""), "123", 123.098 )) 

 //Objective-C NSLog(@"%@", [NSString stringWithFormat: NSLocalizedString(@"stringWithArgs", @""), @"123", 123.098]); 

entonces la aplicación simplemente se bloqueará en el momento de la ejecución del código.


¡Empújame, bebé! Localización de notificaciones


Otra tarea importante al trabajar con recursos de cadena localizados, de la que me gustaría hablar brevemente, es la tarea de localizar notificaciones. La conclusión es que la mayoría de los tutoriales (tanto en Push Notifications como en Localizable Strings ) a menudo descuidan este problema, y ​​tales tareas no son tan raras. Por lo tanto, cuando se enfrenta a esto por primera vez, el desarrollador puede tener una pregunta razonable: ¿ es esto posible en principio? Aquí no consideraré el mecanismo de operación del Apple Push Notification Service , especialmente desde que comencé con iOS 10.0, las notificaciones push y locales se implementan a través del mismo marco: UserNotifications .


Debe desarrollar un problema similar al desarrollar aplicaciones multilingües cliente-servidor. Cuando tal tarea me enfrentó por primera vez, lo primero que me vino a la mente fue descartar el problema de la localización de mensajes en el lado del servidor. La idea era extremadamente simple: la aplicación envía la localización actual al backend al inicio, y el servidor selecciona el mensaje apropiado al enviar el envío. Pero el problema maduró de inmediato: si la localización del dispositivo cambió y la aplicación no se reinició (no actualizó los datos en la base de datos), el servidor envió el texto correspondiente a la última localización "registrada". Y si la aplicación se instala en varios dispositivos con diferentes idiomas de sistema a la vez, entonces toda la implementación funcionaría como el infierno sabe qué. Como tal solución me pareció la muleta más salvaje de inmediato, inmediatamente comencé a buscar soluciones adecuadas (divertido, pero en muchos foros los "desarrolladores" me aconsejaron localizar a los agresores exactamente en el backend ).


La decisión correcta fue terriblemente simple, aunque no del todo obvia. En lugar del JSON estándar enviado por el servidor en APNS


  "aps" : { "alert" : { "body" : "some message"; }; }; 

necesita enviar JSON del formulario


  "aps" : { "alert" : { "loc-key" : "message localized key"; }; }; 

donde la loc-key se usa para pasar la clave de cadena Localizable.strings archivo Localizable.strings . En consecuencia, el mensaje de inserción se muestra de acuerdo con la localización actual del dispositivo.


El mecanismo para interpolar cadenas localizadas en notificaciones push funciona de manera similar:


  "aps" : { "alert" : { "loc-key" : "message localized key"; "loc-args" : [ "First argument", "Second argument" ]; }; }; 

La clave loc-args pasa una serie de argumentos que deben incrustarse en el texto de notificación localizado.


Para resumir ...


Y entonces, ¿qué tenemos al final?


  • estándar para almacenar datos de cadena en archivos .string especializados con sintaxis simple y accesible;
  • la capacidad de localizar la interfaz sin manipulaciones adicionales en el código;
  • acceso rápido a recursos localizados desde el código;
  • generación automática de archivos de localización y estructuración de los recursos del directorio del proyecto (aplicación) usando herramientas Xcode;
  • La capacidad de localizar el texto de notificación.

, Xcode , .


.

Source: https://habr.com/ru/post/es419077/


All Articles