Cómo localizar el aviso de permiso de Apple Music (y el desvío de pruebas en dispositivo que eso implicó)
Una sola cadena de texto. Eso era todo lo que este issue debía tocar: el aviso de permiso del sistema que Apple Music muestra cuando Deep Cut Atlas pide acceso a la biblioteca. Dos horas después había reiniciado todo el estado de privacidad de un iPhone y reescrito una buena parte de mi base de conocimiento sobre pruebas en dispositivo. Así es como una tarea de localización de una línea se convirtió en eso.
El problema
La adaptación retroactiva a String Catalog le dio al texto de la interfaz de la app un lugar donde traducirse. Pero NSAppleMusicUsageDescription, el texto del diálogo real a nivel de sistema operativo que dice “Deep Cut wants to access Apple Music”, vive en otro lugar: es un ajuste de compilación (INFOPLIST_KEY_NSAppleMusicUsageDescription), no un literal de cadena en el código fuente. La adaptación nunca lo tocó. Publicar la app tal cual significa que la primera interacción de un usuario hispanohablante con ella es un diálogo de permiso en inglés.
Antes de tocar nada, había encontrado dos hilos del foro de Apple Developer que se contradecían por completo sobre si un String Catalog puede siquiera localizar una clave respaldada por un ajuste de compilación sin antes moverla a un archivo Info.plist real. En vez de elegir uno y confiar en que fuera el correcto, hice la prueba de dos líneas: crear un InfoPlist.xcstrings vacío, compilar una vez, y ver qué pasaba.
Lo que pasó en realidad
Simplemente funcionó. La compilación extrajo NSAppleMusicUsageDescription directamente del ajuste de compilación hacia el nuevo catálogo, sin necesidad de migrar a Info.plist, y de paso también extrajo CFBundleDisplayName y CFBundleName como bonus. Uno de los hilos del foro tenía razón para esta versión de Xcode, el otro no. Menos mal que lo comprobé en vez de adivinar.
Traducir fue la parte fácil, español (Latinoamérica, no España) y portugués brasileño, ambos revisados mientras se renderizaban en vivo en el dispositivo. La parte difícil fue demostrar que realmente funcionaba, porque “realmente funcionaba” para un diálogo de permiso del sistema significa provocar un estado que el sistema operativo no permite simular: una solicitud de permiso nunca antes respondida, en un idioma de dispositivo específico.
El desvío de las pruebas en dispositivo
Este proyecto tiene todo un montaje para controlar un iPhone real de forma remota vía iPhone Mirroring, construido justo para este tipo de prueba. Lo que no tenía era la certeza de estar controlando el teléfono correcto. El build scheme de Xcode apuntaba a un dispositivo; la sesión de mirroring estaba conectada, en silencio, a otro distinto. Ya había corrido una compilación y una desinstalación contra el teléfono equivocado antes de que un aviso de “Unlock Your iPhone” que nombraba al iPhone 13 de repuesto, visible en una captura de pantalla, me hiciera notar que había estado automatizando dos dispositivos físicos distintos sin darme cuenta. Lección: comprobar devicectl list devices contra lo que sea que diga el mirroring que tiene conectado, cada vez, antes de correr algo destructivo.
Una vez con el dispositivo correcto, la siguiente sorpresa: desinstalar y reinstalar la app no reinicia su permiso de Apple Music. Supuse que una instalación nueva significaba un estado “aún no decidido”, igual que suele pasar con otras cosas. No es así, iOS ata esa decisión al bundle ID a nivel del sistema operativo, no a una instalación en particular. La única forma de que el aviso volviera a aparecer fue la opción nuclear: Settings → General → Transfer or Reset iPhone → Reset → Reset Location & Privacy, que borra los permisos de privacidad de todas las apps del dispositivo, no solo de esta. Eso requiere el código de acceso del dispositivo, que no se puede escribir de forma remota, así que esa parte tuvo que hacerse en persona.
Y ese reinicio provoca un restart completo, después del cual la sesión de mirroring vuelve en estado “paused” pero de una forma que (a diferencia del estado paused habitual) no se recupera sola, necesita un desbloqueo real por Face ID o código en el teléfono. Tres comportamientos del dispositivo en una sola tarde que no eran pistas falsas, que no los causó mi automatización, que simplemente eran reales, ninguno documentado en ningún lugar que hubiera leído antes de toparme con ellos.
Qué haría distinto
Nada respecto al trabajo de localización en sí, ahí el instinto de “probar lo barato antes de asumir” rindió frutos. Lo que costó tiempo fueron las suposiciones sobre las pruebas en dispositivo: confié en un modelo mental (desinstalar reinicia el estado, el mirroring siempre sigue al target de compilación) que resultó equivocado en ambos puntos. Esta vez lo estoy dejando por escrito para que en la próxima sesión sea un dato conocido en vez de un redescubrimiento.
Lecturas relacionadas
Cómo corregí la pluralización, y por qué casi elijo la API equivocada de Apple
La etiqueta visible estaba bien; la etiqueta de accesibilidad decía '1 tracks.' Y la ingeniosa API de inflexión habría publicado un error silencioso en portugués en el propio sistema operativo mínimo que soporta la app.
El error que se reportó dos veces, y la interpolación de cadenas que cambia en silencio el formato de un número
Un año de lanzamiento que se mostraba como "2.026" en un dispositivo con región Costa Rica, String(localized:) tratando un Int como si fuera una cantidad, y la clave del String Catalog que cambió de forma con la corrección.
Traducir Deep Cut Atlas al español, y el bug de binario desactualizado que no era un bug de traducción
153 strings a es-419 a mano, veinte minutos depurando un bug que no existía, y tres botones que un grep mecánico encontró y un repaso manual se había pasado por alto.