SwiftGen icon indicating copy to clipboard operation
SwiftGen copied to clipboard

SwiftUI use of LocalizedStringKey

Open krummler opened this issue 5 years ago • 19 comments
trafficstars

Hi there!

First off, thanks for the amazing tools you guys have created!

I'm trying to adapt SwiftGen to play nice with SwiftUI, but localisation seems to be a bit more work. I'll try to explain the situation.

SwiftUI utilises new UI components, such as Text, which takes as parameter a String or a LocalizedStringKey.

Example:

"welcome.title" = "Hey and welcome to the show!";

Can be called like:

/// Implicitly
Text("welcome.title")

/// Explicitly
Text(LocalizedStringKey("welcome.title"))

This allows the framework to automatically look up the localised string in the right language, combining this with the environment variables, allows for live debugging/testing languages. A neat example can be found in this blog post.

Like shown in the blog post, one could use a custom template to return the key wrapped in a LocalizedStringKey. But this doesn't work for parameterised translations, the LocalizedStringKey has no way to explicitly add parameters to the formatting.

To pass in parameterised strings, the key has to also contain them, example:

# Note: replacing _%lld_ with _%ld_ or _%i_ doesn't work in SwiftUI:
"nameAge %@ %lld" = "Hi there! My name is %1$@ and I'm %2$lld.";

To use this, the key has to be the interpolated string:

let name = "Dave"
let age = 42
Text("nameAge \(name) \(age)")

Swift figures out it's an interpolated string and uses that information to retrieve the right localisation.

TL;DR: currently this gets me stuck at two points:

  • %lld isn't stripped from the function name, the function name becomes nameAgeLld instead of nameAge.
  • I can't find a way to create an interpolated string in the template that SwiftUI understands.

Ideally, the generated file would return something like:

internal static func nameAge(_ p1: String, _ p2: Int) -> LocalizedStringKey {
  return LocalizedStringKey("nameAge \(p1) \(p2)")
}

Thanks so much for your help!

krummler avatar Mar 26 '20 10:03 krummler

internal static func nameAge(_ p1: String, _ p2: Int) -> LocalizedStringKey {
  return LocalizedStringKey("nameAge \(p1) \(p2)")
}

dirk-bester avatar Apr 26 '20 01:04 dirk-bester

That syntax is so bizarre 🤔

Your proposed solution seems allright. Is there a way to check for SwiftUI? How do we make this work for non-SwiftUI users and SwiftUI users? Or will we need separate templates?

djbe avatar May 17 '20 21:05 djbe

I've come up with this adaptation of swift 4 structured template:

...
import Foundation
import SwiftUI
...
{% macro recursiveBlock table item %}
  {% for string in item.strings %}
  {% if not param.noComments %}
  /// {{string.translation}}
  {% endif %}
  {% if string.types %}
  {{accessModifier}} static func {{string.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}({% call parametersBlock string.types %}) -> Text {
    Text({{enumName}}.tr("{{table}}", "{{string.key}}", {% call argumentsBlock string.types %}))
  }
  {% else %}
  {{accessModifier}} static let {{string.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = Text("{{string.key}}", tableName: "{{table}}", bundle: Bundle(for: BundleToken.self))
  {% endif %}
  {% endfor %}
  {% for child in item.children %}

  {{accessModifier}} enum {{child.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
    {% filter indent:2 %}{% call recursiveBlock table child %}{% endfilter %}
  }
  {% endfor %}
{% endmacro %}

result:

"featuredPage.title" = "Featured";
"featuredPage.seeAll" = "See All";
"featuredPage.count" = "Featured %@ items";
"featuredPage.countMultiple" = "Featured %s of %s elements";

converts to:

internal enum L10n {

  internal enum FeaturedPage {
    /// Featured %@ items
    internal static func count(_ p1: String) -> Text {
      Text(L10n.tr("Localizable", "featuredPage.count", p1))
    }
    /// Featured %s of %s elements
    internal static func countMultiple(_ p1: UnsafePointer<CChar>, _ p2: UnsafePointer<CChar>) -> Text {
      Text(L10n.tr("Localizable", "featuredPage.countMultiple", p1, p2))
    }
    /// See All
    internal static let seeAll = Text("featuredPage.seeAll", tableName: "Localizable", bundle: Bundle(for: BundleToken.self))
    /// Featured
    internal static let title = Text("featuredPage.title", tableName: "Localizable", bundle: Bundle(for: BundleToken.self))
  }
}

Usage:

VStack {
    L10n.FeaturedPage.count("10")
        .fontWeight(.heavy)
        .padding()
    Button(action: {
        // your action here
    }) {
        L10n.FeaturedPage.seeAll
    }
}

kacper1703 avatar May 18 '20 12:05 kacper1703

Why return Text instead of LocalizedStringKey though? Prevents you from using Button(L10n.FeaturedPage.seeAll) (or something like that).

But my initial question remains: is it possible to tackle this in a way that's compatible for SwiftUI and non-SwiftUI users at the same time?

Another thing is, is it still a requirement to have %@ in the string key as @krummler initially noted? @kacper1703's example doesn't have this.

djbe avatar May 18 '20 13:05 djbe

Yeah the syntax really feels like magic to me, not sure what's happening under the hood there.

As far as I can tell, using the method described by @kacper1703 would mean the SwiftUI Environment object is being ignored. This would mean you cannot preview multiple locales like in the GIF from this blog post. In that case you might as well use the default template provided :).

Regarding detecting SwiftUI, I'm not sure if that is possible (yet). Target could be both iOS or macOS.

Personally I wouldn't mind an extra template, at least to get started. As long as there's a way to gather all the required variables?

krummler avatar May 18 '20 13:05 krummler

Separate template it is then, or maybe a template variable for existing templates, that may work as well.

Is the key format (in the strings) file needed? If so, we may need to add some parser options as well...

djbe avatar May 18 '20 14:05 djbe

Why return Text instead of LocalizedStringKey though? Prevents you from using Button(L10n.FeaturedPage.seeAll) (or something like that).

LocalizedStringKey does not take a table name (.strings file) as a parameter, but Text does. You're right about the shorthand syntax though.

As far as I can tell, using the method described by @kacper1703 would mean the SwiftUI Environment object is being ignored. This would mean you cannot preview multiple locales like in the GIF from this blog post.

It works for strings without parameters (e.g. featuredPage.seeAll from my example, but not featuredPage.count.

I must admit I'm quite new to SwiftUI so maybe there is a way to write an extension to Button to receive Text.

kacper1703 avatar May 19 '20 08:05 kacper1703

Is the key format (in the strings) file needed? If so, we may need to add some parser options as well...

I think that's what it uses to determine what key to find. So I'm afraid so.

LocalizedStringKey does not take a table name (.strings file) as a parameter, but Text does. You're right about the shorthand syntax though.

Yikes, good point...

I must admit I'm quite new to SwiftUI so maybe there is a way to write an extension to Button to receive Text.

On the plus side, in a Button you can also use Text to dress up the contents, using Text shouldn't cause a direct problem for SwiftUI components, I think.

But it still feels weird to have to directly use Text, seems really close to the UI layer. What if you want to send localized text to the console or a backend (anything not directly related to SwiftUI).

It works for strings without parameters (e.g. featuredPage.seeAll from my example, but not featuredPage.count.

I think for both using LocalizedStringKey or Text this can be solved the same way; where your example now reads:

Text(L10n.tr("Localizable", "featuredPage.count", p1))

It could be something along the lines of:

Text("featuredPage.count \(p1)", tableName: "Localizable")

I just pushed a little sample project here: https://github.com/kevinrmblr/SwiftUILocalizationTest. It includes some different (partially) working options, and a button :).

kevinrmblr avatar May 19 '20 09:05 kevinrmblr

I just pushed a little sample project here: https://github.com/kevinrmblr/SwiftUILocalizationTest. It includes some different (partially) working options, and a button :).

Your solution still requires to include placeholder(s) in the key. This would be a bummer for multi-platform projects that use online translation tools like PhraseApp or POEditor and share keys between platforms. Also, Android's XML files cannot include anything else than alphabet and numbers in the key name AFAIR.

kacper1703 avatar May 19 '20 09:05 kacper1703

Your solution still requires to include placeholder(s) in the key.

I know, but otherwise you break proper support for parameterised strings. The environment object and dynamically populating content are two very powerful concepts of SwiftUI, which are currently not fully available.

(Edit: I agree the syntax is weird, and I'm not sure why it seems to be so hard to support localized interpolated strings).

This would be a bummer for multi-platform projects that use online translation tools like PhraseApp or POEditor and share keys between platforms. Also, Android's XML files cannot include anything else than alphabet and numbers in the key name AFAIR.

I reckon you could fix this by parsing the generated keys and suffixing them with the parameters from the value, though that does sound messy.

Of course, you don't have to use placeholders in the key, but right now it's not even an option. :) There's just no way to use a LocalizedStringKey with an interpolated string the way SwiftUI would like you to.

kevinrmblr avatar May 19 '20 09:05 kevinrmblr

Why return Text instead of LocalizedStringKey though? Prevents you from using Button(L10n.FeaturedPage.seeAll) (or something like that).

Agreed. You can pass back LocalizedStringKey and it is an implementation detail if you use that directly vs wrapping it in a Text creating function. The only issue is does it yield something that works in Preview? That is the quest that landed me in this thread.

But my initial question remains: is it possible to tackle this in a way that's compatible for SwiftUI and non-SwiftUI users at the same time?

In theory maybe? You are still just allowing parameters to show up in strings for either user. THe SwiftUI needs seem more complex but abstracted out properly it would allow reuse by non SwiftUI users to handle more situations?

Another thing is, is it still a requirement to have %@ in the string key as @krummler initially noted? @kacper1703's example doesn't have this.

I was unable to make the all %@ code work, but I also no longer see it in this thread. For instance numbers and a string are as follows: "someKey %lld %lld %@" = "%1$lld ipsum %2$lld lorem %3$@ delenda est"; It seems they need to be typed in the key string. Then usage is: return LocalizedStringKey("someKey \(someInt) \(anotherInt) \(aString)") %@ is the String type. If you convert to just strings in the input key it will break number handling such as "1st", "2nd", "1 chocolate bar", "2 chocolate bars", etc.

Note that type conversion is always explicit in Swift. var x: UInt64 = UInt64(someUint32orUInt16etc) It is unlikely that spamming %@ String parameters everywhere yields good results.

dirk-bester avatar May 23 '20 17:05 dirk-bester

Your solution still requires to include placeholder(s) in the key. This would be a bummer for multi-platform projects that use online translation tools like PhraseApp or POEditor and share keys between platforms.

That LocalizedStringKey isn't backwards-compatible with String.localizedStringWithFormat(_:_:) is a bummer, for the reasons you mentioned. I wonder if it would be a sensible approach to generate LocalizableStringKeys for localizable strings which either do not make use of placeholder parameters at all, or have the placeholders present in their keys, and generate good old Strings (via NSLocalizedString) for those localizable strings which make use of placeholder parameters only in their values (but not in their keys).

If we have localizable strings like these:

// Localizable.strings
"HELLO_WORLD" = "Hello, world!";
"HELLO_WORLD_CROSSPLATFORM" = "Hello, %@!";
"HELLO_WORLD_%@" = "Hello, %@!";

It's possible to use them like this:

import SwiftUI

enum L10n {
        /// Resolves to "Hello, world!".
        ///
        /// This is an example of a "simple" (unparameterized) localized string.
        static let helloWorld = LocalizedStringKey("HELLO_WORLD")

        /// Resolves to "Hello, (name)!".
        ///
        /// This is using string interpolation (`ExpressibleByStringInterpolation`) to look up the key and replace the placeholder value.
        /// This only works if the key (not just the value) of the localizable string contains the placeholder.
        static func hello(_ name: String) -> LocalizedStringKey {
            return LocalizedStringKey("HELLO_WORLD_\(name)")
        }

        /// Resolves to "Hello, (name)!", but as a `String`.
        ///
        /// This is a workaround for cross-platform-enabled localizable strings for which the keys themselves are not parameterized.
        /// We can't use those keys to create correct `LocalizedStringKey`s, but we can still make use of them by using e.g. `Text(verbatim:)` and inserting the string verbatim.
        /// (I'm not advocating that these should end with `__asString`, I just added that suffix to make it easier to tell apart from the other `hello(_:)` function for this example.)
        static func hello__asString(_ name: String) -> String {
            return String.localizedStringWithFormat(NSLocalizedString("HELLO_WORLD_CROSSPLATFORM", comment: ""), name)
        }
    }
}

struct ExampleView: View {
    var body: some View {
        VStack {
            Text(L10n.helloWorld) // Uses LocalizedStringKey to show "Hello, world!"

            Text(L10n.hello("Alice")) // Uses LocalizedStringKey to show "Hello, Alice!"

            Text(verbatim: L10n.Example.hello__asString("Bob")) // Uses NSLocalizedString to show "Hello, Bob!"
            Text(L10n.hello__asString("Bob")) // This is equivalent to the version using `verbatim` above.
        }
    }
}

msewell avatar Jun 03 '20 10:06 msewell

@dirk-bester Did you check out the example repo I mentioned above? This should give some working examples of what works and what doesn't (yet?).

@msewell Come to think of it, perhaps a second SwiftGen template could fix this for us. Instead of generating a L10n.swift file, it can output an 'enhanced' Localizable.strings file?

The output from a tool like POEdit would be probably something like:

"HELLO_NAME" = "Hello, %@!";

The new SwiftGen template converts this to a new Localizable.strings file:

"HELLO_NAME_%@" = "Hello, %@!";

Seems pretty meta, but also flexible. According to the SwiftGenKit docs, not all required information needed is currently available though, such as the parameter placeholders (%@, @ld, etc).

kevinrmblr avatar Jun 04 '20 11:06 kevinrmblr

Can't say that I'm a fan of changing the .strings file itself, although I'm sure that solution would work for some people/projects. For my use case the .strings file is downloaded from a localization tool (Phrase) and I'd like it to match the "ground truth" there to avoid possible conflicts and confusion that might arise from a diff between the local file and the remote file.

msewell avatar Jun 04 '20 15:06 msewell

@kacper1703 do you have a template available that I can use on SwiftUI project?

netbe avatar Dec 17 '20 14:12 netbe

I'm currently developing my first pure SwiftUI app and I figured out a custom template that works well for me:

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

Please note that part of the template is a LocalizedString wrapper type which allows for both accessing a LocalizedStringKey (via .key) and also a plain pre-localized String (via .text), depending on what is needed:

public struct LocalizedString {
  internal let lookupKey: String

  var key: LocalizedStringKey {
    LocalizedStringKey(lookupKey)
  }

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

On usage side code looks something like this:

var body: some View {
  VStack {
    Text(L10n.Login.PageTitle.header.key)
    CustomView(string: L10n.Login.Step1.title.text)
  }
}

Please note that if you're using SwiftUI previews then only where .key is used the preview will localize texts in the preview. But I felt .text is needed as some APIs require a String and don't work with a LocalizedStringKey.

I hope this helps.

Jeehut avatar Feb 21 '21 17:02 Jeehut

Thanks for the solution shared by @Jeehut . I created my ownself version base on that, and it works well so far:

swiftui-strings-template.stencil

// 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 tr("{{table}}", "{{string.key}}", {% call argumentsBlock string.types %})
  }
  {% else %}
  public static let {{string.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = LocalizedString(table: "{{table}}", 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 %}
{% set enumName %}{{param.enumName|default:"L10n"}}{% endset %}
public enum {{enumName}} {
  {% 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
{% if not param.withoutSupporter %}
fileprivate func tr(_ table: String, _ key: String, _ locale: Locale = Locale.current, _ args: CVarArg...) -> String {
  let path = Bundle.main.path(forResource: locale.identifier, ofType: "lproj") ?? ""
  let format: String
  if let bundle = Bundle(path: path) {
    format = NSLocalizedString(key, tableName: table, bundle: bundle, comment: "")
  } else {
    format = BundleToken.bundle.localizedString(forKey: key, value: nil, table: table)
  }
  return String(format: format, locale: locale, arguments: args)
}

public struct LocalizedString: Hashable {
  let table: String
  fileprivate let lookupKey: String

  init(table: String, lookupKey: String) {
    self.table = table
    self.lookupKey = lookupKey
  }

  var key: LocalizedStringKey {
    LocalizedStringKey(lookupKey)
  }

  var text: String {
    tr(table, lookupKey)
  }

  func text(withLocale locale: Locale) -> String {
    tr(table, lookupKey, locale)
  }
}
{% endif %}
{% 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 %}

swiftgen.yml

input_dir: ${TARGET_NAME}/Resources/
output_dir: ${TARGET_NAME}/Generated/
strings:
    - inputs: en.lproj/Localizable.strings
      outputs:
        - templatePath: swiftui-strings-template.stencil
          output: strings.swift
    - inputs: en.lproj/
      filter: ^((?!InfoPlist|Localizable).)+\.strings$
      outputs:
        - templatePath: swiftui-strings-template.stencil
          output: stringsExtra.swift
          params:
            enumName: L10nExtra
            withoutSupporter:
xcassets:
  inputs: .
  filter: .+\.xcassets$
  outputs:
    - templatePath: swiftui-assets-template.stencil
      output: assets.swift

Here is how you can use it:

// Any string defined at Localizable.strings will be compiled into L10n
L10n.textName      // return type as LocalizedString which conform Hashable
L10n.textName.key  // return type as LocalizedStringKey
L10n.textName.text // return type as String
L10n.textName.text(withLocale: locale) // return type as String with particular locale's version

// And the others defined at .strings file not named as Localizable.strings will be compiled into L10nExtra
L10nExtra.fileName.textName
L10nExtra.fileName.textName.key
L10nExtra.fileName.textName.text
L10nExtra.fileName.textName.text(withLocale: locale)

The repo of config files has been create at https://github.com/wavky/SwiftGenConfigForSwiftUI but only described in Chinese. Anyone who can translate it into English would be helpful :)

wavky avatar Jul 21 '21 08:07 wavky

The issue with LocalizedStringKey is that many of the SwiftUI APIs that accept it don't also allow for specifying the bundle or tableName, for example TextField. This means workflows that involve a Swift Package with SwiftUI and localized strings don't work properly because those SwiftUI APIs accepting LocalizedStringKey only look within the main bundle.

You can, however, specify the tableName and bundle parameters using Text: https://developer.apple.com/documentation/swiftui/text/init(_:tablename:bundle:comment:)

It looks like iOS 15 added a number of new APIs allowing for usage of Text: https://developer.apple.com/documentation/swiftui/textfield/init(_:text:prompt:)-70zi2

If using one of the above templates when your strings are outside of Bundle.main, you might want to not use key directly and instead expose a Text property and fall back to stringValue where needed:

internal struct LocalizedString: Hashable {
  let table: String
  fileprivate let lookupKey: String

  init(table: String, lookupKey: String) {
    self.table = table
    self.lookupKey = lookupKey
  }

  private var key: LocalizedStringKey {
    LocalizedStringKey(lookupKey)
  }

  var text: Text {
    Text(key, tableName: table, bundle: {{param.bundle|default:"BundleToken.bundle"}})
  }

  var stringValue: String {
    tr(table, lookupKey)
  }

  func stringValue(withLocale locale: Locale) -> String {
    tr(table, lookupKey, locale)
  }
}

chrisballinger avatar Aug 26 '21 21:08 chrisballinger

Found out that this problem isn't related to SwiftUI only but also to Foundation: https://fatbobman.medium.com/attributedstring-making-text-more-beautiful-than-ever-98deb093f617

// Localizable Chinese
"world %@ %@" = "%@ 世界 %@";
// Localizable English
"world %@ %@" = "world %@ %@";

var world = AttributedString(localized: "world \("👍") \("🥩")",options: .applyReplacementIndexAttribute) // When creating an attributed string, the index will be set in the order of interpolation, 👍 index == 1 🥩 index == 2
for (index,range) in world.runs[\.replacementIndex] {
    switch index {
        case 1:
            world[range].baselineOffset = 20
            world[range].font = .title
        case 2:
            world[range].backgroundColor = .blue
        default:
            world[range].inlinePresentationIntent = .strikethrough
    }
}

So I just have an attributed string which should be localized, no SwiftUI involved. Currently I must choose should I use SwiftGen and have troubles with attributed strings or should I avoid SwiftGen in this case and use a method provided by Apple

gerchicov-vg avatar Mar 06 '24 11:03 gerchicov-vg