Antes de tudo, importante dizer que este artigo foi criado a quatro mãos, ou duas cabeças (ocas… rsrsrs). Já que não dá para atribuir o texto para duas pessoas lá embaixo, queria deixar anotada a participação do Thiago Holanda (ou Trollanda, como é conhecido por alguns).
Este artigo foi oriundo de um momento comum que temos aqui na Concrete. Após criar uma feature interessante, corremos na mesa de amigos, mostramos e discutimos melhorias ou alterações. Vou tentar transcrever como foi a conversa sobre essa feature em especial.
Um belo dia, às 9:40, Holanda chegou à CS e… ok, ok, sem tanto detalhe. Estávamos mostrando alguns conceitos que vimos, e então o Holanda mostrou uma forma de trabalharmos com Localizable, que basicamente é a utilização de diversos idiomas no app. Trata-se de utilizar enums para abstrair a utilização (e repetição) de NSLocalizedString e organizar a estrutura da chamada das strings necessárias. A estrutura é bem simples, são apenas enums com chaves para cada string no documento (ex: pt-BR.lproj/Localizable.strings, en.proj/Localizable.strings ou qualquer outro idioma que se queira dar suporte).
Esses documentos utilizam o seguinte padrão:
// Common - Portuguese "common.change" = "Mudar"; "common.error" = "Erro"; "common.delete" = "Excluir"; "common.wait" = "Aguarde"; // Common - English "common.change" = "Change"; "common.error" = "Error"; "common.delete" = "Delete"; "common.wait" = "Wait";
A partir da localização configurada no iPhone, ele utilizará um desses idiomas (veja aqui mais informações sobre internacionalização de app).
Mas vocês falaram sobre usar enums, como é isso?
A ideia nasceu no Android e foi estendida a alguns frameworks em Swift, como SwiftGen, R.swift e outros. Aplicamos o mesmo conceito, como no exemplo abaixo:
enum Project { // Localizable Strings enum Localizable { // Common enum Common: String { case change = "common.change" case error = "common.error" case delete = "common.delete" case wait = "common.wait" } } }
Está ficando mais claro. Mas como utilizamos?
É realmente bem simples usar essa estrutura no seu projeto. Em primeiro lugar, após configurar todos os valores nos enums, você ganha o poder do code completion, o que impede que você se engane na hora de usar a string desejada.
Digamos que você vá configurar o valor em algum UILabel:
self.youLabel.text = NSLocalizedString(Project.Localizable.Common.wait.rawValue, comment: nil)
Essa estrutura é bem legal! Por que não usá-la para estruturar nossas constantes ou até mesmo para nossas strings de imagens? Boa ideia, vamos lá:
// MARK: - Project Constants enum Project { // Image Names enum Images { enum User: String { case first = "icon-first-image" case second = "icon-second-image" } } // Localizable Strings enum Localizable { // Common enum Common: String { case change = "common.change" case error = "common.error" case delete = "common.delete" case wait = "common.wait" } } }
Muito bom! Agora, para usar essa string, basta:
self.photo.image = UIImage(named:Project.Images.User.first.rawValue)
É, assim está meio estranho. Será que podemos melhorar isso? Claro! Vamos lá.
Inicializar o NSLocalizedString, assim como UIImage, toda vez pode ser cansativo (e feio). Uma extensão pode ser útil para colocarmos isso em propriedades. Vamos lá:
extension RawRepresentable where RawValue == String { var image: UIImage? { return UIImage(named: rawValue) } var localized: String { return NSLocalizedString(rawValue, comment: "") } }
Swift nos dá uma gama de protocolos que são implementados por alguns tipos. Por exemplo, o enum implementa o RawRepresentable, que nos permite usar a propriedade rawValue, que é o valor correspondente ao raw, aquele que atribuímos em cada case (exemplo: “common.change”). Além disso, garantimos que seu tipo é String usando where RawValue == String. Com isso, apenas enums que têm seu rawValue string poderão utilizar as variáveis image e localized.
Hmmmm… interessante! E a chamada?
self.photo.image = Project.Images.User.first.image self.youLabel.text = Project.Localizable.Common.wait.localize
Opa, tá bem melhor! Mas, olhando bem, a chamada abaixo pode dar problema:
Project.Localizable.Common.wait.image
Sim, não existe essa imagem, a string se refere a um Localizable. Droga! O que podemos fazer?
Programação Orientada a Protocolo FTW (nota do Erick: sou meio dislexo escrevendo, olhei 10 vezes para ter certeza de que não escrevi WTF… rsrsrsrs)!
Uma das grandes features do Swift 2.0 é permitir extensões de protocolo. Com isso, conseguimos atribuir um comportamento a nossos enums separadamente. Como fazer isso? Vamos começar com os protocolos:
// MARK: - Representable Protocols protocol ImageRepresentable: RawRepresentable { var image: UIImage? { get } } protocol LocalizeRepresentable: RawRepresentable { var localized: String { get } }
Adicionamos responsabilidades específicas para cada protocolo, eles implementam RawRepresentable e possuem propriedades que definem sua responsabilidade. Assim, podemos criar extensions desses protocolos e implementar nossas propriedades como no exemplo a seguir:
// MARK: - Representable Protocols Extensions extension ImageRepresentable where RawValue == String { var image: UIImage? { return UIImage(named: rawValue) } } extension LocalizeRepresentable where RawValue == String { var localized: String { return NSLocalizedString(rawValue, comment: "") } }
Nota 1: a documentação da Apple nos diz que caso uma imagem não possa ser encontrada quando informamos o “named” dela, o retorno será nil. Dessa forma, é necessário configurar a nossa propriedade como Optional (UIImage?) e testar com um If let na hora de chamar a propriedade image.
Nota 2: normalmente, vemos o uso do NSLocalizedString(_:comment? com as frases prontas previamente, como NSLocalizedString(“Olá Mundo”, comment: nil). Dessa forma, caso essa frase não seja encontrada nos arquivos Localizable.string, essa mesma frase é retornada. No caso deste artigo, incentivamos o uso dessa estrutura de “path” (“common.change”). Se por algum motivo você seguir a sugestão do artigo e usar essa forma de identificação das frases, não se esqueça de adicionar essa chave com a frase traduzida em todos os arquivos Localizable.string. Se isso não for feito, existe o risco de você encontrar em algum label, button ou qualquer outro elemento um texto parecido com: common.change.
Implementamos nossas variáveis nas extensões, agora basta aplicar nossos protocolos aos seus devidos enums:
// MARK: - Project Constants enum Project { // Image Names enum Images { enum User: String, ImageRepresentable { case first = "icon-first-image" case second = "icon-second-image" } } // Localizable Strings enum Localizable { // Common enum Common: String, LocalizeRepresentable { case change = "common.change" case error = "common.error" case delete = "common.delete" case wait = "common.wait" } } }
E voilà!
Adicionamos comportamentos distintos para cada enum. User possui a propriedade image e não possui localized, e Common possui localized mas não image.
Você ainda pode perguntar: “mas por que diabos isso serve pra mim ?”
Então… Você pode minimizar e/ou dificultar que erros básicos possam ocorrer, como digitar errado o nome de uma imagem ou localizável ou mudar a string apenas em um lugar e não por todo o código, o que pode prevenir dores de cabeça desnecessárias.
Bem, é isso! Temos certeza de que ainda podemos melhorar de outras formas, até porque nunca está legal o suficiente. E por que não ajudar a gente a melhorar? Deixe comentários e dicas, vamos fazer algo mais legal! =)
Veja o exemplo completo aqui.
***
Artigo publicado originalmente em http://www.concretesolutions.com.br/2016/09/26/localizable-e-constantes-em-swift/.