liamnichols / xcstrings-tool

A plugin to generate Swift constants for your Strings Catalogs.
https://swiftpackageindex.com/liamnichols/xcstrings-tool/documentation/documentation
MIT License
173 stars 26 forks source link

Strings Catalogs migrated from .stringsdict don't work #26

Open Kaspik opened 9 months ago

Kaspik commented 9 months ago

Hey @liamnichols !

I downloaded your demo project DogTracker to test things out, and replaced the Localizable.xcstrings with our own (we have a lot of languages, and a lot of strings - the file itself has 12MB and ~600 000 lines).

When compiling the dog Tracker, error is shown: Decoding error at ‘strings → common.feed.translations_seen_by → localizations → ar → substitutions → number_of_parents‘ - The data couldn’t be read because it is missing.

As you can see on the screenshot, everything looks correct. I wonder if you are able to debug this issue. Note: It is using 0.1.1 version released 30 mins ago. :)

Screenshot 2023-11-28 at 13 35 36 Screenshot 2023-11-28 at 13 35 50
liamnichols commented 9 months ago

It sounds like your Strings Catalog is a good stress-test for the tool 😄

That is strange though, you are right, the screenshot looks fine... Would you be able to pull the JSON for common.feed.translations_seen_by out of the .xcstrings file and share it here?

We can then put it into the fixture tests to try and replicate the issue... So far, this is what the tests are checking:

https://github.com/liamnichols/xcstrings-tool/blob/0c77774313bb148f726b1d31b016a4e739c85f6e/Tests/XCStringsToolTests/__Fixtures__/Substitution.xcstrings#L1-L54

Kaspik commented 9 months ago
Sure, here it goes, paste it to your file and test it out :) ```json "common.feed.translations_seen_by" : { "extractionState" : "manual", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", "value" : "مشاهدة الترجمة من قبل %#@number_of_parents@" }, "substitutions" : { "number_of_parents" : { "formatSpecifier" : "ld", "variations" : { "plural" : { "few" : { "stringUnit" : { "state" : "translated", "value" : "%a أولياء أمور" } }, "many" : { "stringUnit" : { "state" : "translated", "value" : "%a ولي أمر" } }, "one" : { "stringUnit" : { "state" : "translated", "value" : "ولي أمر واحد (%a)" } }, "other" : { "stringUnit" : { "state" : "translated", "value" : "%a ولي أمر" } }, "two" : { "stringUnit" : { "state" : "translated", "value" : "وليا أمر (%a)" } }, "zero" : { "stringUnit" : { "state" : "translated", "value" : "%a ولي أمر" } } } } } } }, "ca" : { "stringUnit" : { "state" : "translated", "value" : "Traducció vista per %#@number_of_parents@" }, "substitutions" : { "number_of_parents" : { "formatSpecifier" : "ld", "variations" : { "plural" : { "one" : { "stringUnit" : { "state" : "translated", "value" : "%arg pare" } }, "other" : { "stringUnit" : { "state" : "translated", "value" : "%arg pares" } } } } } } }, "de" : { "stringUnit" : { "state" : "translated", "value" : "Übersetzung gesehen von %#@number_of_parents@" }, "substitutions" : { "number_of_parents" : { "formatSpecifier" : "ld", "variations" : { "plural" : { "one" : { "stringUnit" : { "state" : "translated", "value" : "%arg Elternteil" } }, "other" : { "stringUnit" : { "state" : "translated", "value" : "%arg Eltern" } } } } } } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Translation viewed by %#@number_of_parents@" }, "substitutions" : { "number_of_parents" : { "formatSpecifier" : "ld", "variations" : { "plural" : { "one" : { "stringUnit" : { "state" : "translated", "value" : "%arg parent" } }, "other" : { "stringUnit" : { "state" : "translated", "value" : "%arg parents" } } } } } } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Translation viewed by %#@number_of_parents@" }, "substitutions" : { "number_of_parents" : { "formatSpecifier" : "ld", "variations" : { "plural" : { "one" : { "stringUnit" : { "state" : "translated", "value" : "%arg parent" } }, "other" : { "stringUnit" : { "state" : "translated", "value" : "%arg parents" } } } } } } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Traducción vista por %#@number_of_parents@" }, "substitutions" : { "number_of_parents" : { "formatSpecifier" : "ld", "variations" : { "plural" : { "one" : { "stringUnit" : { "state" : "translated", "value" : "%arg padre" } }, "other" : { "stringUnit" : { "state" : "translated", "value" : "%arg padres" } } } } } } }, "es-ES" : { "stringUnit" : { "state" : "translated", "value" : "Traducción vista por %#@number_of_parents@" }, "substitutions" : { "number_of_parents" : { "formatSpecifier" : "ld", "variations" : { "plural" : { "one" : { "stringUnit" : { "state" : "translated", "value" : "%arg padre" } }, "other" : { "stringUnit" : { "state" : "translated", "value" : "%arg padres" } } } } } } }, "fil" : { "stringUnit" : { "state" : "translated", "value" : "Ang pagsasalin ay tiningnan ng %#@number_of_parents@" }, "substitutions" : { "number_of_parents" : { "formatSpecifier" : "ld", "variations" : { "plural" : { "one" : { "stringUnit" : { "state" : "translated", "value" : "%arg magulang" } }, "other" : { "stringUnit" : { "state" : "translated", "value" : "%arg magulang" } } } } } } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Traduction vue par %#@number_of_parents@" }, "substitutions" : { "number_of_parents" : { "formatSpecifier" : "ld", "variations" : { "plural" : { "one" : { "stringUnit" : { "state" : "translated", "value" : "%arg parent" } }, "other" : { "stringUnit" : { "state" : "translated", "value" : "%arg parents" } } } } } } }, "fr-CA" : { "stringUnit" : { "state" : "translated", "value" : "Traduction vue par %#@number_of_parents@" }, "substitutions" : { "number_of_parents" : { "formatSpecifier" : "ld", "variations" : { "plural" : { "one" : { "stringUnit" : { "state" : "translated", "value" : "%arg parent" } }, "other" : { "stringUnit" : { "state" : "translated", "value" : "%arg parents" } } } } } } }, "hi" : { "stringUnit" : { "state" : "translated", "value" : "अनुवाद को %#@number_of_parents@ अभिभावकों द्वारा देखा गया" }, "substitutions" : { "number_of_parents" : { "formatSpecifier" : "ld", "variations" : { "plural" : { "one" : { "stringUnit" : { "state" : "translated", "value" : "%arg अभिभावक" } }, "other" : { "stringUnit" : { "state" : "translated", "value" : "%arg अभिभावक" } } } } } } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Terjemahan dilihat oleh %#@number_of_parents@" }, "substitutions" : { "number_of_parents" : { "formatSpecifier" : "ld", "variations" : { "plural" : { "other" : { "stringUnit" : { "state" : "translated", "value" : "%arg orang tua" } } } } } } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Traduzione visualizzato da %#@number_of_parents@" }, "substitutions" : { "number_of_parents" : { "formatSpecifier" : "ld", "variations" : { "plural" : { "one" : { "stringUnit" : { "state" : "translated", "value" : "%arg genitore" } }, "other" : { "stringUnit" : { "state" : "translated", "value" : "%arg genitori" } } } } } } }, "ja-JP" : { "stringUnit" : { "state" : "translated", "value" : "%#@number_of_parents@ 人が翻訳を閲覧" }, "substitutions" : { "number_of_parents" : { "formatSpecifier" : "ld", "variations" : { "plural" : { "other" : { "stringUnit" : { "state" : "translated", "value" : "%arg人の保護者" } } } } } } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "학부모 %#@number_of_parents@ 명 번역 읽음" }, "substitutions" : { "number_of_parents" : { "formatSpecifier" : "ld", "variations" : { "plural" : { "other" : { "stringUnit" : { "state" : "translated", "value" : "학부모 %arg명" } } } } } } }, "ms" : { "stringUnit" : { "state" : "translated", "value" : "Terjemahan dilihat oleh %#@number_of_parents@" }, "substitutions" : { "number_of_parents" : { "formatSpecifier" : "ld", "variations" : { "plural" : { "other" : { "stringUnit" : { "state" : "translated", "value" : "%arg ibu bapa" } } } } } } }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Vertaling door %#@number_of_parents@ ouder(s) bekeken" }, "substitutions" : { "number_of_parents" : { "formatSpecifier" : "ld", "variations" : { "plural" : { "one" : { "stringUnit" : { "state" : "new", "value" : "%arg parent" } }, "other" : { "stringUnit" : { "state" : "new", "value" : "%arg parents" } } } } } } }, "pa-Arab-PK" : { "stringUnit" : { "state" : "translated", "value" : " ولوں ترجمہ ویکھیا گیا%#@number_of_parents@" }, "substitutions" : { "number_of_parents" : { "formatSpecifier" : "ld", "variations" : { "plural" : { "one" : { "stringUnit" : { "state" : "new", "value" : "%arg parent" } }, "other" : { "stringUnit" : { "state" : "new", "value" : "%arg parents" } } } } } } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Tłumaczenie wyświetlone przez %#@number_of_parents@" }, "substitutions" : { "number_of_parents" : { "formatSpecifier" : "ld", "variations" : { "plural" : { "few" : { "stringUnit" : { "state" : "translated", "value" : "%arg rodziców" } }, "many" : { "stringUnit" : { "state" : "translated", "value" : "%arg rodziców" } }, "one" : { "stringUnit" : { "state" : "translated", "value" : "%arg rodzic" } }, "other" : { "stringUnit" : { "state" : "translated", "value" : "%arg rodziców" } } } } } } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Tradução visualizada por %#@number_of_parents@" }, "substitutions" : { "number_of_parents" : { "formatSpecifier" : "ld", "variations" : { "plural" : { "one" : { "stringUnit" : { "state" : "translated", "value" : "%arg pai/mãe" } }, "other" : { "stringUnit" : { "state" : "translated", "value" : "%arg pais" } } } } } } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Tradução vista por %#@number_of_parents@" }, "substitutions" : { "number_of_parents" : { "formatSpecifier" : "ld", "variations" : { "plural" : { "one" : { "stringUnit" : { "state" : "translated", "value" : "%arg pai/mãe" } }, "other" : { "stringUnit" : { "state" : "translated", "value" : "%arg pais/mães" } } } } } } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "%#@number_of_parents@ просмотрели перевод" }, "substitutions" : { "number_of_parents" : { "formatSpecifier" : "ld", "variations" : { "plural" : { "few" : { "stringUnit" : { "state" : "translated", "value" : "%arg родителей" } }, "many" : { "stringUnit" : { "state" : "translated", "value" : "%arg родителей" } }, "one" : { "stringUnit" : { "state" : "translated", "value" : "%arg родитель" } }, "other" : { "stringUnit" : { "state" : "translated", "value" : "%arg родителей" } } } } } } }, "th" : { "stringUnit" : { "state" : "translated", "value" : "มีผู้ดูคำแปลแล้ว %#@number_of_parents@ คน" }, "substitutions" : { "number_of_parents" : { "formatSpecifier" : "ld", "variations" : { "plural" : { "other" : { "stringUnit" : { "state" : "translated", "value" : "ผู้ปกครอง %arg คน" } } } } } } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Tercüme %#@number_of_parents@ tarafından görütülendi" }, "substitutions" : { "number_of_parents" : { "formatSpecifier" : "ld", "variations" : { "plural" : { "one" : { "stringUnit" : { "state" : "translated", "value" : "%arg veli" } }, "other" : { "stringUnit" : { "state" : "translated", "value" : "%arg veli" } } } } } } }, "ur-PK" : { "stringUnit" : { "state" : "translated", "value" : "%#@number_of_parents@ نے ترجمہ دیکھا" }, "substitutions" : { "number_of_parents" : { "formatSpecifier" : "ld", "variations" : { "plural" : { "one" : { "stringUnit" : { "state" : "translated", "value" : "%arg والد/والدہ" } }, "other" : { "stringUnit" : { "state" : "translated", "value" : "%arg والدین" } } } } } } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Bài dịch đã được %#@number_of_parents@ phụ huynh xem " }, "substitutions" : { "number_of_parents" : { "formatSpecifier" : "ld", "variations" : { "plural" : { "other" : { "stringUnit" : { "state" : "translated", "value" : "%arg phụ huynh " } } } } } } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "%#@number_of_parents@ 查看了译文" }, "substitutions" : { "number_of_parents" : { "formatSpecifier" : "ld", "variations" : { "plural" : { "other" : { "stringUnit" : { "state" : "translated", "value" : "%arg 名家长" } } } } } } } } } ```
liamnichols commented 9 months ago

Great, thanks! From a quick glance, it looks like argNum is missing... It's fun working with an undocumented file format 😆 ... I'll try to reproduce to that kind of configuration in the test suite and confirm if it is that 👍

liamnichols commented 9 months ago

That said.. How do you actually get the Strings Catalog GUI to let you add a single substitution?

The option to Vary by Plural isn't enabled in this case:

Screenshot 2023-11-28 at 16 03 42
Kaspik commented 9 months ago

Honestly, no idea, that was an automated mgiration of Xcode from .stringsdict :) But we have plenty of those.

Screenshot 2023-11-28 at 16 33 54
liamnichols commented 9 months ago

that was an automated migration of Xcode

Ahh, that makes sense.. .stringsdict required that you had a single substitution (purple) whereas .xcstrings seems to support one plural without the substitution syntax.

I created a .stringsdict file that replicates a few valid scenarios:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>fromStringsDictionary</key>
    <dict>
        <key>NSStringLocalizedFormatKey</key>
        <string>There %#@number_of_substitutions@ in this phrase</string>
        <key>number_of_substitutions</key>
        <dict>
            <key>NSStringFormatSpecTypeKey</key>
            <string>NSStringPluralRuleType</string>
            <key>NSStringFormatValueTypeKey</key>
            <string>lld</string>
            <key>zero</key>
            <string>are no substitutions</string>
            <key>one</key>
            <string>is %lld substitution</string>
            <key>other</key>
            <string>are %lld substitutions</string>
        </dict>
    </dict>
    <key>fromStringsDictionaryWithArg</key>
    <dict>
        <key>NSStringLocalizedFormatKey</key>
        <string>%@! There %#@number_of_substitutions@ in this phrase</string>
        <key>number_of_substitutions</key>
        <dict>
            <key>NSStringFormatSpecTypeKey</key>
            <string>NSStringPluralRuleType</string>
            <key>NSStringFormatValueTypeKey</key>
            <string>lld</string>
            <key>zero</key>
            <string>are no substitutions</string>
            <key>one</key>
            <string>is %lld substitution</string>
            <key>other</key>
            <string>are %lld substitutions</string>
        </dict>
    </dict>
    <key>fromStringsDictionaryWithPositionalArgs</key>
    <dict>
        <key>NSStringLocalizedFormatKey</key>
        <string>%2$@! There %1$#@number_of_substitutions@ in this phrase</string>
        <key>number_of_substitutions</key>
        <dict>
            <key>NSStringFormatSpecTypeKey</key>
            <string>NSStringPluralRuleType</string>
            <key>NSStringFormatValueTypeKey</key>
            <string>lld</string>
            <key>zero</key>
            <string>are no substitutions</string>
            <key>one</key>
            <string>is %lld substitution</string>
            <key>other</key>
            <string>are %lld substitutions</string>
        </dict>
    </dict>
</dict>
</plist>
func testStringsDictionary() throws {
    let bundle = Bundle.module

    XCTAssertEqual(
        String(format: bundle.localizedString(forKey: "fromStringsDictionary", value: nil, table: "Migration"), 0),
        "There are no substitutions in this phrase"
    )

    XCTAssertEqual(
        String(format: bundle.localizedString(forKey: "fromStringsDictionaryWithArg", value: nil, table: "Migration"), "John", 10),
        "John! There are 10 substitutions in this phrase"
    )

    XCTAssertEqual(
        String(format: bundle.localizedString(forKey: "fromStringsDictionaryWithPositionalArgs", value: nil, table: "Migration"), 1, "John"),
        "John! There is 1 substitution in this phrase"
    )
}

Using the migrator, this gives me the following Strings Catalog:

{
  "sourceLanguage" : "en",
  "strings" : {
    "fromStringsDictionary" : {
      "extractionState" : "migrated",
      "localizations" : {
        "en" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "There %#@number_of_substitutions@ in this phrase"
          },
          "substitutions" : {
            "number_of_substitutions" : {
              "formatSpecifier" : "lld",
              "variations" : {
                "plural" : {
                  "one" : {
                    "stringUnit" : {
                      "state" : "translated",
                      "value" : "is %arg substitution"
                    }
                  },
                  "other" : {
                    "stringUnit" : {
                      "state" : "translated",
                      "value" : "are %arg substitutions"
                    }
                  },
                  "zero" : {
                    "stringUnit" : {
                      "state" : "translated",
                      "value" : "are no substitutions"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "fromStringsDictionaryWithArg" : {
      "extractionState" : "migrated",
      "localizations" : {
        "en" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "%@! There %#@number_of_substitutions@ in this phrase"
          },
          "substitutions" : {
            "number_of_substitutions" : {
              "formatSpecifier" : "lld",
              "variations" : {
                "plural" : {
                  "one" : {
                    "stringUnit" : {
                      "state" : "translated",
                      "value" : "is %arg substitution"
                    }
                  },
                  "other" : {
                    "stringUnit" : {
                      "state" : "translated",
                      "value" : "are %arg substitutions"
                    }
                  },
                  "zero" : {
                    "stringUnit" : {
                      "state" : "translated",
                      "value" : "are no substitutions"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "fromStringsDictionaryWithPositionalArgs" : {
      "extractionState" : "migrated",
      "localizations" : {
        "en" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "%2$@! There %#@number_of_substitutions@ in this phrase"
          },
          "substitutions" : {
            "number_of_substitutions" : {
              "argNum" : 1,
              "formatSpecifier" : "lld",
              "variations" : {
                "plural" : {
                  "one" : {
                    "stringUnit" : {
                      "state" : "translated",
                      "value" : "is %lld substitution"
                    }
                  },
                  "other" : {
                    "stringUnit" : {
                      "state" : "translated",
                      "value" : "are %lld substitutions"
                    }
                  },
                  "zero" : {
                    "stringUnit" : {
                      "state" : "translated",
                      "value" : "are no substitutions"
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "version" : "1.0"
}

So argNum is only non-nil in a couple of scenarios:

  1. You create the substitutions in the Strings Catalog GUI yourself
  2. You had specified them in your Strings Dictionary NSStringLocalizedFormatKey value explicitly

This means that we should not assume that it will be defined. The only reason that we require it anyway is to use the substitution key as the label for the argument.. I'll have a play around to see how to relax this a bit 👍