Localização de aplicativos no iOS. Parte 1. O que temos?

Localização de aplicativos no iOS


Parte 1. O que temos?


Guia de recursos de cadeia localizada


1. Introdução


Há alguns anos, mergulhei no mundo mágico do desenvolvimento iOS, que com toda sua essência me prometeu um futuro feliz no campo da TI. No entanto, investigando profundamente os recursos da plataforma e do ambiente de desenvolvimento, enfrentei muitas dificuldades e inconvenientes em resolver tarefas aparentemente muito triviais: o "conservadorismo inovador" da Apple às vezes torna os desenvolvedores muito sofisticados para satisfazer o desenfreado cliente "EU QUERO".


Um desses problemas é a questão de localizar os recursos de seqüência de caracteres do aplicativo. Gostaria de dedicar várias das minhas primeiras publicações sobre as extensões de Habr a este problema.


Inicialmente, eu esperava encaixar meus pensamentos em um artigo, mas a quantidade de informações que eu gostaria de apresentar era bastante grande. Neste artigo, tentarei descobrir a essência dos mecanismos padrão para trabalhar com recursos localizados, com ênfase em alguns aspectos que a maioria dos guias e tutoriais negligencia. O material é destinado principalmente aos desenvolvedores iniciantes (ou àqueles que não encontraram essas tarefas). Para desenvolvedores experientes, essas informações podem não ser particularmente valiosas. Mas sobre os inconvenientes e desvantagens que podem ser encontrados na prática, vou contar no futuro ...


Fora da caixa. Como o armazenamento de recursos de sequência é organizado em aplicativos iOS


Para começar, observamos que a presença de mecanismos de localização na plataforma já é uma grande vantagem, porque salva o programador do desenvolvimento adicional e define um formato único para trabalhar com dados. E, freqüentemente, os mecanismos básicos são suficientes para a implementação de projetos relativamente pequenos.


E assim, que oportunidades o Xcode nos oferece "fora da caixa"? Primeiro, vejamos o padrão para armazenar recursos de string em um projeto.


Em projetos com conteúdo estático, os dados da string podem ser armazenados diretamente na interface (arquivos de marcação .xib e .xib , que por sua vez são arquivos XML renderizados usando as ferramentas do Interface Builder ) ou no código. A primeira abordagem nos permite simplificar e acelerar o processo de marcação de telas e displays individuais, como o desenvolvedor pode observar a maioria das alterações sem criar o aplicativo. No entanto, nesse caso, não é difícil encontrar redundância de dados (se o mesmo texto for usado por vários elementos, será exibido). A segunda abordagem apenas elimina o problema da redundância de dados, mas leva à necessidade de preencher as telas manualmente (configurando IBOutlet adicionais e atribuindo-lhes valores de texto correspondentes), o que leva à redundância de código (é claro, exceto nos casos em que o texto deve ser instalado diretamente pelo código do aplicativo).


Além disso, a Apple fornece uma extensão de arquivo padrão .strings . Este padrão regula o formato para armazenar dados de string na forma de uma matriz associativa ( "-" ):


 "key" = "value"; 

A chave diferencia maiúsculas de minúsculas, permite o uso de espaços, sublinhados, pontuação e caracteres especiais.


É importante observar que, apesar da sintaxe direta, os arquivos Strings são fontes regulares de erros durante a compilação, montagem ou operação de um aplicativo. Existem várias razões para isso.


Em primeiro lugar, erros de sintaxe. Ponto e vírgula em falta, sinais de igual, citações extras ou sem escape inevitavelmente levarão a um erro do compilador. Além disso, o Xcode apontará para o arquivo com o erro, mas não destacará a linha na qual algo está errado. Encontrar esse erro de digitação pode levar um tempo considerável, especialmente se o arquivo contiver uma quantidade significativa de dados.


Em segundo lugar, duplicação de chaves. O aplicativo, é claro, não trava por causa disso, mas dados incorretos podem ser exibidos para o usuário. O fato é que, ao acessar uma linha por chave, o valor correspondente à última ocorrência da chave no arquivo é exibido.


Como resultado, um design simples exige que o programador seja muito cuidadoso e atencioso ao preencher arquivos com dados.


Conhecimento os desenvolvedores podem exclamar imediatamente: "Mas e JSON e PLIST? O que eles não agradaram?" Bem, primeiro, JSON e PLIST (de fato, XML comum) são padrões universais que permitem armazenar seqüências de caracteres e dados numéricos, lógicos ( BOOL ), binários, hora e data, além de coleções - indexadas ( Array ) e associativas ( Dictionary ). Consequentemente, a sintaxe desses padrões é mais saturada e, portanto, mais fácil de mordê-los. Em segundo lugar, a velocidade de processamento desses arquivos é um pouco menor que os arquivos Strings, novamente devido à sintaxe mais complexa. Isso sem mencionar o fato de que, para trabalhar com eles, é necessário executar várias manipulações no código.


Localizado, localizado, mas não localizado. Localização da interface do usuário


E assim, com os padrões estabelecidos, agora vamos descobrir como usar tudo.


Vamos em ordem. Primeiro, crie um aplicativo de exibição única simples e adicione alguns componentes de texto ao Main.storyboard no ViewController .




O conteúdo neste caso é armazenado diretamente na interface. Para localizá-lo, você deve fazer o seguinte:


1) Vá para as configurações do projeto




2) Então - do alvo ao projeto




3) Abra a guia Informações




Na seção Localizações , vemos imediatamente que já temos a entrada "Inglês - Idioma de desenvolvimento" . Isso significa que o inglês está definido como o idioma de desenvolvimento (ou padrão).


Vamos adicionar outro idioma agora. Para fazer isso, clique em " + " e selecione o idioma desejado (por exemplo, eu escolhi russo). O Caring Xcode oferece-nos imediatamente a escolha de quais arquivos localizar para o idioma adicionado.




Clique em Concluir , veja o que aconteceu. No navegador do projeto, os botões para exibir o aninhamento apareceram perto dos arquivos selecionados. Ao clicar neles, vemos que os arquivos selecionados anteriormente contêm os arquivos de localização criados.




Por exemplo, Main.storyboard (Base) é o arquivo de marcação da interface padrão no idioma de desenvolvimento base e, ao formar a localização, o Main.strings (Russian) associado foi criado em pares - um arquivo de seqüência de caracteres para a localização em russo. Ao abri-lo, você pode ver o seguinte:


 /* 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"; 

Aqui, em geral, tudo é simples, mas por uma questão de clareza, consideraremos com mais detalhes, prestando atenção aos comentários gerados pelo Xcode atencioso:


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

Aqui está uma instância da classe UILabel com o valor "Label" para o parâmetro de text . ObjectID - o identificador do objeto no arquivo de marcação - esta é uma linha exclusiva atribuída a qualquer componente no momento em que é colocado no Storyboard/Xib . É a partir do ObjectID e do nome do parâmetro do objeto (nesse caso, text ) que a chave é formada e o próprio registro pode ser formalmente interpretado da seguinte maneira:


Defina o parâmetro de texto do objeto tQe-tG-eeo como Label.


Neste registro, apenas o " valor " está sujeito a alterações. Substitua " Etiqueta " por " Etiqueta ". Faremos o mesmo com outros 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" = ""; 

Lançamos nosso aplicativo.




Mas o que vemos? O aplicativo usa localização básica. Como verificar se fizemos a transferência corretamente?


Aqui vale a pena fazer uma pequena digressão e cavar um pouco na direção dos recursos da plataforma e estrutura do aplicativo iOS.


Para começar, considere alterar a estrutura do projeto no processo de adicionar localização. É assim que o diretório do projeto se parece antes de adicionar a localização em russo:




E depois:




Como podemos ver, o Xcode criou um novo diretório ru.lproj , no qual colocou as strings localizadas criadas.




E onde está a estrutura do projeto Xcode para o aplicativo iOS concluído? E apesar do fato de isso ajudar a entender melhor os recursos da plataforma, bem como os princípios de distribuição e armazenamento de recursos diretamente no aplicativo finalizado. O ponto principal é que, ao montar um projeto Xcode, além de gerar um arquivo executável, o ambiente transfere recursos (arquivos de layout da interface Storyboard / Xib , imagens, arquivos de linha etc.) para o aplicativo finalizado, preservando a hierarquia especificada no estágio de desenvolvimento.


Para trabalhar com essa hierarquia, a Apple fornece a classe Bundle(NSBundle) ( tradução gratuita ):


A Apple usa o Bundle para fornecer acesso a aplicativos, estruturas, plugins e muitos outros tipos de conteúdo. Pacotes configuráveis ​​organizam recursos em subdiretórios claramente definidos, e as estruturas de pacotes configuráveis ​​variam de acordo com a plataforma e o tipo. Usando o bundle , você pode acessar os recursos de um pacote sem conhecer sua estrutura. Bundle é uma interface única para procurar elementos, levando em consideração a estrutura do pacote, as necessidades do usuário, as localizações disponíveis e outros fatores relevantes.
Pesquisa e descoberta de um recurso
Antes de começar a trabalhar com um recurso, você deve especificar seu bundle . A classe Bundle tem muitos construtores, mas main é usado com mais frequência. Bundle.main fornece um caminho para diretórios que contêm o código executável atual. Dessa forma, Bundle.main fornece acesso aos recursos usados ​​pelo aplicativo atual.

Considere a estrutura Bundle.main usando a classe FileManager :




Com base no exposto, podemos concluir: quando o aplicativo é carregado, seu Bundle.main é formado, a localização atual do dispositivo (idioma do sistema), a localização do aplicativo e os recursos localizados são analisados. Em seguida, o aplicativo seleciona de todas as localizações disponíveis a que corresponde ao idioma atual do sistema e extrai os recursos localizados correspondentes. Se não houver correspondências, os recursos do diretório padrão serão utilizados (no nosso caso, localização em inglês, pois o inglês foi definido como um idioma de desenvolvimento e a necessidade de localização adicional de recursos pode ser negligenciada). Se você alterar o idioma do dispositivo para russo e reiniciar o aplicativo, a interface já corresponderá à localização em russo.




Porém, antes de encerrar o tópico de localização da interface do usuário por meio do Interface Builder , vale a pena observar outra maneira digna de nota. Ao criar arquivos de localização (adicionando um novo idioma ao projeto ou no inspetor de arquivos localizado), é fácil perceber que o Xcode oferece a capacidade de escolher o tipo de arquivo a ser criado:




Em vez de um arquivo de linha, você pode criar facilmente um Storyboard/Xib localizado que salvará toda a marcação do arquivo base. Uma grande vantagem dessa abordagem é que o desenvolvedor pode ver imediatamente como o conteúdo será exibido em um idioma específico e corrigir imediatamente o layout da tela, especialmente se a quantidade de texto for diferente ou se outra direção do texto for usada (por exemplo, em árabe, hebraico) e assim por diante. . Mas, ao mesmo tempo, a criação de arquivos Storyboard / Xib adicionais aumenta significativamente o tamanho do próprio aplicativo (mesmo assim, os arquivos de sequência ocupam muito menos espaço).


Portanto, ao escolher um ou outro método de localização de interface, vale a pena considerar qual abordagem será mais apropriada e prática em uma situação específica.


Faça você mesmo. Trabalhando com recursos de sequência localizados no código


Felizmente, com conteúdo estático, tudo fica mais ou menos claro. Mas e o texto definido diretamente no código?


Os desenvolvedores do sistema operacional iOS cuidaram disso.


Para trabalhar com recursos de texto localizados, a estrutura do Foundation fornece a família de métodos NSLocalizedStrings no Swift


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

e macros em Objective-C


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

Vamos começar com o óbvio. O parâmetro key é a chave de string no arquivo Strings; val (valor padrão) - o valor padrão usado se a chave especificada não estiver no arquivo; comment - (menos óbvio) uma breve descrição da string localizada (na verdade, ela não possui funcionalidade útil e se destina a explicar o propósito de usar uma string específica).


Quanto aos parâmetros tableName ( tbl ) e bunble , eles devem ser considerados com mais detalhes.


tableName ( tbl ) é o nome do arquivo String (para ser sincero, não sei por que a Apple a chama de tabela), que contém a linha que precisamos pela chave especificada; quando é transferido, a extensão .string não .string especificada. A capacidade de navegar entre tabelas permite não armazenar recursos de sequência em um arquivo, mas distribuí-los a seu próprio critério. Isso permite que você se livrar do congestionamento de arquivos, simplifica a edição e minimiza a chance de erros.


O parâmetro bundle estende a navegação de recursos ainda mais. Como mencionado anteriormente, o pacote configurável é um mecanismo para acessar os recursos do aplicativo, ou seja, podemos determinar independentemente a fonte de recursos.


Um pouco mais Iremos diretamente à Fundação e consideraremos a declaração de métodos (macros) para uma imagem mais clara, porque a grande maioria dos tutoriais simplesmente ignora esse ponto. A estrutura Swift não é muito informativa:


 /// 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 

"O pacote principal retorna uma string localizada" - tudo o que temos. Objective-C é um pouco 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)] 

Aqui você já pode ver claramente que nada além de bundle (nos dois primeiros casos mainBundle ) funciona com arquivos de recursos de sequência - o mesmo que no caso da localização da interface. É claro que eu poderia dizer imediatamente isso, considerando a classe Bundle ( NSBundle ) no parágrafo anterior, mas naquela época essas informações não tinham um valor prático específico. Mas no contexto de trabalhar com linhas no código, isso não pode ser dito. De fato, as funções globais fornecidas pela Fundação são apenas invólucros dos métodos de pacote padrão, cuja principal tarefa é tornar o código mais conciso e seguro. Ninguém proíbe inicializar bundle manualmente e acessar diretamente recursos em seu nome, mas dessa maneira parece (embora muito, muito pequena) a probabilidade de formação de links circulares e vazamentos de memória.


Os exemplos abaixo descrevem como trabalhar com funções e macros globais.


Vamos ver como tudo funciona.
Primeiro, crie um arquivo String que conterá nossos recursos de string. Chame-o Localizable.strings * e adicione-o.


 "testKey" = "testValue"; 

(A localização dos arquivos String é realizada exatamente da mesma maneira que o Storyboard / Xib , portanto, não descreverei esse processo. Substituiremos “ testValue ” no arquivo de localização russo por “ test value *”.)


Importante! No iOS, um arquivo com esse nome é o arquivo de recurso de sequência padrão, ou seja, se você não especificar o nome da tabela tableName ( tbl ), o aplicativo irá bater automaticamente em Localizable.strings .


Adicione o seguinte código ao nosso projeto


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

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

e execute o projeto. Depois de executar o código, uma linha aparecerá no console


 String for 'testKey': testValue 

Tudo funciona bem!


Da mesma forma com o exemplo de localização da interface, altere a localização e execute o aplicativo. O resultado da execução do código será


 String for 'testKey':   

Agora vamos tentar obter o valor pela chave, que não está no arquivo Localizable.strings :


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

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

O resultado da execução desse código será


 String for 'unknownKey': unknownKey 

Como não há chave no arquivo, o método retorna a própria chave como resultado. Se esse resultado for inaceitável, é melhor usar o 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", @"")); 

onde existe um parâmetro de value ( valor padrão ). Mas, neste caso, você deve especificar a fonte de recursos - bundle .


As seqüências localizadas suportam o mecanismo de interpolação, semelhante às seqüências padrão do iOS. Para fazer isso, adicione um registro ao arquivo de string usando literais de string ( %@ , %li , %f , etc.), por exemplo:


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

Para gerar essa linha, você deve adicionar um código no formato


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

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

Mas ao usar esses designs, você precisa ter muito cuidado! O fato é que o iOS monitora estritamente o número, a ordem dos argumentos e a correspondência de seus tipos com os literais especificados. Portanto, por exemplo, se você substituir a sequência como o segundo argumento em vez do valor inteiro


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

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

o aplicativo substituirá o código inteiro da sequência "123" no lugar de incompatibilidade


 "String with some: 4307341664, 123.089000" 

Se você pular, temos


 "String with some: 0, 123.089000" 

Mas se você pular o objeto correspondente a %@ na lista de argumentos


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

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

o aplicativo simplesmente trava no momento da execução do código.


Me empurre, baby! Localização de notificações


Outra tarefa importante no trabalho com recursos de string localizados, dos quais gostaria de falar brevemente, é a tarefa de localizar notificações. A conclusão é que a maioria dos tutoriais (tanto em Push Notifications quanto em Localizable Strings ) geralmente negligencia esse problema e essas tarefas não são tão raras. Portanto, quando confrontado com isso pela primeira vez, o desenvolvedor pode ter uma pergunta razoável: isso é possível em princípio? Não considerarei o mecanismo de operação do Apple Push Notification Service aqui, principalmente porque, a partir do iOS 10.0, as notificações Push e local são implementadas na mesma estrutura - UserNotifications .


Você precisa enfrentar um problema semelhante ao desenvolver aplicativos multilíngües cliente-servidor. Quando essa tarefa me confrontou, a primeira coisa que me veio à mente foi afastar o problema da localização de mensagens no servidor. A idéia era extremamente simples: o aplicativo envia a localização atual para o back - end na inicialização e o servidor seleciona a mensagem apropriada ao enviar o push . Mas o problema desapareceu imediatamente: se a localização do dispositivo mudou e o aplicativo não foi reiniciado (não atualizou os dados no banco de dados), o servidor enviou o texto correspondente à última localização "registrada". E se o aplicativo estiver instalado em vários dispositivos com diferentes linguagens do sistema ao mesmo tempo, toda a implementação funcionaria como se o inferno fosse o que fosse. Como essa solução imediatamente me pareceu a muleta mais louca, comecei imediatamente a procurar soluções adequadas (engraçado, mas em muitos fóruns os "desenvolvedores" aconselharam a localizar os pushies exatamente no back - end ).


A decisão certa foi terrivelmente simples, embora não totalmente óbvia. Em vez do JSON padrão enviado pelo servidor no APNS


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

precisa enviar JSON do formulário


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

em que a loc-key é usada para passar a chave de cadeia localizada do arquivo Localizable.strings . Consequentemente, a mensagem push é exibida de acordo com a localização atual do dispositivo.


O mecanismo para interpolar seqüências de caracteres localizadas em notificações por push funciona de maneira semelhante:


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

A chave loc-args transmite uma matriz de argumentos que devem ser incorporados no texto da notificação localizada.


Para resumir ...


E então, o que temos no final:


  • padrão para armazenar dados de strings em arquivos .string especializados com sintaxe simples e acessível;
  • a capacidade de localizar a interface sem manipulações adicionais no código;
  • acesso rápido a recursos localizados a partir do código;
  • geração automática de arquivos de localização e estruturação dos recursos do diretório do projeto (aplicativo) usando as ferramentas do Xcode;
  • a capacidade de localizar o texto da notificação.

, Xcode , .


.

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


All Articles