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

Migrate to Apple-native string catalog and codegen LocalizedStringResources with xcstrings-tool-plugin #738

Merged
merged 1 commit into from
Dec 28, 2023

Conversation

tyiu
Copy link
Contributor

@tyiu tyiu commented Dec 15, 2023

Closes: #718

Previously,
English strings were written in Nos/Assets/Localization/Localized.swift on the Localized enum, and then Generated.strings were generated for each locale that Nos supports, which could be localized and read in code. Argument substitutions were done in a bespoke way using regex. Locales (such as Arabic) that did not follow the pluralization rules of English would not be able to localize properly. There were no comments to provide context to translators, which probably made their work more difficult.

Now,
Nos/Assets/Localization/*.xcstrings (string catalog) files are used instead. String catalogs were introduced in Xcode 15 and they are supported natively, including argument substitution and localized plural rules. Each xcstrings file contains the translations of all locales. Crowdin has support for xcstrings files, and will write translations into the same file. I opted to use an MIT-licensed open source plugin called xcstrings-tool that transforms xcstrings files to generated code, so that we can reference LocalizedStringResources in a type-safe way. There's also now a way to provide comments to strings so that translators have context when translating.

I copied all the current translations from Crowdin to the string catalog files. There was a bit of manual work here so hopefully I got it all right.

Someone who has access to the Crowdin project will need to make sure the integration works fine with these changes. I did some testing on my local Crowdin project and it seemed to work. Keep in mind that the history of the past translation work and translator attribution will probably get wiped out.

There also seems to be a bug where Xcode can occasionally crash when entering strings with arguments in the string catalog editor for non-default locales. The workaround for now is to edit in a text editor. But typically translators won't be interacting with Xcode, just Crowdin.

English German Chinese (Traditional)
Simulator Screenshot - iPhone 15 Pro - 2023-12-14 at 10 33 57 Medium Simulator Screenshot - iPhone 15 Pro - 2023-12-14 at 10 33 40 Medium Simulator Screenshot - iPhone 15 Pro - 2023-12-14 at 10 42 46 Medium

Xcode String Catalog Editor
Screenshot 2023-12-15 at 10 08 22 AM

Generated code from xcstrings-tool
Screenshot 2023-12-15 at 10 00 52 AM
Screenshot 2023-12-15 at 10 07 41 AM

@@ -251,9 +251,7 @@ fileprivate struct BottomOverlay: View {
if replyCount == 0 {
return nil
}
let replyCount = replyCount
let localized = replyCount == 1 ? Localized.Reply.one : Localized.Reply.many
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic did not work for locales that support different pluralization rules than just one and many.

@@ -57,16 +57,16 @@ struct RepostButton: View {
.padding(.vertical, 12)
}
.disabled(tapped)
.confirmationDialog("Repost", isPresented: $shouldConfirmRepost) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardcoded string that was not localized.

@@ -57,16 +57,16 @@ struct RepostButton: View {
.padding(.vertical, 12)
}
.disabled(tapped)
.confirmationDialog("Repost", isPresented: $shouldConfirmRepost) {
Button("Repost") {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardcoded string that was not localized.

tapped = false
}
}
.confirmationDialog("Delete repost", isPresented: $shouldConfirmDelete) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardcoded string that was not localized.

tapped = false
}
}
.confirmationDialog("Delete repost", isPresented: $shouldConfirmDelete) {
Button("Delete repost", role: .destructive) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardcoded string that was not localized.

@@ -42,9 +42,7 @@ struct NoteCard: View {
if replyCount == 0 {
return nil
}
let replyCount = replyCount
let localized = replyCount == 1 ? Localized.Reply.one : Localized.Reply.many
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic did not work for locales that support different pluralization rules than just one and many.

"two": second.safeName,
"count": "\(followers.count - 2)"
]))
Text(.localizable.followedByTwoAndMore(first.safeName, second.safeName, followers.count - 2))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't able to figure out if named arguments could be supported. Leaving that as an open question.

translation: /Nos/Assets/Localization/%osx_code%/%original_file_name%
- source: /Nos/Assets/Localization/ImagePicker.xcstrings
translation: /Nos/Assets/Localization/ImagePicker.xcstrings
multilingual: 1
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indicates that the file contains translations for multiple locales.

@tyiu tyiu changed the title Migrate to Apple-native strings catalog and codegen LocalizedStringResources with xcstrings-tool-plugin Migrate to Apple-native string catalog and codegen LocalizedStringResources with xcstrings-tool-plugin Dec 18, 2023
@tyiu
Copy link
Contributor Author

tyiu commented Dec 18, 2023

Rebased on the main branch and resolved the conflicts. I also brought in new Persian translations that came in after I created this pull request.

Copy link
Member

@mplorentz mplorentz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow, this is a huge contribution and a big improvement over the previous system! Regarding the history here: I actually don't know why we were using a handmade ExportStrings.sh to do localization. It was set up for the Planetary app before I started, and when we started Nos we copied a lot of the strings over and with it came this system.

@martindsq can you give this a proper review since you manage the Crowdin integration and have a lot more expertise than me in this area?

@tyiu
Copy link
Contributor Author

tyiu commented Dec 19, 2023

There seems to be an issue where Xcode formats the JSON in the *.xcstrings files with a space after the JSON key, but Crowdin does not. There will likely be some noise flip-flopping back and forth between the spacing discrepancies until either one of them decides to change how they write xcstrings files to be consistent with the other. This may cause merge conflicts, not sure what the best path forward is. I've filed an issue here and they've said that they're working on a fix: https://community.crowdin.com/t/apple-xcstrings-string-catalog-format-discrepancy-between-crowdin-and-xcode/5937

Crowdin has fixed the issue. I'm removing this blurb from the PR description.

@tyiu
Copy link
Contributor Author

tyiu commented Dec 19, 2023

@mplorentz
Copy link
Member

Yeah sorry about that. Just clicked merge on that PR. Also we have some tests failing on main, I will be addressing them tomorrow.

@tyiu
Copy link
Contributor Author

tyiu commented Dec 20, 2023

Yeah sorry about that. Just clicked merge on that PR. Also we have some tests failing on main, I will be addressing them tomorrow.

No worries. I just rebased and resolved the conflicts..

@@ -55,7 +55,7 @@ struct PreviewData {
}()

lazy var bob: Author = {
let author = Author(context: previewContext)
let author = try! Author.findOrCreate(by: KeyFixture.bob.publicKeyHex, context: previewContext)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this was a bug that bob wasn't in Core Data. alice and eve were though, so I copied what they did.

@@ -157,13 +157,13 @@ struct ContentWarningMessage: View {
// Assuming there's a property or method 'safeName' in 'Author' that safely returns the author's name.
private var firstAuthorSafeName: String {
// Getting the safe name of the first author. Adjust according to your actual data structure.
reports.first?.author?.safeName ?? "Unknown Author"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pulled out Unknown Author to localize it.

}

private var reason: String {
// Extract content from reports and remove empty content
let contents = reports.compactMap { (($0.content?.isEmpty) != nil) ? nil : $0.content }
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was a bug. It would always return nil.

@tyiu
Copy link
Contributor Author

tyiu commented Dec 25, 2023

Rebased on the main branch, resolved conflicts, and added new translations for Chinese Simplified, Chinese Traditional, and German locales.

@martindsq martindsq merged commit 0209599 into planetary-social:main Dec 28, 2023
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

String localization is not fully internationalized
3 participants