Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automate addition of LocalizedStringKeys to source locale #194

Open
Jeehut opened this issue Jul 19, 2020 · 7 comments
Open

Automate addition of LocalizedStringKeys to source locale #194

Jeehut opened this issue Jul 19, 2020 · 7 comments
Labels
BartyCrouch5 Out of scope for BartyCrouch 4.x as it (probably) needs bigger changes. enhancement Priority: Medium

Comments

@Jeehut
Copy link
Member

Jeehut commented Jul 19, 2020

As described in my article Safer Localization in SwiftUI, BartyCrouch can already be used for normalizing and linting localiation on SwiftUI-only applications. But the transformation from NSLocalizedString or BartyCrouch.translate doesn't support the new LocalizedStringKey type yet.

We should consider adding an option to transform BartyCrouch.translate into a literal String and adding the key to the source Localizable.strings file automatically while doing that like before. Alternatively, we could also provide the SafeLocalizedStringKey file from the article and convert into that instead. That way, whenever the key would be removed, there would be an error shown during development, providing even more safety.

A more general approach could also include rewriting the entire BartyCrouch transformation logic for more flexibility, configurability, performance and therewhile drop the dependency extractLocStrings, at least for SwiftUI-only applications.

@Jeehut
Copy link
Member Author

Jeehut commented Feb 21, 2021

Just in case this helps someone out there until we have proper support for SwiftUI in SwiftGen and BartyCrouch:

In my pure SwiftUI projects, I'm currently using SwiftGen with a custom Strings.stencil template:

// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen

{% if tables.count > 0 %}
import SwiftUI

// MARK: - Strings

{% macro parametersBlock types %}{% filter removeNewlines:"leading" %}
  {% for type in types %}
    {% if type == "String" %}
    _ p{{forloop.counter}}: Any
    {% else %}
    _ p{{forloop.counter}}: {{type}}
    {% endif %}
    {{ ", " if not forloop.last }}
  {% endfor %}
{% endfilter %}{% endmacro %}
{% macro argumentsBlock types %}{% filter removeNewlines:"leading" %}
  {% for type in types %}
    {% if type == "String" %}
    String(describing: p{{forloop.counter}})
    {% elif type == "UnsafeRawPointer" %}
    Int(bitPattern: p{{forloop.counter}})
    {% else %}
    p{{forloop.counter}}
    {% endif %}
    {{ ", " if not forloop.last }}
  {% endfor %}
{% endfilter %}{% endmacro %}
{% macro recursiveBlock table item %}
  {% for string in item.strings %}
  {% if not param.noComments %}
  /// {{string.translation}}
  {% endif %}
  {% if string.types %}
  public static func {{string.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}({% call parametersBlock string.types %}) -> String {
    return L10n.tr("{{table}}", "{{string.key}}", {% call argumentsBlock string.types %})
  }
  {% else %}
  public static let {{string.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = LocalizedString(lookupKey: "{{string.key}}")
  {% endif %}
  {% endfor %}
  {% for child in item.children %}
  public enum {{child.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
    {% filter indent:2 %}{% call recursiveBlock table child %}{% endfilter %}
  }
  {% endfor %}
{% endmacro %}
public enum L10n {
  {% if tables.count > 1 or param.forceFileNameEnum %}
  {% for table in tables %}
  public enum {{table.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
    {% filter indent:2 %}{% call recursiveBlock table.name table.levels %}{% endfilter %}
  }
  {% endfor %}
  {% else %}
  {% call recursiveBlock tables.first.name tables.first.levels %}
  {% endif %}
}

// MARK: - Implementation Details

extension L10n {
  fileprivate static func tr(_ table: String, _ key: String, _ args: CVarArg...) -> String {
    let format = {{param.bundle|default:"BundleToken.bundle"}}.localizedString(forKey: key, value: nil, table: table)
    return String(format: format, locale: Locale.current, arguments: args)
  }
}

public struct LocalizedString {
  internal let lookupKey: String

  var key: LocalizedStringKey {
    LocalizedStringKey(lookupKey)
  }

  var text: String {
    L10n.tr("Localizable", lookupKey)
  }
}
{% if not param.bundle %}

private final class BundleToken {
  static let bundle: Bundle = {
    #if SWIFT_PACKAGE
      return Bundle.module
    #else
      return Bundle(for: BundleToken.self)
    #endif
  }()
}
{% endif %}
{% else %}
// No string found
{% endif %}

I also altered the BartyCrouch.swift file I include to provide both access to a LocalizedStringKey and a plain String:

//  This file is required in order for the `transform` task of the translation helper tool BartyCrouch to work.
//  See here for more details: https://github.com/Flinesoft/BartyCrouch

import Foundation

enum BartyCrouch {
  enum SupportedLanguage: String {
    case english = "en"
    case french = "fr"
    case german = "de"
    case japanese = "ja"
    case turkish = "tr"
  }

  static func translate(key: String, translations: [SupportedLanguage: String], comment: String? = nil) -> LocalizedString {
    let typeName = String(describing: BartyCrouch.self)
    let methodName = #function

    print(
      "Warning: [BartyCrouch]",
      "Untransformed \(typeName).\(methodName) method call found with key '\(key)' and base translations '\(translations)'.",
      "Please ensure that BartyCrouch is installed and configured correctly."
    )

    // fall back in case something goes wrong with BartyCrouch transformation
    return LocalizedString(lookupKey: "BC: KEY NOT TRANSFORMED!")
  }
}

The LocalizedString is a type that's generated as part of my Strings.stencil file and looks like this:

public struct LocalizedString {
  internal let lookupKey: String

  var key: LocalizedStringKey {
    LocalizedStringKey(lookupKey)
  }

  var text: String {
    L10n.tr("Localizable", lookupKey)
  }
}

This works very well for me.

@Patrick-Kladek
Copy link

Thank you very much for this workaround.

Unfortunately I have a hard time understanding how to make all of this work.
This starts with the Strings.stencil template. How do I tell BartyCrouch to use it? Sadly the documentation doesn't mention this with one word. In the readme is a sample file but no explanation what todo with it. Do I need to add this to .bartycrouch.toml somehow?

The same question came up with BartyCrouch.swift. Do I need to add this to compiled sources in Xcode or is it just needed in the Repo so BartyCrouch knows what to do? If so again how do I tell BartyCrouch to use this file? Is this file automatically recognized via it's filename?

I've created the .bartycrouch.toml via bartycrouch init. What do I need to add to make this work?

Sorry for my dumb questions. I have no idea how all of this works and had no luck finding tutorials or anything on Stackoverflow so far.

@Jeehut
Copy link
Member Author

Jeehut commented May 16, 2021

@Patrick-Kladek You are right, the README of BartyCrouch doesn't include detailed step-by-step explanations for beginners, it's targeted towards somewhat experiences iOS developers who have already used similar tools, like SwiftGen. The most detailed tutorial is I think still my blog post which is also mentioned at the top of the README. Please read it, it should clarify how SwiftGen and BartyCrouch are interrelated (basically, they're not at all, BartyCrouch only supports generation of L10n if swiftgenStructured is chosen as the transformer as explained in point 3 of the Configuration section).

But let me also answer the more specific questions you asked:

This starts with the Strings.stencil template. How do I tell BartyCrouch to use it?

Not at all. If anything, you tell SwiftGen how to use it. Of course the documentation of BartyCrouch doesn't mention how to use SwiftGen. The custom stencil is a feature of SwiftGen and needs to be specified in the swiftgen.yml file, see the README of SwiftGen. You don't have to use SwiftGen to use BartyCrouch though, you could just stick to NSLocalizedString.

The same question came up with BartyCrouch.swift. Do I need to add this to compiled sources in Xcode or is it just needed in the Repo so BartyCrouch knows what to do?

The BartyCrouch.swift file should to be included in your respository as part of the compiled sources, otherwise your build will fail when you use BartyCrouch.translate (which is transformed to other calls in the build script if you set that up, but build scripts are part of the build process).

If so again how do I tell BartyCrouch to use this file? Is this file automatically recognized via it's filename?

Please see the options of the transform action, the options supportedLanguageEnumPath and typeName specify where the file is searched in and you can also change the name to something else if needed. Only the SupportedLanguage enum is required.
Bildschirmfoto 2021-05-16 um 21 22 12

I've created the .bartycrouch.toml via bartycrouch init. What do I need to add to make this work?

I think the blog post already answers this question (the README does so, too).

Sorry for my dumb questions. I have no idea how all of this works and had no luck finding tutorials or anything on Stackoverflow so far.

Those are not dumb questions. But I think if you've invested a little more time to read the README in more detail (especially if you also read the linked blog post), you could have found most of the answers. But I can understand that all the different existing options can be overwhelming if you're just getting started with BartyCrouch. I am working on a new version which is much easier to use, but it won't be released before 2022, I think.

I hope this answer helps you get on the right track (in case you're still interested, sorry for the late answer). Good luck!

@Sam-Spencer
Copy link

Bump! @Flinesoft any progress on this?

@Jeehut
Copy link
Member Author

Jeehut commented Aug 18, 2021

@Sam-Spencer Currently BartyCrouch is based on tools from Apple which don't support LocalizedStringKeys though. While I am planning to do a bigger rewrite of BartyCrouch sometime in the future with my own parser that could cover this, that is probably quite some time away.

In the mean time, please note that there's 2 other options based on the Pro Localization Workflow:

  1. If you're using SwiftGen, you can simply use these custom templates I've shared elsewhere and use the safer L10n structure instead of LocalizedStringKey
  2. If you're not using SwiftGen, what we could do is add a new transformer type called swiftui besides the already existing foundation and swiftgen types.

I won't have the time to do the change no. 2 in the comings months, maybe beginning next year. But it's just a matter of adding it here and adding a buildSwiftUIExpression function below this function and use it further above when the transformer is set to swiftui.

So if someone is willing to take some time to implement as described above, I would be willing to review & merge. :)

@FlineDevPublic FlineDevPublic added BartyCrouch5 Out of scope for BartyCrouch 4.x as it (probably) needs bigger changes. Priority: Medium labels Jan 14, 2022
@tmolitor-stud-tu
Copy link

For Monal IM I'm using a combination of Xcodes xliff string extraction on compilation, bash, a python script and bartycrouch to get SwiftUI strings added to my strings file.

Here is the commit Introducing this functionality, maybe this is useful to someone: monal-im/Monal@6fcdcb6

I had to temporarily turn off "SWIFT_EMIT_LOC_STRINGS" when extracting the xliff file because it could not compile the project. I don't know why this is necessary, but since in our CI our build script builds the app before our translation update script gets to run, this is fine (the SwiftUI string are already extracted at this point, even with SWIFT_EMIT_LOC_STRINGS turned off while extracting the xliff).

@Jeehut
Copy link
Member Author

Jeehut commented Nov 8, 2022

@tmolitor-stud-tu Thank you for sharing your solution and experiences.

Since my last comment on this thread, there's been a development that might be of interest to all SwiftUI users of BartyCrouch: I released a new Mac app called ReMafoX which basically is a successor to BartyCrouch with more features.

Besides other things, it has native support for LocalizedStringKey but its workflow is a little different. Instead of writing code and then running a build-script to transform the code (which breaks your edit history), it comes with an Xcode extension and a UI to localize Strings, so you can simply trigger a shortcut and get a LocalizedStringKey as a result. See this GIF:
Add-Translation---Edit-with-Audio-2

To learn more, see this short summary article:
https://www.fline.dev/introducing-remafox-easy-app-localization/

Give ReMafoX a try! Unlike BartyCrouch, which is more of a community project, I can provide proper support for it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
BartyCrouch5 Out of scope for BartyCrouch 4.x as it (probably) needs bigger changes. enhancement Priority: Medium
Development

No branches or pull requests

5 participants