├── Apps ├── Resources │ ├── ru.lproj │ │ ├── LaunchScreen.strings │ │ ├── Localizable.strings │ │ └── Localizable.stringsdict │ ├── Colors.xcassets │ │ ├── Contents.json │ │ └── customGray.colorset │ │ │ └── Contents.json │ ├── macOS.xcassets │ │ ├── Contents.json │ │ ├── AppIcon.appiconset │ │ │ ├── 128.png │ │ │ ├── 16.png │ │ │ ├── 256.png │ │ │ ├── 32.png │ │ │ ├── 512.png │ │ │ ├── 64.png │ │ │ ├── 256-1.png │ │ │ ├── 32-1.png │ │ │ ├── New Project (11)-1.png │ │ │ ├── New Project (12).png │ │ │ └── Contents.json │ │ └── status-bar-icon.imageset │ │ │ ├── letterboxd-decal-l-pos-rgb-500px.png │ │ │ ├── letterboxd-decal-l-pos-rgb-500px@2x.png │ │ │ ├── letterboxd-decal-l-pos-rgb-500px@3x.png │ │ │ └── Contents.json │ ├── en.lproj │ │ ├── Localizable.strings │ │ └── Localizable.stringsdict │ ├── Base.lproj │ │ ├── Localizable.strings │ │ └── Localizable.stringsdict │ └── LocalizationHelper.entitlements ├── TestsProjects │ ├── Test1 │ │ ├── Test1 │ │ │ ├── Assets.xcassets │ │ │ │ ├── Contents.json │ │ │ │ ├── AccentColor.colorset │ │ │ │ │ └── Contents.json │ │ │ │ └── AppIcon.appiconset │ │ │ │ │ └── Contents.json │ │ │ ├── en.lproj │ │ │ │ └── Localizable.strings │ │ │ ├── vi.lproj │ │ │ │ └── Localizable.strings │ │ │ ├── Test1.xcdatamodeld │ │ │ │ ├── .xccurrentversion │ │ │ │ └── Test1.xcdatamodel │ │ │ │ │ └── contents │ │ │ ├── ViewController.swift │ │ │ ├── Base.lproj │ │ │ │ ├── Main.storyboard │ │ │ │ └── LaunchScreen.storyboard │ │ │ ├── Info.plist │ │ │ ├── SceneDelegate.swift │ │ │ └── AppDelegate.swift │ │ ├── Test1.xcodeproj │ │ │ └── project.xcworkspace │ │ │ │ ├── contents.xcworkspacedata │ │ │ │ └── xcshareddata │ │ │ │ └── IDEWorkspaceChecks.plist │ │ ├── Test1Tests │ │ │ ├── Info.plist │ │ │ └── Test1Tests.swift │ │ └── Test1UITests │ │ │ ├── Info.plist │ │ │ └── Test1UITests.swift │ ├── Test2 │ │ ├── Test2 │ │ │ ├── Assets.xcassets │ │ │ │ ├── Contents.json │ │ │ │ ├── AccentColor.colorset │ │ │ │ │ └── Contents.json │ │ │ │ └── AppIcon.appiconset │ │ │ │ │ └── Contents.json │ │ │ ├── Resources │ │ │ │ ├── en.lproj │ │ │ │ │ └── Localizable.strings │ │ │ │ └── vi.lproj │ │ │ │ │ └── Localizable.strings │ │ │ ├── Test2.xcdatamodeld │ │ │ │ ├── .xccurrentversion │ │ │ │ └── Test2.xcdatamodel │ │ │ │ │ └── contents │ │ │ ├── ViewController.swift │ │ │ ├── Base.lproj │ │ │ │ ├── Main.storyboard │ │ │ │ └── LaunchScreen.storyboard │ │ │ ├── Info.plist │ │ │ ├── SceneDelegate.swift │ │ │ └── AppDelegate.swift │ │ ├── Test2.xcodeproj │ │ │ └── project.xcworkspace │ │ │ │ ├── contents.xcworkspacedata │ │ │ │ └── xcshareddata │ │ │ │ └── IDEWorkspaceChecks.plist │ │ ├── Test2Tests │ │ │ ├── Info.plist │ │ │ └── Test2Tests.swift │ │ └── Test2UITests │ │ │ ├── Info.plist │ │ │ └── Test2UITests.swift │ ├── Test3 │ │ ├── Test3 │ │ │ ├── 1 │ │ │ │ └── 1.1 │ │ │ │ │ └── 1.1.1 │ │ │ │ │ └── 1.1.1.2 │ │ │ │ │ ├── en.lproj │ │ │ │ │ └── Localizable.strings │ │ │ │ │ └── vi.lproj │ │ │ │ │ └── Localizable.strings │ │ │ ├── Assets.xcassets │ │ │ │ ├── Contents.json │ │ │ │ ├── AccentColor.colorset │ │ │ │ │ └── Contents.json │ │ │ │ └── AppIcon.appiconset │ │ │ │ │ └── Contents.json │ │ │ ├── Test3.xcdatamodeld │ │ │ │ ├── .xccurrentversion │ │ │ │ └── Test3.xcdatamodel │ │ │ │ │ └── contents │ │ │ ├── ViewController.swift │ │ │ ├── Base.lproj │ │ │ │ ├── Main.storyboard │ │ │ │ └── LaunchScreen.storyboard │ │ │ ├── Info.plist │ │ │ ├── SceneDelegate.swift │ │ │ └── AppDelegate.swift │ │ ├── Test3.xcodeproj │ │ │ └── project.xcworkspace │ │ │ │ ├── contents.xcworkspacedata │ │ │ │ └── xcshareddata │ │ │ │ └── IDEWorkspaceChecks.plist │ │ ├── Test3Tests │ │ │ ├── Info.plist │ │ │ └── Test3Tests.swift │ │ └── Test3UITests │ │ │ ├── Info.plist │ │ │ └── Test3UITests.swift │ ├── Test4 │ │ ├── Test4 │ │ │ ├── Assets.xcassets │ │ │ │ ├── Contents.json │ │ │ │ ├── AccentColor.colorset │ │ │ │ │ └── Contents.json │ │ │ │ └── AppIcon.appiconset │ │ │ │ │ └── Contents.json │ │ │ ├── Test4.xcdatamodeld │ │ │ │ ├── .xccurrentversion │ │ │ │ └── Test4.xcdatamodel │ │ │ │ │ └── contents │ │ │ ├── ViewController.swift │ │ │ ├── Base.lproj │ │ │ │ ├── Main.storyboard │ │ │ │ └── LaunchScreen.storyboard │ │ │ ├── Info.plist │ │ │ ├── SceneDelegate.swift │ │ │ └── AppDelegate.swift │ │ ├── Test4.xcodeproj │ │ │ └── project.xcworkspace │ │ │ │ ├── contents.xcworkspacedata │ │ │ │ └── xcshareddata │ │ │ │ └── IDEWorkspaceChecks.plist │ │ ├── Test4Tests │ │ │ ├── Info.plist │ │ │ └── Test4Tests.swift │ │ └── Test4UITests │ │ │ ├── Info.plist │ │ │ └── Test4UITests.swift │ └── TestWithTuist │ │ ├── Tuist │ │ ├── Config.swift │ │ └── ProjectDescriptionHelpers │ │ │ └── Project+Templates.swift │ │ ├── Targets │ │ ├── TestWithTuist │ │ │ ├── Tests │ │ │ │ └── AppTests.swift │ │ │ ├── Sources │ │ │ │ └── AppDelegate.swift │ │ │ └── Resources │ │ │ │ └── LaunchScreen.storyboard │ │ ├── TestWithTuistUI │ │ │ ├── Sources │ │ │ │ └── TestWithTuistUI.swift │ │ │ └── Tests │ │ │ │ └── TestWithTuistUITests.swift │ │ └── TestWithTuistKit │ │ │ ├── Sources │ │ │ └── TestWithTuistKit.swift │ │ │ └── Tests │ │ │ └── TestWithTuistKitTests.swift │ │ ├── Project.swift │ │ └── .gitignore ├── Sources │ ├── Preview Content │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ ├── Extensions │ │ ├── Typealiases.swift │ │ ├── Strings+Extensions.swift │ │ ├── Binding+Extensions.swift │ │ └── XcodeProj+Extensions.swift │ ├── Models │ │ ├── Constants.swift │ │ ├── AppState.swift │ │ ├── LocalizationHelperError.swift │ │ ├── Project │ │ │ ├── TuistProject.swift │ │ │ └── DefaultProject.swift │ │ └── LocalizableFile.swift │ ├── Services │ │ ├── LocalizableFileService.swift │ │ ├── XCodeProjectService.swift │ │ ├── StringsFileGenerator.swift │ │ └── FilePickerService.swift │ ├── System │ │ ├── Application.swift │ │ ├── AppDelegate.swift │ │ └── StatusBarController.swift │ ├── Utilities │ │ ├── CancelBag.swift │ │ ├── Store.swift │ │ └── Loadable.swift │ ├── UI │ │ ├── Link.swift │ │ ├── OpenProject │ │ │ └── OpenProjectView.swift │ │ ├── Main │ │ │ ├── MainViewModel.swift │ │ │ └── MainView.swift │ │ ├── BETextEditor.swift │ │ ├── SelectLanguageView │ │ │ └── SelectLanguageView.swift │ │ └── Project │ │ │ └── LocalizableFileView.swift │ ├── Handlers │ │ └── OpenProjectHandler.swift │ ├── Injected │ │ └── App+Resolver.swift │ └── Repository │ │ └── ProjectRepository.swift ├── Tests │ ├── EditMeBeforeTesting.swift │ ├── Utilities │ │ └── TestProjectRepository.swift │ ├── Extensions │ │ └── PBXGroupTests.swift │ └── Services │ │ └── XcodeProjectServiceTests.swift └── Info.plist ├── images ├── screenshot.png ├── translate-key.png ├── choose-languages.png └── add-and-copy-to-clipboard.png ├── ruby ├── Localizable.strings └── index.rb ├── release └── LocalizationHelper.zip ├── Demo ├── LocalizationHelperDemo │ ├── Assets.xcassets │ │ ├── Contents.json │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── Preview Content │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ ├── en.lproj │ │ └── Localizable.strings │ ├── ar-LY.lproj │ │ └── Localizable.strings │ ├── es.lproj │ │ └── Localizable.strings │ ├── fr.lproj │ │ └── Localizable.strings │ ├── vi.lproj │ │ └── Localizable.strings │ ├── ru-RU.lproj │ │ └── Localizable.strings │ ├── LocalizationHelperDemoApp.swift │ ├── ContentView.swift │ └── Info.plist ├── LocalizationHelperDemo.xcodeproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── .gitignore ├── Kit ├── Tests │ ├── Resources │ │ └── Localizable.strings │ ├── ConfigRepositoryTests.swift │ └── StreamReaderTests.swift └── Sources │ ├── ConfigManager │ ├── ConfigRepositoryError.swift │ ├── Config.swift │ ├── ConfigManager.swift │ └── ConfigRepository.swift │ ├── EventMonitor.swift │ ├── GoogleTranslate.swift │ └── StreamReader.swift ├── LICENSE.txt ├── .gitignore ├── .package.resolved ├── Project.swift └── Tuist └── ResourceSynthesizers └── Strings.stencil /Apps/Resources/ru.lproj/LaunchScreen.strings: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /images/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigearsenal/XCodeLocalizationHelper/HEAD/images/screenshot.png -------------------------------------------------------------------------------- /ruby/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | 4 | Created with LocalizationHelper. 5 | 6 | */ 7 | -------------------------------------------------------------------------------- /images/translate-key.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigearsenal/XCodeLocalizationHelper/HEAD/images/translate-key.png -------------------------------------------------------------------------------- /images/choose-languages.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigearsenal/XCodeLocalizationHelper/HEAD/images/choose-languages.png -------------------------------------------------------------------------------- /Apps/Resources/Colors.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Apps/Resources/macOS.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /release/LocalizationHelper.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigearsenal/XCodeLocalizationHelper/HEAD/release/LocalizationHelper.zip -------------------------------------------------------------------------------- /images/add-and-copy-to-clipboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigearsenal/XCodeLocalizationHelper/HEAD/images/add-and-copy-to-clipboard.png -------------------------------------------------------------------------------- /Apps/TestsProjects/Test1/Test1/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test2/Test2/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test3/Test3/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test4/Test4/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Apps/TestsProjects/TestWithTuist/Tuist/Config.swift: -------------------------------------------------------------------------------- 1 | import ProjectDescription 2 | 3 | let config = Config( 4 | generationOptions: [] 5 | ) 6 | -------------------------------------------------------------------------------- /Demo/LocalizationHelperDemo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Apps/Sources/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test1/Test1/en.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | 4 | Created with LocalizationHelper. 5 | 6 | */ 7 | 8 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test1/Test1/vi.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | 4 | Created with LocalizationHelper. 5 | 6 | */ 7 | 8 | -------------------------------------------------------------------------------- /Demo/LocalizationHelperDemo/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Apps/Resources/en.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | p2p_wallet 4 | 5 | Created by Chung Tran on 10/23/20. 6 | 7 | */ 8 | "hello" = "hello"; 9 | -------------------------------------------------------------------------------- /Apps/Resources/ru.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | p2p_wallet 4 | 5 | Created by Chung Tran on 10/23/20. 6 | 7 | */ 8 | "hello" = "привет"; 9 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test2/Test2/Resources/en.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | Test2 4 | 5 | Created by Chung Tran on 20/07/2021. 6 | 7 | */ 8 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test2/Test2/Resources/vi.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | Test2 4 | 5 | Created by Chung Tran on 20/07/2021. 6 | 7 | */ 8 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test3/Test3/1/1.1/1.1.1/1.1.1.2/en.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | 4 | Created with LocalizationHelper. 5 | 6 | */ 7 | 8 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test3/Test3/1/1.1/1.1.1/1.1.1.2/vi.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | 4 | Created with LocalizationHelper. 5 | 6 | */ 7 | 8 | -------------------------------------------------------------------------------- /Apps/Resources/Base.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | p2p_wallet 4 | 5 | Created by Chung Tran on 10/23/20. 6 | 7 | */ 8 | "hello" = "hello"; 9 | -------------------------------------------------------------------------------- /Apps/Resources/macOS.xcassets/AppIcon.appiconset/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigearsenal/XCodeLocalizationHelper/HEAD/Apps/Resources/macOS.xcassets/AppIcon.appiconset/128.png -------------------------------------------------------------------------------- /Apps/Resources/macOS.xcassets/AppIcon.appiconset/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigearsenal/XCodeLocalizationHelper/HEAD/Apps/Resources/macOS.xcassets/AppIcon.appiconset/16.png -------------------------------------------------------------------------------- /Apps/Resources/macOS.xcassets/AppIcon.appiconset/256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigearsenal/XCodeLocalizationHelper/HEAD/Apps/Resources/macOS.xcassets/AppIcon.appiconset/256.png -------------------------------------------------------------------------------- /Apps/Resources/macOS.xcassets/AppIcon.appiconset/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigearsenal/XCodeLocalizationHelper/HEAD/Apps/Resources/macOS.xcassets/AppIcon.appiconset/32.png -------------------------------------------------------------------------------- /Apps/Resources/macOS.xcassets/AppIcon.appiconset/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigearsenal/XCodeLocalizationHelper/HEAD/Apps/Resources/macOS.xcassets/AppIcon.appiconset/512.png -------------------------------------------------------------------------------- /Apps/Resources/macOS.xcassets/AppIcon.appiconset/64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigearsenal/XCodeLocalizationHelper/HEAD/Apps/Resources/macOS.xcassets/AppIcon.appiconset/64.png -------------------------------------------------------------------------------- /Apps/Resources/macOS.xcassets/AppIcon.appiconset/256-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigearsenal/XCodeLocalizationHelper/HEAD/Apps/Resources/macOS.xcassets/AppIcon.appiconset/256-1.png -------------------------------------------------------------------------------- /Apps/Resources/macOS.xcassets/AppIcon.appiconset/32-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigearsenal/XCodeLocalizationHelper/HEAD/Apps/Resources/macOS.xcassets/AppIcon.appiconset/32-1.png -------------------------------------------------------------------------------- /Apps/Resources/macOS.xcassets/AppIcon.appiconset/New Project (11)-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigearsenal/XCodeLocalizationHelper/HEAD/Apps/Resources/macOS.xcassets/AppIcon.appiconset/New Project (11)-1.png -------------------------------------------------------------------------------- /Apps/Resources/macOS.xcassets/AppIcon.appiconset/New Project (12).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigearsenal/XCodeLocalizationHelper/HEAD/Apps/Resources/macOS.xcassets/AppIcon.appiconset/New Project (12).png -------------------------------------------------------------------------------- /Demo/LocalizationHelperDemo/en.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | 4 | Created with LocalizationHelper. 5 | 6 | */ 7 | 8 | "test" = "test"; 9 | "hello" = "hello"; 10 | "good" = "good"; 11 | -------------------------------------------------------------------------------- /Demo/LocalizationHelperDemo/ar-LY.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | 4 | Created with LocalizationHelper. 5 | 6 | */ 7 | 8 | "test" = "اختبار"; 9 | "hello" = "مرحبا"; 10 | "good" = "حسن"; 11 | -------------------------------------------------------------------------------- /Demo/LocalizationHelperDemo/es.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | 4 | Created with LocalizationHelper. 5 | 6 | */ 7 | 8 | "test" = "prueba"; 9 | "hello" = "Hola"; 10 | "good" = "bien"; 11 | -------------------------------------------------------------------------------- /Demo/LocalizationHelperDemo/fr.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | 4 | Created with LocalizationHelper. 5 | 6 | */ 7 | 8 | "test" = "test"; 9 | "hello" = "Bonjour"; 10 | "good" = "bien"; 11 | -------------------------------------------------------------------------------- /Kit/Tests/Resources/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | LocalizationHelper 4 | 5 | Created by Chung Tran on 15/04/2023. 6 | 7 | */ 8 | 9 | "test"="test"; 10 | "test\nnewline"="test\nnewline"; 11 | -------------------------------------------------------------------------------- /Demo/LocalizationHelperDemo/vi.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | 4 | Created with LocalizationHelper. 5 | 6 | */ 7 | 8 | "test" = "kiểm tra"; 9 | "hello" = "xin chào"; 10 | "good" = "tốt"; 11 | -------------------------------------------------------------------------------- /Apps/Sources/Extensions/Typealiases.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Typealiases.swift 3 | // Bigvalut 4 | // 5 | // Created by Chung Tran on 27/06/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | public typealias Strings = L10n.Localizable 11 | -------------------------------------------------------------------------------- /Apps/Resources/LocalizationHelper.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Apps/Sources/Models/Constants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Constants.swift 3 | // LocalizationHelper 4 | // 5 | // Created by Chung Tran on 21/07/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | let LOCALIZABLE_STRINGS = "Localizable.strings" 11 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test1/Test1.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test2/Test2.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test3/Test3.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test4/Test4.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Demo/LocalizationHelperDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Demo/LocalizationHelperDemo/ru-RU.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | 4 | Created with LocalizationHelper. 5 | 6 | */ 7 | 8 | "test" = "контрольная работа"; 9 | "hello" = "Привет"; 10 | "good" = "хорошо"; 11 | -------------------------------------------------------------------------------- /Apps/Resources/macOS.xcassets/status-bar-icon.imageset/letterboxd-decal-l-pos-rgb-500px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigearsenal/XCodeLocalizationHelper/HEAD/Apps/Resources/macOS.xcassets/status-bar-icon.imageset/letterboxd-decal-l-pos-rgb-500px.png -------------------------------------------------------------------------------- /Apps/TestsProjects/TestWithTuist/Targets/TestWithTuist/Tests/AppTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XCTest 3 | 4 | final class TestWithTuistTests: XCTestCase { 5 | func test_twoPlusTwo_isFour() { 6 | XCTAssertEqual(2+2, 4) 7 | } 8 | } -------------------------------------------------------------------------------- /Apps/TestsProjects/TestWithTuist/Targets/TestWithTuistUI/Sources/TestWithTuistUI.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public final class TestWithTuistUI { 4 | public static func hello() { 5 | print("Hello, from your UI framework") 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Demo/LocalizationHelperDemo/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Apps/Resources/macOS.xcassets/status-bar-icon.imageset/letterboxd-decal-l-pos-rgb-500px@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigearsenal/XCodeLocalizationHelper/HEAD/Apps/Resources/macOS.xcassets/status-bar-icon.imageset/letterboxd-decal-l-pos-rgb-500px@2x.png -------------------------------------------------------------------------------- /Apps/Resources/macOS.xcassets/status-bar-icon.imageset/letterboxd-decal-l-pos-rgb-500px@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigearsenal/XCodeLocalizationHelper/HEAD/Apps/Resources/macOS.xcassets/status-bar-icon.imageset/letterboxd-decal-l-pos-rgb-500px@3x.png -------------------------------------------------------------------------------- /Apps/TestsProjects/Test1/Test1/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test2/Test2/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test3/Test3/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test4/Test4/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Apps/TestsProjects/TestWithTuist/Targets/TestWithTuistKit/Sources/TestWithTuistKit.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public final class TestWithTuistKit { 4 | public static func hello() { 5 | print("Hello, from your Kit framework") 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Apps/TestsProjects/TestWithTuist/Targets/TestWithTuistUI/Tests/TestWithTuistUITests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XCTest 3 | 4 | final class TestWithTuistUITests: XCTestCase { 5 | func test_example() { 6 | XCTAssertEqual("TestWithTuistUI", "TestWithTuistUI") 7 | } 8 | } -------------------------------------------------------------------------------- /Apps/TestsProjects/TestWithTuist/Targets/TestWithTuistKit/Tests/TestWithTuistKitTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XCTest 3 | 4 | final class TestWithTuistKitTests: XCTestCase { 5 | func test_example() { 6 | XCTAssertEqual("TestWithTuistKit", "TestWithTuistKit") 7 | } 8 | } -------------------------------------------------------------------------------- /Apps/Sources/Services/LocalizableFileService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocalizableFileService.swift 3 | // LocalizationHelper 4 | // 5 | // Created by Chung Tran on 21/07/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol LocalizableFileServiceType { 11 | var file: LocalizableFile {get} 12 | 13 | } 14 | -------------------------------------------------------------------------------- /Kit/Sources/ConfigManager/ConfigRepositoryError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConfigReaderError.swift 3 | // LocalizationHelperKit 4 | // 5 | // Created by Chung Tran on 26/04/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | enum ConfigRepositoryError: Error { 11 | case couldNotOpenFile 12 | case invalidFileFormat 13 | } 14 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test1/Test1/Test1.xcdatamodeld/.xccurrentversion: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | _XCCurrentVersionName 6 | Test1.xcdatamodel 7 | 8 | 9 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test2/Test2/Test2.xcdatamodeld/.xccurrentversion: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | _XCCurrentVersionName 6 | Test2.xcdatamodel 7 | 8 | 9 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test3/Test3/Test3.xcdatamodeld/.xccurrentversion: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | _XCCurrentVersionName 6 | Test3.xcdatamodel 7 | 8 | 9 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test4/Test4/Test4.xcdatamodeld/.xccurrentversion: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | _XCCurrentVersionName 6 | Test4.xcdatamodel 7 | 8 | 9 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test1/Test1.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test2/Test2.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test3/Test3.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test4/Test4.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Demo/LocalizationHelperDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test1/Test1/Test1.xcdatamodeld/Test1.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test2/Test2/Test2.xcdatamodeld/Test2.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test3/Test3/Test3.xcdatamodeld/Test3.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test4/Test4/Test4.xcdatamodeld/Test4.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Demo/LocalizationHelperDemo/LocalizationHelperDemoApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocalizationHelperDemoApp.swift 3 | // LocalizationHelperDemo 4 | // 5 | // Created by Chung Tran on 28/06/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct LocalizationHelperDemoApp: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | ContentView() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test1/Test1/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Test1 4 | // 5 | // Created by Chung Tran on 20/07/2021. 6 | // 7 | 8 | import UIKit 9 | 10 | class ViewController: UIViewController { 11 | 12 | override func viewDidLoad() { 13 | super.viewDidLoad() 14 | // Do any additional setup after loading the view. 15 | } 16 | 17 | 18 | } 19 | 20 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test2/Test2/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Test2 4 | // 5 | // Created by Chung Tran on 20/07/2021. 6 | // 7 | 8 | import UIKit 9 | 10 | class ViewController: UIViewController { 11 | 12 | override func viewDidLoad() { 13 | super.viewDidLoad() 14 | // Do any additional setup after loading the view. 15 | } 16 | 17 | 18 | } 19 | 20 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test3/Test3/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Test3 4 | // 5 | // Created by Chung Tran on 20/07/2021. 6 | // 7 | 8 | import UIKit 9 | 10 | class ViewController: UIViewController { 11 | 12 | override func viewDidLoad() { 13 | super.viewDidLoad() 14 | // Do any additional setup after loading the view. 15 | } 16 | 17 | 18 | } 19 | 20 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test4/Test4/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Test4 4 | // 5 | // Created by Chung Tran on 20/07/2021. 6 | // 7 | 8 | import UIKit 9 | 10 | class ViewController: UIViewController { 11 | 12 | override func viewDidLoad() { 13 | super.viewDidLoad() 14 | // Do any additional setup after loading the view. 15 | } 16 | 17 | 18 | } 19 | 20 | -------------------------------------------------------------------------------- /Apps/Sources/System/Application.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Application.swift 3 | // LocalizationHelper_macos 4 | // 5 | // Created by Chung Tran on 28/06/2021. 6 | // 7 | 8 | import Cocoa 9 | 10 | class Application: NSApplication { 11 | 12 | let strongDelegate = AppDelegate() 13 | 14 | override init() { 15 | super.init() 16 | self.delegate = strongDelegate 17 | } 18 | 19 | required init?(coder: NSCoder) { 20 | fatalError("init(coder:) has not been implemented") 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /Demo/LocalizationHelperDemo/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // LocalizationHelperDemo 4 | // 5 | // Created by Chung Tran on 28/06/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ContentView: View { 11 | var body: some View { 12 | Text("hello") 13 | .padding() 14 | } 15 | } 16 | 17 | struct ContentView_Previews: PreviewProvider { 18 | static var previews: some View { 19 | ContentView() 20 | .environment(\.locale, .init(identifier: "es")) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Apps/Sources/Extensions/Strings+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Strings+Extensions.swift 3 | // LocalizationHelper 4 | // 5 | // Created by Chung Tran on 24/07/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | extension String { 11 | static var stringsFileHeader: String { 12 | """ 13 | /* 14 | Localizable.strings 15 | 16 | Created with XCodeLocalizationHelper. 17 | https://github.com/bigearsenal/xcodelocalizationhelper 18 | 19 | */ 20 | 21 | 22 | """ 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Apps/Sources/Models/AppState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppState.swift 3 | // LocalizationHelperKit 4 | // 5 | // Created by Chung Tran on 19/07/2021. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | 11 | /// The struct that contains all shared data inside application 12 | struct AppState: Equatable { 13 | var project: Project? 14 | 15 | static var initial: Self { 16 | .init(project: nil) 17 | } 18 | } 19 | 20 | #if DEBUG 21 | extension AppState { 22 | static var preview: AppState { 23 | AppState() 24 | } 25 | } 26 | #endif 27 | -------------------------------------------------------------------------------- /Apps/Sources/Extensions/Binding+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Extensions.swift 3 | // LocalizationHelper 4 | // 5 | // Created by Chung Tran on 27/12/2020. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | extension Binding { 12 | func didSet(_ execute: @escaping (Value) -> Void) -> Binding { 13 | return Binding( 14 | get: { 15 | return self.wrappedValue 16 | }, 17 | set: { 18 | self.wrappedValue = $0 19 | execute($0) 20 | } 21 | ) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Apps/Sources/Utilities/CancelBag.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CancelBag.swift 3 | // CountriesSwiftUI 4 | // 5 | // Created by Alexey Naumov on 04.04.2020. 6 | // Copyright © 2020 Alexey Naumov. All rights reserved. 7 | // 8 | 9 | import Combine 10 | 11 | final class CancelBag { 12 | fileprivate(set) var subscriptions = Set() 13 | 14 | func cancel() { 15 | subscriptions.removeAll() 16 | } 17 | } 18 | 19 | extension AnyCancellable { 20 | 21 | func store(in cancelBag: CancelBag) { 22 | cancelBag.subscriptions.insert(self) 23 | } 24 | } 25 | 26 | -------------------------------------------------------------------------------- /Apps/Resources/macOS.xcassets/status-bar-icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "letterboxd-decal-l-pos-rgb-500px.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "letterboxd-decal-l-pos-rgb-500px@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "letterboxd-decal-l-pos-rgb-500px@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Apps/Tests/EditMeBeforeTesting.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // LocalizationHelperTests 4 | // 5 | // Created by Chung Tran on 20/07/2021. 6 | // 7 | 8 | import Foundation 9 | import XcodeProj 10 | 11 | // FIXME: - REPLACE THIS URL BEFORE TESTING 12 | let repositoryLocalURL = "/Users/bigears/Documents/macos/XCodeLocalizationHelper" 13 | 14 | let homeUrl = repositoryLocalURL + "/Apps/TestsProjects/" 15 | 16 | let LOCALIZABLE_STRINGS = "Localizable.strings" 17 | 18 | func xcodeprojPath(fileName: String) -> String { 19 | homeUrl + fileName + "/" + fileName + ".xcodeproj" 20 | } 21 | func getXcodeProj(fileName: String) throws -> XcodeProj { 22 | try XcodeProj(pathString: xcodeprojPath(fileName: fileName)) 23 | } 24 | -------------------------------------------------------------------------------- /Apps/Sources/Models/LocalizationHelperError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Error.swift 3 | // LocalizationHelper 4 | // 5 | // Created by Chung Tran on 21/07/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | enum LocalizationHelperError: String, Swift.Error { 11 | case projectNotFound 12 | case targetNotFound 13 | case localizableStringsGroupNotFound 14 | case localizableStringsGroupFullPathNotFound 15 | case couldNotCreateLocalizableStringsGroup 16 | case resourcePathIsNotADirectory 17 | case resourcePathMustBeInsideProjectPath 18 | case lineReaderInitializingError 19 | } 20 | 21 | extension LocalizationHelperError: LocalizedError { 22 | var errorDescription: String? { 23 | self.rawValue 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Apps/Sources/Models/Project/TuistProject.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TuistProject.swift 3 | // LocalizationHelper 4 | // 5 | // Created by Chung Tran on 21/07/2021. 6 | // 7 | 8 | import Foundation 9 | import PathKit 10 | 11 | struct TuistProject: Equatable { 12 | var path: Path 13 | var resourcePath: Path 14 | var projectName: String 15 | } 16 | 17 | extension TuistProject { 18 | func localize(fileGenerator: FileGeneratorType, languageCode: String) throws 19 | { 20 | let path = resourcePath 21 | guard path.isDirectory else { 22 | throw LocalizationHelperError.resourcePathIsNotADirectory 23 | } 24 | 25 | try fileGenerator.generateFile(at: path + "\(languageCode).lproj", fileName: LOCALIZABLE_STRINGS, content: .stringsFileHeader) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Kit/Sources/ConfigManager/Config.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XCodeLocalizationHelperConfig.swift 3 | // LocalizationHelperKit 4 | // 5 | // Created by Chung Tran on 26/04/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct Config: Codable { 11 | public init(automation: AutomationConfig) { 12 | self.automation = automation 13 | } 14 | 15 | public let automation: AutomationConfig 16 | } 17 | 18 | public struct AutomationConfig: Codable { 19 | public init(script: String, pathType: AutomationConfigPathType) { 20 | self.script = script 21 | self.pathType = pathType 22 | } 23 | 24 | public let script: String 25 | public let pathType: AutomationConfigPathType 26 | } 27 | 28 | public enum AutomationConfigPathType: String, Codable { 29 | case relative 30 | case absolute 31 | } 32 | -------------------------------------------------------------------------------- /Apps/TestsProjects/TestWithTuist/Targets/TestWithTuist/Sources/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import TestWithTuistKit 3 | import TestWithTuistUI 4 | 5 | @main 6 | class AppDelegate: UIResponder, UIApplicationDelegate { 7 | 8 | var window: UIWindow? 9 | 10 | func application( 11 | _ application: UIApplication, 12 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil 13 | ) -> Bool { 14 | window = UIWindow(frame: UIScreen.main.bounds) 15 | let viewController = UIViewController() 16 | viewController.view.backgroundColor = .white 17 | window?.rootViewController = viewController 18 | window?.makeKeyAndVisible() 19 | TestWithTuistKit.hello() 20 | TestWithTuistUI.hello() 21 | 22 | return true 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test1/Test1Tests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test1/Test1UITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test2/Test2Tests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test2/Test2UITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test3/Test3Tests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test3/Test3UITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test4/Test4Tests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test4/Test4UITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Apps/Sources/UI/Link.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Link.swift 3 | // LocalizationHelper 4 | // 5 | // Created by Chung Tran on 14/01/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct Link: View { 11 | let url: String 12 | let description: String? 13 | 14 | var body: some View { 15 | Button(action: { 16 | if let url = URL(string: url) { 17 | NSWorkspace.shared.open(url) 18 | } 19 | }) { 20 | Text(description ?? url).underline() 21 | .foregroundColor(Color.blue) 22 | } 23 | .buttonStyle(PlainButtonStyle()) 24 | .onHover { inside in 25 | if inside { 26 | NSCursor.pointingHand.push() 27 | } else { 28 | NSCursor.pop() 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Demo/.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | .idea/ 9 | 10 | ## Various settings 11 | *.pbxuser 12 | !default.pbxuser 13 | *.mode1v3 14 | !default.mode1v3 15 | *.mode2v3 16 | !default.mode2v3 17 | *.perspectivev3 18 | !default.perspectivev3 19 | xcuserdata/ 20 | 21 | ## Other 22 | *.moved-aside 23 | *.xccheckout 24 | *.xcscmblueprint 25 | 26 | ## Obj-C/Swift specific 27 | *.hmap 28 | *.ipa 29 | *.dSYM.zip 30 | *.dSYM 31 | 32 | ## Playgrounds 33 | timeline.xctimeline 34 | playground.xcworkspace 35 | 36 | # Swift Package Manager 37 | # 38 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 39 | # Packages/ 40 | # Package.pins 41 | # Package.resolved 42 | .build/ 43 | 44 | -------------------------------------------------------------------------------- /Kit/Sources/EventMonitor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EventMonitor.swift 3 | // LocalizationHelper 4 | // 5 | // Created by Chung Tran on 14/01/2021. 6 | // 7 | 8 | import Cocoa 9 | 10 | public class EventMonitor { 11 | private var monitor: Any? 12 | private let mask: NSEvent.EventTypeMask 13 | private let handler: (NSEvent?) -> Void 14 | 15 | public init(mask: NSEvent.EventTypeMask, handler: @escaping (NSEvent?) -> Void) { 16 | self.mask = mask 17 | self.handler = handler 18 | } 19 | 20 | deinit { 21 | stop() 22 | } 23 | 24 | public func start() { 25 | monitor = NSEvent.addGlobalMonitorForEvents(matching: mask, handler: handler) as! NSObject 26 | } 27 | 28 | public func stop() { 29 | if monitor != nil { 30 | NSEvent.removeMonitor(monitor!) 31 | monitor = nil 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Apps/Resources/Colors.xcassets/customGray.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xBA", 9 | "green" : "0xA5", 10 | "red" : "0xA3" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "1.000", 27 | "green" : "1.000", 28 | "red" : "1.000" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Apps/Sources/Handlers/OpenProjectHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OpenProjectHandler.swift 3 | // LocalizationHelperKit 4 | // 5 | // Created by Chung Tran on 25/07/2021. 6 | // 7 | 8 | import Foundation 9 | import XcodeProj 10 | import PathKit 11 | 12 | protocol OpenProjectHandler { 13 | func openProject(_ project: Project) throws 14 | } 15 | 16 | extension OpenProjectHandler { 17 | func openDefaultProject(xcodeproj: XcodeProj, targetName: String, path: Path) throws { 18 | guard let target = xcodeproj.pbxproj.targets(named: targetName).first 19 | else { 20 | throw LocalizationHelperError.targetNotFound 21 | } 22 | try openProject(.default(.init(pxbproj: xcodeproj, target: target, path: path))) 23 | } 24 | } 25 | 26 | #if DEBUG 27 | struct OpenProjectHandler_Preview: OpenProjectHandler { 28 | func openProject(_ project: Project) throws { 29 | // do nothing 30 | } 31 | } 32 | #endif 33 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test1/Test1Tests/Test1Tests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Test1Tests.swift 3 | // Test1Tests 4 | // 5 | // Created by Chung Tran on 20/07/2021. 6 | // 7 | 8 | import XCTest 9 | @testable import Test1 10 | 11 | class Test1Tests: XCTestCase { 12 | 13 | override func setUpWithError() throws { 14 | // Put setup code here. This method is called before the invocation of each test method in the class. 15 | } 16 | 17 | override func tearDownWithError() throws { 18 | // Put teardown code here. This method is called after the invocation of each test method in the class. 19 | } 20 | 21 | func testExample() throws { 22 | // This is an example of a functional test case. 23 | // Use XCTAssert and related functions to verify your tests produce the correct results. 24 | } 25 | 26 | func testPerformanceExample() throws { 27 | // This is an example of a performance test case. 28 | self.measure { 29 | // Put the code you want to measure the time of here. 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test2/Test2Tests/Test2Tests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Test2Tests.swift 3 | // Test2Tests 4 | // 5 | // Created by Chung Tran on 20/07/2021. 6 | // 7 | 8 | import XCTest 9 | @testable import Test2 10 | 11 | class Test2Tests: XCTestCase { 12 | 13 | override func setUpWithError() throws { 14 | // Put setup code here. This method is called before the invocation of each test method in the class. 15 | } 16 | 17 | override func tearDownWithError() throws { 18 | // Put teardown code here. This method is called after the invocation of each test method in the class. 19 | } 20 | 21 | func testExample() throws { 22 | // This is an example of a functional test case. 23 | // Use XCTAssert and related functions to verify your tests produce the correct results. 24 | } 25 | 26 | func testPerformanceExample() throws { 27 | // This is an example of a performance test case. 28 | self.measure { 29 | // Put the code you want to measure the time of here. 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test3/Test3Tests/Test3Tests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Test3Tests.swift 3 | // Test3Tests 4 | // 5 | // Created by Chung Tran on 20/07/2021. 6 | // 7 | 8 | import XCTest 9 | @testable import Test3 10 | 11 | class Test3Tests: XCTestCase { 12 | 13 | override func setUpWithError() throws { 14 | // Put setup code here. This method is called before the invocation of each test method in the class. 15 | } 16 | 17 | override func tearDownWithError() throws { 18 | // Put teardown code here. This method is called after the invocation of each test method in the class. 19 | } 20 | 21 | func testExample() throws { 22 | // This is an example of a functional test case. 23 | // Use XCTAssert and related functions to verify your tests produce the correct results. 24 | } 25 | 26 | func testPerformanceExample() throws { 27 | // This is an example of a performance test case. 28 | self.measure { 29 | // Put the code you want to measure the time of here. 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test4/Test4Tests/Test4Tests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Test4Tests.swift 3 | // Test4Tests 4 | // 5 | // Created by Chung Tran on 20/07/2021. 6 | // 7 | 8 | import XCTest 9 | @testable import Test4 10 | 11 | class Test4Tests: XCTestCase { 12 | 13 | override func setUpWithError() throws { 14 | // Put setup code here. This method is called before the invocation of each test method in the class. 15 | } 16 | 17 | override func tearDownWithError() throws { 18 | // Put teardown code here. This method is called after the invocation of each test method in the class. 19 | } 20 | 21 | func testExample() throws { 22 | // This is an example of a functional test case. 23 | // Use XCTAssert and related functions to verify your tests produce the correct results. 24 | } 25 | 26 | func testPerformanceExample() throws { 27 | // This is an example of a performance test case. 28 | self.measure { 29 | // Put the code you want to measure the time of here. 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /Apps/TestsProjects/TestWithTuist/Project.swift: -------------------------------------------------------------------------------- 1 | import ProjectDescription 2 | import ProjectDescriptionHelpers 3 | 4 | /* 5 | +-------------+ 6 | | | 7 | | App | Contains TestWithTuist App target and TestWithTuist unit-test target 8 | | | 9 | +------+-------------+-------+ 10 | | depends on | 11 | | | 12 | +----v-----+ +-----v-----+ 13 | | | | | 14 | | Kit | | UI | Two independent frameworks to share code and start modularising your app 15 | | | | | 16 | +----------+ +-----------+ 17 | 18 | */ 19 | 20 | // MARK: - Project 21 | 22 | // Creates our project using a helper function defined in ProjectDescriptionHelpers 23 | let project = Project.app(name: "TestWithTuist", 24 | platform: .iOS, 25 | additionalTargets: ["TestWithTuistKit", "TestWithTuistUI"]) 26 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Othneil Drew 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Apps/Sources/Injected/App+Resolver.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Resolver.swift 3 | // LocalizationHelperKit 4 | // 5 | // Created by Chung Tran on 24/07/2021. 6 | // 7 | 8 | @_exported import Resolver 9 | 10 | extension Resolver: ResolverRegistering { 11 | #if DEBUG 12 | static let mock = Resolver(parent: main) 13 | #endif 14 | 15 | public static func registerAllServices() { 16 | register {StringsFileGenerator() as FileGeneratorType} 17 | register {UserDefaultsProjectRepository() as ProjectRepositoryType} 18 | register {FilePickerService() as FilePickerServiceType} 19 | register {XCodeProjectService() as XCodeProjectServiceType} 20 | 21 | #if DEBUG 22 | // mock.register {FakeStringsFileGenerator() as FileGeneratorType} 23 | // mock.register {InMemoryProjectRepository.default as ProjectRepositoryType} 24 | // mock.register {MockFilePickerService() as FilePickerServiceType} 25 | // mock.register {OpenProjectHandler_Preview() as OpenProjectHandler} 26 | 27 | // register entire container as replacement for main 28 | // root = mock 29 | #endif 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Apps/Sources/System/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // LocalizationHelper 4 | // 5 | // Created by Chung Tran on 10/17/20. 6 | // 7 | 8 | import Cocoa 9 | import SwiftUI 10 | 11 | @NSApplicationMain 12 | class AppDelegate: NSObject, NSApplicationDelegate { 13 | 14 | var window: NSWindow! 15 | var statusBar: StatusBarController? 16 | var popover = NSPopover() 17 | 18 | func applicationDidFinishLaunching(_ aNotification: Notification) { 19 | // Create the SwiftUI view that provides the window contents. 20 | let viewModel = MainViewModel() 21 | let contentView = MainView(viewModel: viewModel) 22 | 23 | // Set the SwiftUI's ContentView to the Popover's ContentViewController 24 | popover.contentSize = NSSize(width: 680, height: 300) 25 | popover.contentViewController = NSHostingController(rootView: contentView) 26 | 27 | // Initialising the status bar 28 | statusBar = StatusBarController(popover) { [weak viewModel] in 29 | viewModel?.refresh() 30 | } 31 | } 32 | 33 | func applicationWillTerminate(_ aNotification: Notification) { 34 | // Insert code here to tear down your application 35 | } 36 | } 37 | 38 | -------------------------------------------------------------------------------- /Apps/Tests/Utilities/TestProjectRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestProjectRepository.swift 3 | // LocalizationHelperTests 4 | // 5 | // Created by Chung Tran on 20/07/2021. 6 | // 7 | 8 | import Foundation 9 | @testable import LocalizationHelper 10 | import PathKit 11 | 12 | struct TestProjectRepository: ProjectRepositoryType { 13 | let testName: String 14 | 15 | func getCurrentProject() -> Project? { 16 | switch testName { 17 | case let testName where testName.starts(with: "TestWithTuist"): 18 | // TuistProject 19 | let path = Path(homeUrl) + testName + "Targets" + testName + "Resources" 20 | return .tuist(.init(path: path, resourcePath: path, projectName: testName)) 21 | case let testName where testName.starts(with: "Test"): 22 | // DefaultProject 23 | let proj = try! getXcodeProj(fileName: testName) 24 | let target = proj.pbxproj.targets(named: testName).first! 25 | return .default(.init(pxbproj: proj, target: target, path: Path(xcodeprojPath(fileName: testName)))) 26 | default: 27 | return nil 28 | } 29 | } 30 | 31 | func setCurrentProject(_ project: Project) {} 32 | 33 | func clearCurrentProject() {} 34 | } 35 | -------------------------------------------------------------------------------- /Apps/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 2.3 19 | CFBundleVersion 20 | 1 21 | LSMinimumSystemVersion 22 | 12.0 23 | NSHumanReadableCopyright 24 | Copyright ©. All rights reserved. 25 | NSPrincipalClass 26 | LocalizationHelper.Application 27 | LSApplicationCategoryType 28 | public.app-category.developer-tools 29 | UILaunchScreen 30 | 31 | LSUIElement 32 | 33 | NSMainStoryboardFile 34 | Main 35 | 36 | 37 | -------------------------------------------------------------------------------- /Kit/Tests/ConfigRepositoryTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConfigRepositoryTests.swift 3 | // LocalizationHelperKitTests 4 | // 5 | // Created by Chung Tran on 26/04/2023. 6 | // 7 | 8 | import XCTest 9 | import LocalizationHelperKit 10 | 11 | final class ConfigRepositoryTests: XCTestCase { 12 | 13 | var repository: ConfigRepositoryImpl! 14 | 15 | override func setUpWithError() throws { 16 | repository = .init(projectDir: "/Users/chungtran/documents/ios/p2p-wallet-ios") 17 | } 18 | 19 | override func tearDownWithError() throws { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | } 22 | 23 | func testGetConfig() async throws { 24 | let config = try await repository.getConfig() 25 | XCTAssertEqual(config.automation.script, "swiftgen config run --config ${PROJECT_DIR}/swiftgen.yml") 26 | XCTAssertEqual(config.automation.pathType, .absolute) 27 | } 28 | 29 | func testSaveConfig() async throws { 30 | try await repository.saveConfig( 31 | .init( 32 | automation: .init( 33 | script: "swiftgen config run --config swiftgen.yml", 34 | pathType: .relative 35 | ) 36 | ) 37 | ) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Apps/Resources/Base.lproj/Localizable.stringsdict: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | %d hidden wallet 6 | 7 | NSStringLocalizedFormatKey 8 | %#@variable_0@ 9 | variable_0 10 | 11 | NSStringFormatSpecTypeKey 12 | NSStringPluralRuleType 13 | NSStringFormatValueTypeKey 14 | d 15 | one 16 | %d hidden wallet 17 | other 18 | %d hidden wallets 19 | 20 | 21 | Wrong Pin-code, %d attempt(s) left 22 | 23 | NSStringLocalizedFormatKey 24 | %#@variable_0@ 25 | variable_0 26 | 27 | NSStringFormatSpecTypeKey 28 | NSStringPluralRuleType 29 | NSStringFormatValueTypeKey 30 | d 31 | one 32 | Wrong Pin-code, %d attempt left 33 | other 34 | Wrong Pin-code, %d attempts left 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /Apps/Resources/en.lproj/Localizable.stringsdict: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | %d hidden wallet 6 | 7 | NSStringLocalizedFormatKey 8 | %#@variable_0@ 9 | variable_0 10 | 11 | NSStringFormatSpecTypeKey 12 | NSStringPluralRuleType 13 | NSStringFormatValueTypeKey 14 | d 15 | one 16 | %d hidden wallet 17 | other 18 | %d hidden wallets 19 | 20 | 21 | Wrong Pin-code, %d attempt(s) left 22 | 23 | NSStringLocalizedFormatKey 24 | %#@variable_0@ 25 | variable_0 26 | 27 | NSStringFormatSpecTypeKey 28 | NSStringPluralRuleType 29 | NSStringFormatValueTypeKey 30 | d 31 | one 32 | Wrong Pin-code, %d attempt left 33 | other 34 | Wrong Pin-code, %d attempts left 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /Apps/Tests/Extensions/PBXGroupTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PBXGroupTests.swift 3 | // LocalizationHelperKitTests 4 | // 5 | // Created by Chung Tran on 20/07/2021. 6 | // 7 | 8 | import XCTest 9 | @testable import LocalizationHelper 10 | 11 | class PBXGroupTests: XCTestCase { 12 | 13 | func testGetGroupRecursively1() throws { 14 | let project = try getXcodeProj(fileName: "Test1") 15 | 16 | let group = project.pbxproj.rootObject?.mainGroup 17 | .group(named: LOCALIZABLE_STRINGS, recursively: true) 18 | XCTAssertNotNil(group) 19 | } 20 | 21 | func testGetGroupRecursively2() throws { 22 | let project = try getXcodeProj(fileName: "Test2") 23 | 24 | let group = project.pbxproj.rootObject?.mainGroup 25 | .group(named: LOCALIZABLE_STRINGS, recursively: true) 26 | XCTAssertNotNil(group) 27 | } 28 | 29 | func testGetGroupRecursively3() throws { 30 | let project = try getXcodeProj(fileName: "Test3") 31 | 32 | let group = project.pbxproj.rootObject?.mainGroup 33 | .group(named: LOCALIZABLE_STRINGS, recursively: true) 34 | XCTAssertNotNil(group) 35 | } 36 | 37 | func testGetGroupRecursively4() throws { 38 | let project = try getXcodeProj(fileName: "Test4") 39 | 40 | let group = project.pbxproj.rootObject?.mainGroup 41 | .group(named: LOCALIZABLE_STRINGS, recursively: true) 42 | XCTAssertNil(group) 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### macOS ### 2 | # General 3 | .DS_Store 4 | .AppleDouble 5 | .LSOverride 6 | 7 | # Icon must end with two 8 | Icon 9 | 10 | # Thumbnails 11 | ._* 12 | 13 | # Files that might appear in the root of a volume 14 | .DocumentRevisions-V100 15 | .fseventsd 16 | .Spotlight-V100 17 | .TemporaryItems 18 | .Trashes 19 | .VolumeIcon.icns 20 | .com.apple.timemachine.donotpresent 21 | 22 | # Directories potentially created on remote AFP share 23 | .AppleDB 24 | .AppleDesktop 25 | Network Trash Folder 26 | Temporary Items 27 | .apdisk 28 | 29 | ### Xcode ### 30 | # Xcode 31 | # 32 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 33 | 34 | ## User settings 35 | xcuserdata/ 36 | 37 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 38 | *.xcscmblueprint 39 | *.xccheckout 40 | 41 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 42 | build/ 43 | DerivedData/ 44 | *.moved-aside 45 | *.pbxuser 46 | !default.pbxuser 47 | *.mode1v3 48 | !default.mode1v3 49 | *.mode2v3 50 | !default.mode2v3 51 | *.perspectivev3 52 | !default.perspectivev3 53 | 54 | ### Xcode Patch ### 55 | *.xcodeproj/* 56 | !*.xcodeproj/project.pbxproj 57 | !*.xcodeproj/xcshareddata/ 58 | !*.xcworkspace/contents.xcworkspacedata 59 | /*.gcno 60 | 61 | ### Projects ### 62 | /*.xcodeproj 63 | /*.xcworkspace 64 | 65 | ### Tuist derived files ### 66 | graph.dot 67 | Derived/ 68 | 69 | ### Tuist managed dependencies ### 70 | Tuist/Dependencies 71 | -------------------------------------------------------------------------------- /Apps/TestsProjects/TestWithTuist/.gitignore: -------------------------------------------------------------------------------- 1 | ### macOS ### 2 | # General 3 | .DS_Store 4 | .AppleDouble 5 | .LSOverride 6 | 7 | # Icon must end with two 8 | Icon 9 | 10 | # Thumbnails 11 | ._* 12 | 13 | # Files that might appear in the root of a volume 14 | .DocumentRevisions-V100 15 | .fseventsd 16 | .Spotlight-V100 17 | .TemporaryItems 18 | .Trashes 19 | .VolumeIcon.icns 20 | .com.apple.timemachine.donotpresent 21 | 22 | # Directories potentially created on remote AFP share 23 | .AppleDB 24 | .AppleDesktop 25 | Network Trash Folder 26 | Temporary Items 27 | .apdisk 28 | 29 | ### Xcode ### 30 | # Xcode 31 | # 32 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 33 | 34 | ## User settings 35 | xcuserdata/ 36 | 37 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 38 | *.xcscmblueprint 39 | *.xccheckout 40 | 41 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 42 | build/ 43 | DerivedData/ 44 | *.moved-aside 45 | *.pbxuser 46 | !default.pbxuser 47 | *.mode1v3 48 | !default.mode1v3 49 | *.mode2v3 50 | !default.mode2v3 51 | *.perspectivev3 52 | !default.perspectivev3 53 | 54 | ### Xcode Patch ### 55 | *.xcodeproj/* 56 | !*.xcodeproj/project.pbxproj 57 | !*.xcodeproj/xcshareddata/ 58 | !*.xcworkspace/contents.xcworkspacedata 59 | /*.gcno 60 | 61 | ### Projects ### 62 | *.xcodeproj 63 | *.xcworkspace 64 | 65 | ### Tuist derived files ### 66 | graph.dot 67 | Derived/ 68 | 69 | ### Tuist managed dependencies ### 70 | Tuist/Dependencies 71 | -------------------------------------------------------------------------------- /Apps/Sources/UI/OpenProject/OpenProjectView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OpenProjectView.swift 3 | // LocalizationHelper 4 | // 5 | // Created by Chung Tran on 24/07/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct OpenProjectView: View { 11 | // MARK: - Nested type 12 | enum ProjectType: String { 13 | case `default` 14 | case tuist 15 | } 16 | 17 | // MARK: - State 18 | @State private var projectType = ProjectType.default 19 | let handler: OpenProjectHandler 20 | 21 | // MARK: - Body 22 | var body: some View { 23 | Group { 24 | Picker("", selection: $projectType) { 25 | Text("Default project").tag(ProjectType.default) 26 | Text("Tuist project").tag(ProjectType.tuist) 27 | } 28 | .pickerStyle(SegmentedPickerStyle()) 29 | .padding() 30 | 31 | content 32 | .padding() 33 | 34 | Spacer() 35 | } 36 | 37 | } 38 | 39 | var content: AnyView { 40 | switch projectType { 41 | case .default: 42 | return AnyView(DefaultProjectView(handler: handler)) 43 | case .tuist: 44 | return AnyView(TuistProjectView(handler: handler)) 45 | } 46 | } 47 | } 48 | 49 | #if DEBUG 50 | struct OpenProjectView_Previews: PreviewProvider { 51 | static var previews: some View { 52 | OpenProjectView(handler: OpenProjectHandler_Preview()) 53 | .frame(width: 500, height: 500) 54 | } 55 | } 56 | #endif 57 | -------------------------------------------------------------------------------- /Apps/Resources/macOS.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "16.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "32.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "32-1.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "64.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "128.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "256.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "256-1.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "512.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "New Project (11)-1.png", 53 | "idiom" : "mac", 54 | "scale" : "1x", 55 | "size" : "512x512" 56 | }, 57 | { 58 | "filename" : "New Project (12).png", 59 | "idiom" : "mac", 60 | "scale" : "2x", 61 | "size" : "512x512" 62 | } 63 | ], 64 | "info" : { 65 | "author" : "xcode", 66 | "version" : 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Apps/Sources/UI/Main/MainViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainViewModel.swift 3 | // LocalizationHelper 4 | // 5 | // Created by Chung Tran on 24/07/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | @MainActor 11 | class MainViewModel: ObservableObject { 12 | // MARK: - Dependencies 13 | @Injected var projectService: XCodeProjectServiceType 14 | 15 | // MARK: - Properties 16 | @Published var appState: AppState = .initial 17 | var projectViewModel: ProjectViewModel! 18 | 19 | init() { 20 | try? openCurrentProject() 21 | } 22 | 23 | // MARK: - Methods 24 | func openCurrentProject() throws { 25 | let project = try projectService.openCurrentProject() 26 | var appState = appState 27 | appState.project = project 28 | self.appState = appState 29 | } 30 | 31 | func openProject(_ project: Project) throws { 32 | let project = try projectService.openProject(project) 33 | var appState = appState 34 | appState.project = project 35 | self.appState = appState 36 | } 37 | 38 | func closeProject() { 39 | guard let project = appState.project else {return} 40 | projectService.closeProject(project) 41 | var appState = appState 42 | appState.project = nil 43 | self.appState = appState 44 | } 45 | 46 | func refresh() { 47 | guard appState.project != nil else { 48 | return 49 | } 50 | projectViewModel?.refresh() 51 | } 52 | } 53 | 54 | extension MainViewModel: OpenProjectHandler {} 55 | -------------------------------------------------------------------------------- /Apps/Sources/Extensions/XcodeProj+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XcodeProj+Extensions.swift 3 | // LocalizationHelperKit 4 | // 5 | // Created by Chung Tran on 20/07/2021. 6 | // 7 | 8 | import Foundation 9 | import XcodeProj 10 | import PathKit 11 | 12 | extension PBXGroup { 13 | /// Returns group with the given name contained in the project. 14 | /// - Parameters: 15 | /// - name: group's name 16 | /// - recursively: should find recursively or not 17 | /// - Returns: group with the given name in the project 18 | func group(named name: String, recursively: Bool) -> PBXGroup? { 19 | // Non-recursively 20 | if !recursively { 21 | return group(named: name) 22 | } 23 | 24 | // If found 25 | if let group = group(named: name) { 26 | return group 27 | } 28 | 29 | // recursively find group 30 | for child in children where child is PBXGroup { 31 | if let group = (child as? PBXGroup)?.group(named: name, recursively: true) { 32 | return group 33 | } 34 | } 35 | 36 | return nil 37 | } 38 | } 39 | 40 | #if DEBUG 41 | extension XcodeProj { 42 | static var demoProject: (XcodeProj?, Path) { 43 | let repositoryLocalURL = "/Users/bigears/Documents/macos/XCodeLocalizationHelper" 44 | let homeUrl = Path(repositoryLocalURL + "/Apps/TestsProjects/") 45 | let test1 = homeUrl + "Test1" + "Test1.xcodeproj" 46 | 47 | return (try? XcodeProj(path: test1), test1) 48 | } 49 | } 50 | #endif 51 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test1/Test1UITests/Test1UITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Test1UITests.swift 3 | // Test1UITests 4 | // 5 | // Created by Chung Tran on 20/07/2021. 6 | // 7 | 8 | import XCTest 9 | 10 | class Test1UITests: XCTestCase { 11 | 12 | override func setUpWithError() throws { 13 | // Put setup code here. This method is called before the invocation of each test method in the class. 14 | 15 | // In UI tests it is usually best to stop immediately when a failure occurs. 16 | continueAfterFailure = false 17 | 18 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 19 | } 20 | 21 | override func tearDownWithError() throws { 22 | // Put teardown code here. This method is called after the invocation of each test method in the class. 23 | } 24 | 25 | func testExample() throws { 26 | // UI tests must launch the application that they test. 27 | let app = XCUIApplication() 28 | app.launch() 29 | 30 | // Use recording to get started writing UI tests. 31 | // Use XCTAssert and related functions to verify your tests produce the correct results. 32 | } 33 | 34 | func testLaunchPerformance() throws { 35 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { 36 | // This measures how long it takes to launch your application. 37 | measure(metrics: [XCTApplicationLaunchMetric()]) { 38 | XCUIApplication().launch() 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test2/Test2UITests/Test2UITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Test2UITests.swift 3 | // Test2UITests 4 | // 5 | // Created by Chung Tran on 20/07/2021. 6 | // 7 | 8 | import XCTest 9 | 10 | class Test2UITests: XCTestCase { 11 | 12 | override func setUpWithError() throws { 13 | // Put setup code here. This method is called before the invocation of each test method in the class. 14 | 15 | // In UI tests it is usually best to stop immediately when a failure occurs. 16 | continueAfterFailure = false 17 | 18 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 19 | } 20 | 21 | override func tearDownWithError() throws { 22 | // Put teardown code here. This method is called after the invocation of each test method in the class. 23 | } 24 | 25 | func testExample() throws { 26 | // UI tests must launch the application that they test. 27 | let app = XCUIApplication() 28 | app.launch() 29 | 30 | // Use recording to get started writing UI tests. 31 | // Use XCTAssert and related functions to verify your tests produce the correct results. 32 | } 33 | 34 | func testLaunchPerformance() throws { 35 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { 36 | // This measures how long it takes to launch your application. 37 | measure(metrics: [XCTApplicationLaunchMetric()]) { 38 | XCUIApplication().launch() 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test3/Test3UITests/Test3UITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Test3UITests.swift 3 | // Test3UITests 4 | // 5 | // Created by Chung Tran on 20/07/2021. 6 | // 7 | 8 | import XCTest 9 | 10 | class Test3UITests: XCTestCase { 11 | 12 | override func setUpWithError() throws { 13 | // Put setup code here. This method is called before the invocation of each test method in the class. 14 | 15 | // In UI tests it is usually best to stop immediately when a failure occurs. 16 | continueAfterFailure = false 17 | 18 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 19 | } 20 | 21 | override func tearDownWithError() throws { 22 | // Put teardown code here. This method is called after the invocation of each test method in the class. 23 | } 24 | 25 | func testExample() throws { 26 | // UI tests must launch the application that they test. 27 | let app = XCUIApplication() 28 | app.launch() 29 | 30 | // Use recording to get started writing UI tests. 31 | // Use XCTAssert and related functions to verify your tests produce the correct results. 32 | } 33 | 34 | func testLaunchPerformance() throws { 35 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { 36 | // This measures how long it takes to launch your application. 37 | measure(metrics: [XCTApplicationLaunchMetric()]) { 38 | XCUIApplication().launch() 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test4/Test4UITests/Test4UITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Test4UITests.swift 3 | // Test4UITests 4 | // 5 | // Created by Chung Tran on 20/07/2021. 6 | // 7 | 8 | import XCTest 9 | 10 | class Test4UITests: XCTestCase { 11 | 12 | override func setUpWithError() throws { 13 | // Put setup code here. This method is called before the invocation of each test method in the class. 14 | 15 | // In UI tests it is usually best to stop immediately when a failure occurs. 16 | continueAfterFailure = false 17 | 18 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 19 | } 20 | 21 | override func tearDownWithError() throws { 22 | // Put teardown code here. This method is called after the invocation of each test method in the class. 23 | } 24 | 25 | func testExample() throws { 26 | // UI tests must launch the application that they test. 27 | let app = XCUIApplication() 28 | app.launch() 29 | 30 | // Use recording to get started writing UI tests. 31 | // Use XCTAssert and related functions to verify your tests produce the correct results. 32 | } 33 | 34 | func testLaunchPerformance() throws { 35 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { 36 | // This measures how long it takes to launch your application. 37 | measure(metrics: [XCTApplicationLaunchMetric()]) { 38 | XCUIApplication().launch() 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Apps/Sources/Services/XCodeProjectService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XCodeProjectService.swift 3 | // LocalizationHelperKit 4 | // 5 | // Created by Chung Tran on 25/07/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol XCodeProjectServiceType { 11 | func openCurrentProject() throws -> Project 12 | func openProject(_ project: Project) throws -> Project 13 | func localizeProject(_ project: Project, languageCode: String) throws 14 | func getLocalizableFiles(fromProject project: Project) throws -> [LocalizableFile] 15 | func closeProject(_ project: Project) 16 | } 17 | 18 | struct XCodeProjectService: XCodeProjectServiceType { 19 | // MARK: - Dependencies 20 | @Injected var stringsFileGenerator: FileGeneratorType 21 | @Injected var projectRepository: ProjectRepositoryType 22 | 23 | // MARK: - Methods 24 | func openCurrentProject() throws -> Project { 25 | guard let project = projectRepository.getCurrentProject() 26 | else {throw LocalizationHelperError.projectNotFound} 27 | return try openProject(project) 28 | } 29 | 30 | func openProject(_ project: Project) throws -> Project { 31 | projectRepository.setCurrentProject(project) 32 | return project 33 | } 34 | 35 | func localizeProject(_ project: Project, languageCode: String) throws { 36 | try project.localize(fileGenerator: stringsFileGenerator, languageCode: languageCode) 37 | } 38 | 39 | func getLocalizableFiles(fromProject project: Project) throws -> [LocalizableFile] { 40 | try project.getLocalizableFiles() 41 | } 42 | 43 | func closeProject(_ project: Project) { 44 | projectRepository.clearCurrentProject() 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Apps/Sources/Utilities/Store.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Store.swift 3 | // LocalizationHelperKit 4 | // 5 | // Created by Chung Tran on 19/07/2021. 6 | // 7 | 8 | import SwiftUI 9 | import Combine 10 | 11 | typealias Store = CurrentValueSubject 12 | 13 | extension Store { 14 | 15 | subscript(keyPath: WritableKeyPath) -> T where T: Equatable { 16 | get { value[keyPath: keyPath] } 17 | set { 18 | var value = self.value 19 | if value[keyPath: keyPath] != newValue { 20 | value[keyPath: keyPath] = newValue 21 | self.value = value 22 | } 23 | } 24 | } 25 | 26 | func bulkUpdate(_ update: (inout Output) -> Void) { 27 | var value = self.value 28 | update(&value) 29 | self.value = value 30 | } 31 | 32 | func updates(for keyPath: KeyPath) -> 33 | AnyPublisher where Value: Equatable { 34 | return map(keyPath).removeDuplicates().eraseToAnyPublisher() 35 | } 36 | } 37 | 38 | // MARK: - 39 | extension Binding where Value: Equatable { 40 | func dispatched(to state: Store, 41 | _ keyPath: WritableKeyPath) -> Self { 42 | return onSet { state[keyPath] = $0 } 43 | } 44 | } 45 | 46 | extension Binding { 47 | typealias ValueClosure = (Value) -> Void 48 | 49 | func onSet(_ perform: @escaping ValueClosure) -> Self { 50 | return .init(get: { () -> Value in 51 | self.wrappedValue 52 | }, set: { value in 53 | self.wrappedValue = value 54 | perform(value) 55 | }) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Apps/Resources/ru.lproj/Localizable.stringsdict: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | %d hidden wallet 6 | 7 | NSStringLocalizedFormatKey 8 | %#@variable_0@ 9 | variable_0 10 | 11 | NSStringFormatSpecTypeKey 12 | NSStringPluralRuleType 13 | NSStringFormatValueTypeKey 14 | d 15 | zero 16 | %d скрытых кошельков 17 | one 18 | %d скрытый кошелек 19 | two 20 | %d скрытых кошелька 21 | few 22 | %d скрытых кошелька 23 | many 24 | %d скрытых кошельков 25 | other 26 | %d скрытых кошельков 27 | 28 | 29 | Wrong Pin-code, %d attempt(s) left 30 | 31 | NSStringLocalizedFormatKey 32 | %#@variable_0@ 33 | variable_0 34 | 35 | NSStringFormatSpecTypeKey 36 | NSStringPluralRuleType 37 | NSStringFormatValueTypeKey 38 | d 39 | zero 40 | Неверный PIN-код, осталось %d попыток 41 | one 42 | Неверный PIN-код, осталась %d попытка 43 | few 44 | Неверный PIN-код, осталось %d попытки 45 | other 46 | Неверный PIN-код, осталось %d попыток 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test1/Test1/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test2/Test2/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test3/Test3/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test4/Test4/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Apps/Sources/Services/StringsFileGenerator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StringsFileGenerator.swift 3 | // LocalizationHelperKit 4 | // 5 | // Created by Chung Tran on 20/07/2021. 6 | // 7 | 8 | import Foundation 9 | import PathKit 10 | 11 | protocol FileGeneratorType { 12 | func pathExists(path: Path) -> Bool 13 | func createFolder(path: Path) throws 14 | func write(file: Path, content: String) throws 15 | } 16 | 17 | extension FileGeneratorType { 18 | func generateFile(at path: Path, fileName: String, content: String) throws { 19 | let folder = path 20 | let file = folder + fileName 21 | 22 | if !pathExists(path: file) { 23 | if !folder.exists { 24 | try createFolder(path: folder) 25 | } 26 | try write(file: file, content: content) 27 | } 28 | } 29 | } 30 | 31 | struct StringsFileGenerator: FileGeneratorType { 32 | func pathExists(path: Path) -> Bool { 33 | path.exists 34 | } 35 | 36 | func createFolder(path: Path) throws { 37 | try path.mkdir() 38 | } 39 | 40 | func write(file: Path, content: String) throws { 41 | try file.write(content) 42 | } 43 | } 44 | 45 | #if DEBUG 46 | class FakeStringsFileGenerator: FileGeneratorType { 47 | private var generatedFilePaths = [Path]() 48 | 49 | func pathExists(path: Path) -> Bool { 50 | generatedFilePaths.map {$0.parent()}.contains(path) 51 | } 52 | 53 | func createFolder(path: Path) throws { 54 | // do nothing 55 | } 56 | 57 | func write(file: Path, content: String) throws { 58 | // fake writing 59 | if !generatedFilePaths.contains(file) { 60 | generatedFilePaths.append(file) 61 | } 62 | } 63 | } 64 | #endif 65 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test1/Test1/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test2/Test2/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test3/Test3/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test4/Test4/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Demo/LocalizationHelperDemo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | 28 | UIApplicationSupportsIndirectInputEvents 29 | 30 | UILaunchScreen 31 | 32 | UIRequiredDeviceCapabilities 33 | 34 | armv7 35 | 36 | UISupportedInterfaceOrientations 37 | 38 | UIInterfaceOrientationPortrait 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UISupportedInterfaceOrientations~ipad 43 | 44 | UIInterfaceOrientationPortrait 45 | UIInterfaceOrientationPortraitUpsideDown 46 | UIInterfaceOrientationLandscapeLeft 47 | UIInterfaceOrientationLandscapeRight 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /Apps/TestsProjects/TestWithTuist/Targets/TestWithTuist/Resources/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /.package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "aexml", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/tadija/AEXML", 7 | "state" : { 8 | "revision" : "8623e73b193386909566a9ca20203e33a09af142", 9 | "version" : "4.5.0" 10 | } 11 | }, 12 | { 13 | "identity" : "alamofire", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/Alamofire/Alamofire.git", 16 | "state" : { 17 | "revision" : "f96b619bcb2383b43d898402283924b80e2c4bae", 18 | "version" : "5.4.3" 19 | } 20 | }, 21 | { 22 | "identity" : "pathkit", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/kylef/PathKit", 25 | "state" : { 26 | "revision" : "73f8e9dca9b7a3078cb79128217dc8f2e585a511", 27 | "version" : "1.0.0" 28 | } 29 | }, 30 | { 31 | "identity" : "resolver", 32 | "kind" : "remoteSourceControl", 33 | "location" : "https://github.com/hmlongco/Resolver.git", 34 | "state" : { 35 | "revision" : "48e26b8fdf22877cd89b4a1f3927a56cc9f34d46", 36 | "version" : "1.4.3" 37 | } 38 | }, 39 | { 40 | "identity" : "spectre", 41 | "kind" : "remoteSourceControl", 42 | "location" : "https://github.com/kylef/Spectre.git", 43 | "state" : { 44 | "revision" : "f79d4ecbf8bc4e1579fbd86c3e1d652fb6876c53", 45 | "version" : "0.9.2" 46 | } 47 | }, 48 | { 49 | "identity" : "xcodeproj", 50 | "kind" : "remoteSourceControl", 51 | "location" : "https://github.com/tuist/xcodeproj.git", 52 | "state" : { 53 | "revision" : "0b18c3e7a10c241323397a80cb445051f4494971", 54 | "version" : "8.0.0" 55 | } 56 | } 57 | ], 58 | "version" : 2 59 | } 60 | -------------------------------------------------------------------------------- /Kit/Sources/ConfigManager/ConfigManager.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Combine 3 | 4 | public protocol ConfigManager { 5 | var configPublisher: AnyPublisher { get } 6 | func getConfig() async 7 | func saveCurrentConfig() async 8 | } 9 | 10 | /// Class for managing XCodeLocalizationHelper's config 11 | public final class ConfigManagerImpl: ConfigManager { 12 | // MARK: - Properties 13 | 14 | /// Default config 15 | private let defaultConfig: Config = .init( 16 | automation: .init( 17 | script: "Pods/swiftgen/bin/swiftgen config run --config swiftgen.yml", 18 | pathType: .relative 19 | ) 20 | ) 21 | 22 | /// Current config 23 | private let currentConfigSubject: CurrentValueSubject 24 | 25 | /// Repository 26 | private let repository: ConfigRepository 27 | 28 | // MARK: - Public properties 29 | 30 | public var configPublisher: AnyPublisher { 31 | currentConfigSubject.eraseToAnyPublisher() 32 | } 33 | 34 | // MARK: - Initializer 35 | 36 | public init(repository: ConfigRepository) { 37 | self.repository = repository 38 | currentConfigSubject = .init(defaultConfig) 39 | } 40 | 41 | // MARK: - Methods 42 | 43 | /// Get current project's config or return defaultConfig 44 | public func getConfig() async { 45 | // get project config 46 | if let config = try? await repository.getConfig() { 47 | currentConfigSubject.send(config) 48 | } else { 49 | currentConfigSubject.send(defaultConfig) 50 | await saveCurrentConfig() 51 | } 52 | } 53 | 54 | /// Save current config 55 | public func saveCurrentConfig() async { 56 | try? await repository.saveConfig(currentConfigSubject.value) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Demo/LocalizationHelperDemo/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test1/Test1/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test2/Test2/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test3/Test3/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test4/Test4/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Kit/Tests/StreamReaderTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StreamReaderTests.swift 3 | // LocalizationHelperKitTests 4 | // 5 | // Created by Chung Tran on 15/04/2023. 6 | // 7 | 8 | import XCTest 9 | import LocalizationHelperKit 10 | 11 | final class StreamReaderTests: XCTestCase { 12 | var lines: LineReader! 13 | 14 | override func setUpWithError() throws { 15 | let filePath = Bundle.module 16 | .url(forResource: "Localizable", withExtension: "strings")! 17 | .absoluteString 18 | .replacingOccurrences(of: "file://", with: "") 19 | 20 | lines = .init(path: "/Users/chungtran/Documents/ios/LocalizationHelper/Kit/Tests/Resources/Localizable.strings") 21 | // Put setup code here. This method is called before the invocation of each test method in the class. 22 | } 23 | 24 | override func tearDownWithError() throws { 25 | // Put teardown code here. This method is called after the invocation of each test method in the class. 26 | } 27 | 28 | func testExample() throws { 29 | for line in lines { 30 | print(line) 31 | } 32 | 33 | // var line: String? 34 | // 35 | // repeat { 36 | // print(lines.offset) 37 | // line = lines.nextLine() 38 | // print(lines.length) 39 | // print(line) 40 | // } while !lines.isAtEOF 41 | 42 | // This is an example of a functional test case. 43 | // Use XCTAssert and related functions to verify your tests produce the correct results. 44 | // Any test you write for XCTest can be annotated as throws and async. 45 | // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. 46 | // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. 47 | } 48 | 49 | func testPerformanceExample() throws { 50 | // This is an example of a performance test case. 51 | self.measure { 52 | // Put the code you want to measure the time of here. 53 | } 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /ruby/index.rb: -------------------------------------------------------------------------------- 1 | require 'xcodeproj' 2 | require 'pathname' 3 | require 'fileutils' 4 | 5 | LOCALIZABLE_STRINGS = "Localizable.strings" 6 | 7 | # utilities 8 | def add_locale(project_dir, project_name, code, localizable_group) 9 | lproj_folder = project_dir + "/" + project_name + "/" + code + ".lproj" 10 | # create .lproj folder 11 | if !File.exists?(lproj_folder + "/" + LOCALIZABLE_STRINGS) 12 | if !Dir.exists?(lproj_folder) 13 | FileUtils.mkdir_p lproj_folder 14 | end 15 | # create file 16 | FileUtils.cp("./" + LOCALIZABLE_STRINGS, lproj_folder + "/" + LOCALIZABLE_STRINGS) 17 | string_file = File.join(code + ".lproj", LOCALIZABLE_STRINGS) 18 | localizable_group.new_reference(string_file) 19 | puts "Add localization for: " + code 20 | end 21 | end 22 | 23 | 24 | # verify arguments 25 | if ARGV.length != 2 26 | puts "We need exactly 2 arguments that is the path to your xcode project and location code" 27 | exit 28 | end 29 | 30 | # open project 31 | project = Xcodeproj::Project.open(ARGV[0]) 32 | 33 | # get arguments 34 | location_code = ARGV[1] 35 | root_object = project.root_object 36 | project_dir = Pathname(ARGV[0]).dirname.to_s 37 | project_name = root_object.name 38 | target = project.targets.first 39 | 40 | 41 | # add region 42 | known_regions = root_object.known_regions 43 | if !known_regions.include?("en") 44 | known_regions.push("en") 45 | end 46 | if !known_regions.include?(location_code) 47 | known_regions.push(location_code) 48 | end 49 | 50 | # add locale 51 | localizable_group = project.main_group[project_name].children.find {|e| e.name == LOCALIZABLE_STRINGS } 52 | if localizable_group.nil? 53 | localizable_group = project.main_group[project_name].new_variant_group("Localizable.strings") 54 | puts "Created Localizable.strings" 55 | localizable_group.path = "" 56 | target.add_file_references([localizable_group]) 57 | end 58 | 59 | add_locale(project_dir, project_name, "en", localizable_group) 60 | add_locale(project_dir, project_name, location_code, localizable_group) 61 | 62 | # set flag 63 | target.build_configurations.each do |config| 64 | config.build_settings['CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED'] = 'YES' 65 | end 66 | 67 | project.save -------------------------------------------------------------------------------- /Apps/Sources/Models/LocalizableFile.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocalizableFile.swift 3 | // LocalizationHelper 4 | // 5 | // Created by Chung Tran on 12/01/2021. 6 | // 7 | 8 | import PathKit 9 | import Foundation 10 | 11 | public struct LocalizableFile: Identifiable { 12 | public struct Content: Identifiable { 13 | public let offset: Int 14 | public let length: Int 15 | 16 | public let undefinedString: String? 17 | 18 | public let key: String? 19 | public let value: String? 20 | 21 | public var id: String {undefinedString ?? key ?? "\(offset)"} 22 | 23 | init(offset: Int, length: Int, undefinedString: String) { 24 | self.offset = offset 25 | self.length = length 26 | self.undefinedString = undefinedString 27 | self.key = nil 28 | self.value = nil 29 | } 30 | 31 | init(offset: Int, length: Int, key: String, value: String) { 32 | self.offset = offset 33 | self.length = length 34 | self.undefinedString = nil 35 | self.key = key 36 | self.value = value 37 | } 38 | } 39 | 40 | public var languageCode: String 41 | public var path: Path 42 | public var content: [Content] 43 | public var id: String { path.string } 44 | 45 | // for writing 46 | public var newValue: String 47 | 48 | public func filteredContent(query: String) -> [Content] { 49 | let content = content 50 | .compactMap { content -> LocalizableFile.Content? in 51 | guard content.key != nil && content.value != nil else { return nil } 52 | return content 53 | } 54 | 55 | if query.isEmpty { 56 | return Array(content.prefix(5)) 57 | } 58 | 59 | return Array( 60 | content 61 | .filter { 62 | $0.key!.lowercased().contains(query.lowercased()) || 63 | $0.value!.lowercased().contains(query.lowercased()) 64 | } 65 | .prefix(5) 66 | ) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Apps/Sources/System/StatusBarController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StatusBarController.swift 3 | // LocalizationHelper 4 | // 5 | // Created by Chung Tran on 13/01/2021. 6 | // 7 | 8 | import AppKit 9 | import LocalizationHelperKit 10 | 11 | class StatusBarController { 12 | private var statusBar: NSStatusBar 13 | private var statusItem: NSStatusItem 14 | private var popover: NSPopover 15 | private var eventMonitor: EventMonitor? 16 | let popoverWillShow: () -> Void 17 | 18 | init(_ popover: NSPopover, popoverWillShow: @escaping () -> Void) { 19 | self.popover = popover 20 | statusBar = NSStatusBar() 21 | // Creating a status bar item having a fixed length 22 | statusItem = statusBar.statusItem(withLength: 28.0) 23 | self.popoverWillShow = popoverWillShow 24 | 25 | if let statusBarButton = statusItem.button { 26 | statusBarButton.image = Asset.MacOS.statusBarIcon.image 27 | statusBarButton.image?.size = NSSize(width: 18.0, height: 18.0) 28 | statusBarButton.image?.isTemplate = true 29 | 30 | statusBarButton.action = #selector(togglePopover(sender:)) 31 | statusBarButton.target = self 32 | } 33 | 34 | eventMonitor = EventMonitor(mask: [.leftMouseDown, .rightMouseDown], handler: mouseEventHandler) 35 | } 36 | 37 | @objc func togglePopover(sender: AnyObject) { 38 | if(popover.isShown) { 39 | hidePopover(sender) 40 | } 41 | else { 42 | popoverWillShow() 43 | showPopover(sender) 44 | } 45 | } 46 | 47 | func showPopover(_ sender: AnyObject? = nil) { 48 | if let statusBarButton = statusItem.button { 49 | popover.show(relativeTo: statusBarButton.bounds, of: statusBarButton, preferredEdge: NSRectEdge.maxY) 50 | eventMonitor?.start() 51 | } 52 | } 53 | 54 | func hidePopover(_ sender: AnyObject? = nil) { 55 | popover.performClose(sender) 56 | eventMonitor?.stop() 57 | } 58 | 59 | func mouseEventHandler(_ event: NSEvent?) { 60 | if(popover.isShown) { 61 | hidePopover(event!) 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test1/Test1/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | UISceneConfigurations 28 | 29 | UIWindowSceneSessionRoleApplication 30 | 31 | 32 | UISceneConfigurationName 33 | Default Configuration 34 | UISceneDelegateClassName 35 | $(PRODUCT_MODULE_NAME).SceneDelegate 36 | UISceneStoryboardFile 37 | Main 38 | 39 | 40 | 41 | 42 | UIApplicationSupportsIndirectInputEvents 43 | 44 | UILaunchStoryboardName 45 | LaunchScreen 46 | UIMainStoryboardFile 47 | Main 48 | UIRequiredDeviceCapabilities 49 | 50 | armv7 51 | 52 | UISupportedInterfaceOrientations 53 | 54 | UIInterfaceOrientationPortrait 55 | UIInterfaceOrientationLandscapeLeft 56 | UIInterfaceOrientationLandscapeRight 57 | 58 | UISupportedInterfaceOrientations~ipad 59 | 60 | UIInterfaceOrientationPortrait 61 | UIInterfaceOrientationPortraitUpsideDown 62 | UIInterfaceOrientationLandscapeLeft 63 | UIInterfaceOrientationLandscapeRight 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test2/Test2/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | UISceneConfigurations 28 | 29 | UIWindowSceneSessionRoleApplication 30 | 31 | 32 | UISceneConfigurationName 33 | Default Configuration 34 | UISceneDelegateClassName 35 | $(PRODUCT_MODULE_NAME).SceneDelegate 36 | UISceneStoryboardFile 37 | Main 38 | 39 | 40 | 41 | 42 | UIApplicationSupportsIndirectInputEvents 43 | 44 | UILaunchStoryboardName 45 | LaunchScreen 46 | UIMainStoryboardFile 47 | Main 48 | UIRequiredDeviceCapabilities 49 | 50 | armv7 51 | 52 | UISupportedInterfaceOrientations 53 | 54 | UIInterfaceOrientationPortrait 55 | UIInterfaceOrientationLandscapeLeft 56 | UIInterfaceOrientationLandscapeRight 57 | 58 | UISupportedInterfaceOrientations~ipad 59 | 60 | UIInterfaceOrientationPortrait 61 | UIInterfaceOrientationPortraitUpsideDown 62 | UIInterfaceOrientationLandscapeLeft 63 | UIInterfaceOrientationLandscapeRight 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test3/Test3/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | UISceneConfigurations 28 | 29 | UIWindowSceneSessionRoleApplication 30 | 31 | 32 | UISceneConfigurationName 33 | Default Configuration 34 | UISceneDelegateClassName 35 | $(PRODUCT_MODULE_NAME).SceneDelegate 36 | UISceneStoryboardFile 37 | Main 38 | 39 | 40 | 41 | 42 | UIApplicationSupportsIndirectInputEvents 43 | 44 | UILaunchStoryboardName 45 | LaunchScreen 46 | UIMainStoryboardFile 47 | Main 48 | UIRequiredDeviceCapabilities 49 | 50 | armv7 51 | 52 | UISupportedInterfaceOrientations 53 | 54 | UIInterfaceOrientationPortrait 55 | UIInterfaceOrientationLandscapeLeft 56 | UIInterfaceOrientationLandscapeRight 57 | 58 | UISupportedInterfaceOrientations~ipad 59 | 60 | UIInterfaceOrientationPortrait 61 | UIInterfaceOrientationPortraitUpsideDown 62 | UIInterfaceOrientationLandscapeLeft 63 | UIInterfaceOrientationLandscapeRight 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test4/Test4/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | UISceneConfigurations 28 | 29 | UIWindowSceneSessionRoleApplication 30 | 31 | 32 | UISceneConfigurationName 33 | Default Configuration 34 | UISceneDelegateClassName 35 | $(PRODUCT_MODULE_NAME).SceneDelegate 36 | UISceneStoryboardFile 37 | Main 38 | 39 | 40 | 41 | 42 | UIApplicationSupportsIndirectInputEvents 43 | 44 | UILaunchStoryboardName 45 | LaunchScreen 46 | UIMainStoryboardFile 47 | Main 48 | UIRequiredDeviceCapabilities 49 | 50 | armv7 51 | 52 | UISupportedInterfaceOrientations 53 | 54 | UIInterfaceOrientationPortrait 55 | UIInterfaceOrientationLandscapeLeft 56 | UIInterfaceOrientationLandscapeRight 57 | 58 | UISupportedInterfaceOrientations~ipad 59 | 60 | UIInterfaceOrientationPortrait 61 | UIInterfaceOrientationPortraitUpsideDown 62 | UIInterfaceOrientationLandscapeLeft 63 | UIInterfaceOrientationLandscapeRight 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /Kit/Sources/ConfigManager/ConfigRepository.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public protocol ConfigRepository { 4 | func getConfig() async throws -> Config 5 | func saveConfig(_ config: Config) async throws 6 | } 7 | 8 | /// Config reader for XCodeLocalizationHelper 9 | public actor ConfigRepositoryImpl: ConfigRepository { 10 | // MARK: - Properties 11 | 12 | /// Default config JSON file name inside project (Optional) 13 | private let configJSONFileName = ".xcode-localization-helper-config.json" 14 | 15 | /// Directory of the project 16 | private let projectDir: String 17 | 18 | /// Config file absolute path 19 | private var absoluteConfigFilePath: String { 20 | projectDir + "/" + configJSONFileName 21 | } 22 | 23 | // MARK: - Initializer 24 | 25 | /// Initializer 26 | /// - Parameter projectDir: directory of the project 27 | public init(projectDir: String) { 28 | self.projectDir = projectDir 29 | } 30 | 31 | // MARK: - Method 32 | 33 | /// Get XCodeLocalizationHelper from 34 | /// - Returns: XCodeLocalizationHelper's config 35 | public func getConfig() throws -> Config { 36 | // read the json 37 | guard let fileHandler = FileHandle(forReadingAtPath: absoluteConfigFilePath) 38 | else { 39 | throw ConfigRepositoryError.couldNotOpenFile 40 | } 41 | 42 | guard let data = try fileHandler.readToEnd() 43 | else { 44 | throw ConfigRepositoryError.invalidFileFormat 45 | } 46 | let config = try JSONDecoder().decode(Config.self, from: data) 47 | return config 48 | } 49 | 50 | /// Save XCodeLocalizationHelper 51 | public func saveConfig(_ config: Config) throws { 52 | // construct file handler 53 | guard let fileHandler = FileHandle(forWritingAtPath: absoluteConfigFilePath) 54 | else { 55 | throw ConfigRepositoryError.couldNotOpenFile 56 | } 57 | 58 | // erase old data 59 | try fileHandler.truncate(atOffset: 0) 60 | try fileHandler.seek(toOffset: 0) 61 | 62 | // rewrite config 63 | let encoder = JSONEncoder() 64 | encoder.outputFormatting = .prettyPrinted 65 | let data = try encoder.encode(config) 66 | fileHandler.write(data) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Kit/Sources/GoogleTranslate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GoogleTranslate.swift 3 | // bigdicts 4 | // 5 | // Created by Chung Tran on 08/03/2018. 6 | // Copyright © 2018 Chung Tran. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Alamofire 11 | 12 | public struct GoogleTranslate { 13 | public enum TranslateError: Error { 14 | case unknown 15 | } 16 | 17 | public static func translate(text: String, fromLang: String = "auto", toLang: String = "vi", completion: @escaping (Error?, String?) -> Void) { 18 | // Remember adding the oe=utf-8 and ie=utf-8 19 | let replacements: [String: String] = [ 20 | "\\n":"", 21 | "%@":"", 22 | "%d":"" 23 | ] 24 | 25 | var text = text 26 | for (key, value) in replacements { 27 | text = text.replacingOccurrences(of: key, with: value) 28 | } 29 | 30 | let url = "https://translate.googleapis.com/translate_a/single?client=gtx&sl=\(fromLang)&tl=\(toLang)&dt=t&ie=UTF-8&oe=UTF-8&q=\(text.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)!)" 31 | print(url) 32 | AF.request(url) 33 | .validate() 34 | .responseJSON { (response) in 35 | switch response.result { 36 | case .success(let value): 37 | if let value = value as? [Any], 38 | value.count > 0, 39 | let value2 = value[0] as? [Any], 40 | value2.count > 0{ 41 | var translated = "" 42 | for i in value2 { 43 | if let value3 = i as? [Any], 44 | value3.count > 0, 45 | let fraction = value3[0] as? String { 46 | translated = translated + " " + fraction 47 | } 48 | } 49 | translated = translated.trimmingCharacters(in: .whitespaces) 50 | for (key, value) in replacements { 51 | translated = translated.replacingOccurrences(of: value, with: key) 52 | } 53 | completion(nil, translated) 54 | return 55 | } 56 | completion(TranslateError.unknown, nil) 57 | 58 | case .failure(let error): 59 | completion(error, nil) 60 | } 61 | } 62 | } 63 | } 64 | 65 | -------------------------------------------------------------------------------- /Apps/Sources/UI/BETextEditor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BETextEditor.swift 3 | // LocalizationHelper 4 | // 5 | // Created by Chung Tran on 02/08/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | private struct ViewHeightKey: PreferenceKey { 11 | static var defaultValue = CGFloat.zero 12 | static func reduce(value: inout CGFloat, nextValue: () -> Value) { 13 | value += nextValue() 14 | } 15 | } 16 | 17 | struct BETextEditor: View { 18 | let placeholder: String 19 | let lineLimit: Int 20 | 21 | @State fileprivate var height: CGFloat = .zero 22 | @Binding var text: String 23 | var body: some View { 24 | ZStack(alignment: .topLeading) { 25 | TextEditor(text: $text) 26 | .frame(maxHeight: height) 27 | // following line is a hack to force TextEditor to appear 28 | // similar to .textFieldStyle(RoundedBorderTextFieldStyle())... 29 | .cornerRadius(6.0) 30 | .foregroundColor(Color(.labelColor)) 31 | .multilineTextAlignment(.leading) 32 | Text(text.isEmpty ? placeholder: text) 33 | .lineLimit(lineLimit) 34 | // following line is a hack to create an inset similar to the TextEditor inset... 35 | .padding(.leading, 4) 36 | .foregroundColor(Color(.placeholderTextColor)) 37 | .opacity(text.isEmpty ? 1 : 0) 38 | .background(GeometryReader { 39 | Color.clear.preference(key: ViewHeightKey.self, value: $0.frame(in: .local).height) 40 | }) 41 | .allowsHitTesting(false) 42 | } 43 | .font(.body) 44 | .onPreferenceChange(ViewHeightKey.self) { 45 | height = $0 46 | } 47 | } 48 | } 49 | 50 | #if DEBUG 51 | struct BETextEditor_Previews: PreviewProvider { 52 | static var previews: some View { 53 | BETextEditor(placeholder: "Enter text", lineLimit: 3, text: .constant("Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum")) 54 | .previewLayout(.fixed(width: 500, height: 500)) 55 | } 56 | } 57 | #endif 58 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test1/Test1/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // Test1 4 | // 5 | // Created by Chung Tran on 20/07/2021. 6 | // 7 | 8 | import UIKit 9 | 10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 11 | 12 | var window: UIWindow? 13 | 14 | 15 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 16 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 17 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 18 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 19 | guard let _ = (scene as? UIWindowScene) else { return } 20 | } 21 | 22 | func sceneDidDisconnect(_ scene: UIScene) { 23 | // Called as the scene is being released by the system. 24 | // This occurs shortly after the scene enters the background, or when its session is discarded. 25 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 26 | // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). 27 | } 28 | 29 | func sceneDidBecomeActive(_ scene: UIScene) { 30 | // Called when the scene has moved from an inactive state to an active state. 31 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 32 | } 33 | 34 | func sceneWillResignActive(_ scene: UIScene) { 35 | // Called when the scene will move from an active state to an inactive state. 36 | // This may occur due to temporary interruptions (ex. an incoming phone call). 37 | } 38 | 39 | func sceneWillEnterForeground(_ scene: UIScene) { 40 | // Called as the scene transitions from the background to the foreground. 41 | // Use this method to undo the changes made on entering the background. 42 | } 43 | 44 | func sceneDidEnterBackground(_ scene: UIScene) { 45 | // Called as the scene transitions from the foreground to the background. 46 | // Use this method to save data, release shared resources, and store enough scene-specific state information 47 | // to restore the scene back to its current state. 48 | 49 | // Save changes in the application's managed object context when the application transitions to the background. 50 | (UIApplication.shared.delegate as? AppDelegate)?.saveContext() 51 | } 52 | 53 | 54 | } 55 | 56 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test2/Test2/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // Test2 4 | // 5 | // Created by Chung Tran on 20/07/2021. 6 | // 7 | 8 | import UIKit 9 | 10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 11 | 12 | var window: UIWindow? 13 | 14 | 15 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 16 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 17 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 18 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 19 | guard let _ = (scene as? UIWindowScene) else { return } 20 | } 21 | 22 | func sceneDidDisconnect(_ scene: UIScene) { 23 | // Called as the scene is being released by the system. 24 | // This occurs shortly after the scene enters the background, or when its session is discarded. 25 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 26 | // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). 27 | } 28 | 29 | func sceneDidBecomeActive(_ scene: UIScene) { 30 | // Called when the scene has moved from an inactive state to an active state. 31 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 32 | } 33 | 34 | func sceneWillResignActive(_ scene: UIScene) { 35 | // Called when the scene will move from an active state to an inactive state. 36 | // This may occur due to temporary interruptions (ex. an incoming phone call). 37 | } 38 | 39 | func sceneWillEnterForeground(_ scene: UIScene) { 40 | // Called as the scene transitions from the background to the foreground. 41 | // Use this method to undo the changes made on entering the background. 42 | } 43 | 44 | func sceneDidEnterBackground(_ scene: UIScene) { 45 | // Called as the scene transitions from the foreground to the background. 46 | // Use this method to save data, release shared resources, and store enough scene-specific state information 47 | // to restore the scene back to its current state. 48 | 49 | // Save changes in the application's managed object context when the application transitions to the background. 50 | (UIApplication.shared.delegate as? AppDelegate)?.saveContext() 51 | } 52 | 53 | 54 | } 55 | 56 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test3/Test3/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // Test3 4 | // 5 | // Created by Chung Tran on 20/07/2021. 6 | // 7 | 8 | import UIKit 9 | 10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 11 | 12 | var window: UIWindow? 13 | 14 | 15 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 16 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 17 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 18 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 19 | guard let _ = (scene as? UIWindowScene) else { return } 20 | } 21 | 22 | func sceneDidDisconnect(_ scene: UIScene) { 23 | // Called as the scene is being released by the system. 24 | // This occurs shortly after the scene enters the background, or when its session is discarded. 25 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 26 | // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). 27 | } 28 | 29 | func sceneDidBecomeActive(_ scene: UIScene) { 30 | // Called when the scene has moved from an inactive state to an active state. 31 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 32 | } 33 | 34 | func sceneWillResignActive(_ scene: UIScene) { 35 | // Called when the scene will move from an active state to an inactive state. 36 | // This may occur due to temporary interruptions (ex. an incoming phone call). 37 | } 38 | 39 | func sceneWillEnterForeground(_ scene: UIScene) { 40 | // Called as the scene transitions from the background to the foreground. 41 | // Use this method to undo the changes made on entering the background. 42 | } 43 | 44 | func sceneDidEnterBackground(_ scene: UIScene) { 45 | // Called as the scene transitions from the foreground to the background. 46 | // Use this method to save data, release shared resources, and store enough scene-specific state information 47 | // to restore the scene back to its current state. 48 | 49 | // Save changes in the application's managed object context when the application transitions to the background. 50 | (UIApplication.shared.delegate as? AppDelegate)?.saveContext() 51 | } 52 | 53 | 54 | } 55 | 56 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test4/Test4/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // Test4 4 | // 5 | // Created by Chung Tran on 20/07/2021. 6 | // 7 | 8 | import UIKit 9 | 10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 11 | 12 | var window: UIWindow? 13 | 14 | 15 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 16 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 17 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 18 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 19 | guard let _ = (scene as? UIWindowScene) else { return } 20 | } 21 | 22 | func sceneDidDisconnect(_ scene: UIScene) { 23 | // Called as the scene is being released by the system. 24 | // This occurs shortly after the scene enters the background, or when its session is discarded. 25 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 26 | // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). 27 | } 28 | 29 | func sceneDidBecomeActive(_ scene: UIScene) { 30 | // Called when the scene has moved from an inactive state to an active state. 31 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 32 | } 33 | 34 | func sceneWillResignActive(_ scene: UIScene) { 35 | // Called when the scene will move from an active state to an inactive state. 36 | // This may occur due to temporary interruptions (ex. an incoming phone call). 37 | } 38 | 39 | func sceneWillEnterForeground(_ scene: UIScene) { 40 | // Called as the scene transitions from the background to the foreground. 41 | // Use this method to undo the changes made on entering the background. 42 | } 43 | 44 | func sceneDidEnterBackground(_ scene: UIScene) { 45 | // Called as the scene transitions from the foreground to the background. 46 | // Use this method to save data, release shared resources, and store enough scene-specific state information 47 | // to restore the scene back to its current state. 48 | 49 | // Save changes in the application's managed object context when the application transitions to the background. 50 | (UIApplication.shared.delegate as? AppDelegate)?.saveContext() 51 | } 52 | 53 | 54 | } 55 | 56 | -------------------------------------------------------------------------------- /Apps/Sources/UI/Main/MainView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainView.swift 3 | // LocalizationHelper 4 | // 5 | // Created by Chung Tran on 24/07/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct MainView: View { 11 | // MARK: - Observed objects 12 | @ObservedObject var viewModel: MainViewModel 13 | 14 | var body: some View { 15 | VStack { 16 | title 17 | content 18 | footer 19 | } 20 | .frame(minWidth: 600, maxWidth: .infinity, minHeight: 600, maxHeight: .infinity) 21 | } 22 | 23 | var title: some View { 24 | var text = "Open a project" 25 | 26 | if let project = viewModel.appState.project 27 | { 28 | switch project { 29 | case .default(let project): 30 | text = project.target.name 31 | case .tuist(let project): 32 | text = project.projectName 33 | } 34 | } 35 | 36 | return Group { 37 | Text(text) 38 | .lineLimit(1) 39 | .truncationMode(.middle) 40 | .padding(.top, 20) 41 | .font(.title) 42 | Divider() 43 | } 44 | } 45 | 46 | var content: AnyView { 47 | guard let project = viewModel.appState.project 48 | else { 49 | viewModel.projectViewModel = nil 50 | return AnyView(OpenProjectView(handler: viewModel)) 51 | } 52 | viewModel.projectViewModel = .init(project: project) 53 | return AnyView(ProjectView(viewModel: viewModel.projectViewModel)) 54 | } 55 | 56 | var footer: some View { 57 | Group { 58 | Divider() 59 | HStack { 60 | Text("Author: Chung Tran (bigearsenal)") 61 | Link(url: "https://www.linkedin.com/in/chung-tr%E1%BA%A7n-39b46569/", description: "LinkedIn") 62 | Link(url: "https://github.com/bigearsenal/XCodeLocalizationHelper", description: "Repository") 63 | Spacer() 64 | Link(url: "https://www.buymeacoffee.com/bigearsenal", description: "☕ Buy me a coffee") 65 | Spacer() 66 | 67 | if viewModel.appState.project != nil { 68 | Button("Open another...") { 69 | viewModel.closeProject() 70 | } 71 | } 72 | 73 | Button("Quit") { 74 | NSApplication.shared.terminate(self) 75 | } 76 | } 77 | .padding(.bottom, 8) 78 | .padding(.leading) 79 | .padding(.trailing) 80 | } 81 | 82 | } 83 | } 84 | 85 | #if DEBUG 86 | struct MainView_Previews: PreviewProvider { 87 | static var previews: some View { 88 | MainView(viewModel: .init()) 89 | } 90 | } 91 | #endif 92 | -------------------------------------------------------------------------------- /Apps/Sources/UI/SelectLanguageView/SelectLanguageView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SelectLanguageView.swift 3 | // LocalizationHelper 4 | // 5 | // Created by Chung Tran on 25/07/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SelectLanguageView: View { 11 | @State var languages: [ISOLanguageCode] 12 | @Binding var isShowing: Bool 13 | let canSelectMultipleLanguages: Bool 14 | let completion: ([ISOLanguageCode]) -> Void 15 | 16 | var body: some View { 17 | VStack(spacing: 0) { 18 | headerView 19 | listView 20 | Button("Done") { 21 | doneButtonDidTouch() 22 | } 23 | .disabled(languages.filter {$0.isSelected}.count <= 0) 24 | .padding() 25 | } 26 | } 27 | 28 | private var headerView: some View { 29 | HStack { 30 | Spacer() 31 | Text("Select language").font(.title2) 32 | Spacer() 33 | Image(systemName: "xmark.circle.fill") 34 | .resizable() 35 | .onTapGesture { 36 | isShowing.toggle() 37 | } 38 | .frame(width: 20, height: 20) 39 | 40 | } 41 | .padding() 42 | } 43 | 44 | private var listView: some View { 45 | List{ 46 | ForEach(0.. String? { 58 | if isAtEOF { return nil } 59 | 60 | repeat { 61 | if let range = buffer.range(of: delimPattern, options: [], in: buffer.startIndex.. 0) ? String(data: buffer, encoding: encoding) : nil 74 | } 75 | length = tempData.count 76 | buffer.append(tempData) 77 | } 78 | } while true 79 | } 80 | } 81 | 82 | extension LineReader { 83 | public func makeIterator() -> AnyIterator { 84 | .init { 85 | // get offset before calling nextLine 86 | let offset = self.offset 87 | 88 | // call next line 89 | guard let string = self.nextLine() else { 90 | return nil 91 | } 92 | 93 | // return result 94 | return .init(string: string, offset: offset, length: self.length) 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /Apps/Sources/Services/FilePickerService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FilePickerService.swift 3 | // LocalizationHelper 4 | // 5 | // Created by Chung Tran on 25/07/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | #if DEBUG 11 | import XcodeProj 12 | #endif 13 | 14 | protocol FilePickerServiceType { 15 | func showFilePicker(title: String, showsResizeIndicator: Bool, showsHiddenFiles: Bool, allowsMultipleSelection: Bool, canChooseDirectories: Bool, canChooseFiles: Bool, allowedFileTypes: [String]?, directoryURL: String?, completion: @escaping (String) -> Void) 16 | } 17 | 18 | struct FilePickerService: FilePickerServiceType { 19 | func showFilePicker( 20 | title: String, 21 | showsResizeIndicator: Bool = true, 22 | showsHiddenFiles: Bool = false, 23 | allowsMultipleSelection: Bool = false, 24 | canChooseDirectories: Bool, 25 | canChooseFiles: Bool, 26 | allowedFileTypes: [String]? = nil, 27 | directoryURL: String?, 28 | completion: @escaping (String) -> Void 29 | ) { 30 | (NSApplication.shared.delegate as! AppDelegate).statusBar?.hidePopover() 31 | 32 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { 33 | let dialog = NSOpenPanel() 34 | 35 | 36 | dialog.title = title 37 | dialog.showsResizeIndicator = showsResizeIndicator 38 | dialog.showsHiddenFiles = showsHiddenFiles 39 | dialog.allowsMultipleSelection = allowsMultipleSelection 40 | dialog.canChooseDirectories = canChooseDirectories 41 | dialog.canChooseFiles = canChooseFiles 42 | if let allowedFileTypes { 43 | dialog.allowedFileTypes = allowedFileTypes 44 | } 45 | 46 | if let string = directoryURL, let url = URL(string: string) 47 | { 48 | dialog.directoryURL = url 49 | } 50 | 51 | if (dialog.runModal() == .OK) { 52 | let result = dialog.url // Pathname of the file 53 | 54 | if let pathString = result?.path { 55 | completion(pathString) 56 | } 57 | 58 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { 59 | (NSApplication.shared.delegate as! AppDelegate).statusBar?.showPopover() 60 | } 61 | } else { 62 | // User clicked on "Cancel" 63 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { 64 | (NSApplication.shared.delegate as! AppDelegate).statusBar?.showPopover() 65 | } 66 | } 67 | } 68 | } 69 | } 70 | 71 | #if DEBUG 72 | struct MockFilePickerService: FilePickerServiceType { 73 | func showFilePicker(title: String, showsResizeIndicator: Bool, showsHiddenFiles: Bool, allowsMultipleSelection: Bool, canChooseDirectories: Bool, canChooseFiles: Bool, allowedFileTypes: [String]?, directoryURL: String?, completion: @escaping (String) -> Void) { 74 | if canChooseFiles { 75 | completion(XcodeProj.demoProject.1.string) 76 | } 77 | if canChooseDirectories { 78 | completion(XcodeProj.demoProject.1.parent().string) 79 | } 80 | } 81 | } 82 | #endif 83 | -------------------------------------------------------------------------------- /Apps/TestsProjects/TestWithTuist/Tuist/ProjectDescriptionHelpers/Project+Templates.swift: -------------------------------------------------------------------------------- 1 | import ProjectDescription 2 | 3 | /// Project helpers are functions that simplify the way you define your project. 4 | /// Share code to create targets, settings, dependencies, 5 | /// Create your own conventions, e.g: a func that makes sure all shared targets are "static frameworks" 6 | /// See https://docs.tuist.io/guides/helpers/ 7 | 8 | extension Project { 9 | /// Helper function to create the Project for this ExampleApp 10 | public static func app(name: String, platform: Platform, additionalTargets: [String]) -> Project { 11 | var targets = makeAppTargets(name: name, 12 | platform: platform, 13 | dependencies: additionalTargets.map { TargetDependency.target(name: $0) }) 14 | targets += additionalTargets.flatMap({ makeFrameworkTargets(name: $0, platform: platform) }) 15 | return Project(name: name, 16 | organizationName: "tuist.io", 17 | targets: targets) 18 | } 19 | 20 | // MARK: - Private 21 | 22 | /// Helper function to create a framework target and an associated unit test target 23 | private static func makeFrameworkTargets(name: String, platform: Platform) -> [Target] { 24 | let sources = Target(name: name, 25 | platform: platform, 26 | product: .framework, 27 | bundleId: "io.tuist.\(name)", 28 | infoPlist: .default, 29 | sources: ["Targets/\(name)/Sources/**"], 30 | resources: [], 31 | dependencies: []) 32 | let tests = Target(name: "\(name)Tests", 33 | platform: platform, 34 | product: .unitTests, 35 | bundleId: "io.tuist.\(name)Tests", 36 | infoPlist: .default, 37 | sources: ["Targets/\(name)/Tests/**"], 38 | resources: [], 39 | dependencies: [.target(name: name)]) 40 | return [sources, tests] 41 | } 42 | 43 | /// Helper function to create the application target and the unit test target. 44 | private static func makeAppTargets(name: String, platform: Platform, dependencies: [TargetDependency]) -> [Target] { 45 | let platform: Platform = platform 46 | let infoPlist: [String: InfoPlist.Value] = [ 47 | "CFBundleShortVersionString": "1.0", 48 | "CFBundleVersion": "1", 49 | "UIMainStoryboardFile": "", 50 | "UILaunchStoryboardName": "LaunchScreen" 51 | ] 52 | 53 | let mainTarget = Target( 54 | name: name, 55 | platform: platform, 56 | product: .app, 57 | bundleId: "io.tuist.\(name)", 58 | infoPlist: .extendingDefault(with: infoPlist), 59 | sources: ["Targets/\(name)/Sources/**"], 60 | resources: ["Targets/\(name)/Resources/**"], 61 | dependencies: dependencies 62 | ) 63 | 64 | let testTarget = Target( 65 | name: "\(name)Tests", 66 | platform: platform, 67 | product: .unitTests, 68 | bundleId: "io.tuist.\(name)Tests", 69 | infoPlist: .default, 70 | sources: ["Targets/\(name)/Tests/**"], 71 | dependencies: [ 72 | .target(name: "\(name)") 73 | ]) 74 | return [mainTarget, testTarget] 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Apps/Sources/UI/Project/LocalizableFileView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocalizableFileView.swift 3 | // LocalizationHelper 4 | // 5 | // Created by Chung Tran on 31/07/2021. 6 | // 7 | 8 | import SwiftUI 9 | import XcodeProj 10 | import PathKit 11 | 12 | struct LocalizableFileView: View { 13 | let colWidth: CGFloat = 300 14 | let file: LocalizableFile 15 | 16 | let query: String 17 | @Binding var newValue: String 18 | 19 | let removeKeyHandler: (String) -> Void 20 | 21 | var body: some View { 22 | VStack { 23 | Text(languageName) 24 | .layoutPriority(1) 25 | 26 | ScrollView { 27 | phraseListView 28 | } 29 | 30 | Spacer() 31 | 32 | TextField("Localized string for \(languageName)", text: $newValue) 33 | .frame(width: colWidth) 34 | .layoutPriority(1) 35 | .padding(.bottom, 16) 36 | } 37 | .frame(width: colWidth) 38 | } 39 | 40 | // MARK: - Helpers 41 | 42 | private var phraseListView: some View { 43 | LazyVStack { 44 | ForEach(file.filteredContent(query: query)) { text in 45 | phraseView(text: text) 46 | } 47 | } 48 | } 49 | 50 | private func phraseView(text: LocalizableFile.Content) -> some View { 51 | VStack(alignment: .leading) { 52 | Text(text.key!) 53 | .lineLimit(0) 54 | .multilineTextAlignment(.leading) 55 | Text(text.value!) 56 | .lineLimit(0) 57 | .multilineTextAlignment(.leading) 58 | } 59 | .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) 60 | .contentShape(Rectangle()) 61 | .contextMenu { 62 | Button(role: .destructive) { 63 | removeKeyHandler(text.key!) 64 | } label: { 65 | Text("Delete") 66 | Image(systemName: "trash") 67 | } 68 | } 69 | .padding(.bottom, 8) 70 | } 71 | 72 | private var languageName: String { 73 | ISOLanguageCode.all.first(where: {file.languageCode == $0.code})?.name ?? file.languageCode 74 | } 75 | } 76 | 77 | #if DEBUG 78 | struct LocalizableFileView_Previews: PreviewProvider { 79 | static var previews: some View { 80 | let path = XcodeProj.demoProject.1.parent() + "Test1" + "en.lproj" + "/Localizable.strings" 81 | 82 | return LocalizableFileView( 83 | file: .init( 84 | languageCode: "en", 85 | path: path, 86 | content: [ 87 | .init( 88 | offset: 0, 89 | length: 4, 90 | key: "test", 91 | value: "test" 92 | ), 93 | .init( 94 | offset: 0, 95 | length: 5, 96 | key: "test2", 97 | value: "test2" 98 | ), 99 | ], 100 | newValue: "t" 101 | ), 102 | query: "test", 103 | newValue: .constant("new value"), 104 | removeKeyHandler: { key in 105 | 106 | } 107 | ) 108 | } 109 | } 110 | #endif 111 | -------------------------------------------------------------------------------- /Project.swift: -------------------------------------------------------------------------------- 1 | import ProjectDescription // <1> 2 | 3 | let projectName = "LocalizationHelper" 4 | let bundleIdMacOS = "info.bigears.LocalizationHelper" 5 | let kitName = projectName + "Kit" 6 | 7 | let spm: [String: Package] = [ 8 | "Alamofire": .remote( 9 | url: "https://github.com/Alamofire/Alamofire.git", 10 | requirement: .upToNextMajor(from: "5.4.0") 11 | ), 12 | "XcodeProj": .remote( 13 | url: "https://github.com/tuist/xcodeproj.git", 14 | requirement: .upToNextMajor(from: "8.0.0") 15 | ), 16 | "Resolver": .remote( 17 | url: "https://github.com/hmlongco/Resolver.git", 18 | requirement: .upToNextMajor(from: "1.4.3") 19 | ) 20 | ] 21 | 22 | let macOSTargets = makeKitFrameworkTargets(platform: .macOS, spm: spm.map {$0.key}) + 23 | createAppTarget(platform: .macOS, bundleId: bundleIdMacOS) 24 | 25 | let project = Project( 26 | name: projectName, 27 | packages: spm.map {$0.value}, 28 | targets: macOSTargets 29 | ) 30 | 31 | private func createAppTarget( 32 | platform: Platform, 33 | bundleId: String, 34 | spm: [String] = [] 35 | ) -> [Target] { 36 | let name = projectName 37 | let platformDir = "Apps" 38 | 39 | return [ 40 | Target( 41 | name: name, 42 | platform: platform, 43 | product: .app, 44 | bundleId: bundleId, 45 | deploymentTarget: .macOS(targetVersion: "12.0"), 46 | infoPlist: .file(path: .init(platformDir + "/Info.plist")), 47 | sources: [ 48 | "\(platformDir)/Sources/**" 49 | ], 50 | resources: [ 51 | "\(platformDir)/Resources/**" 52 | ], 53 | dependencies: [.target(name: kitName)] 54 | + spm.map {TargetDependency.package(product: $0) } 55 | ), 56 | Target( 57 | name: name + "Tests", 58 | platform: platform, 59 | product: .unitTests, 60 | bundleId: bundleId + "Tests", 61 | deploymentTarget: .macOS(targetVersion: "12.0"), 62 | infoPlist: .default, 63 | sources: [ 64 | "\(platformDir)/Tests/**" 65 | ], 66 | dependencies: [ 67 | .target(name: "\(name)") 68 | ]) 69 | ] 70 | } 71 | 72 | /// Helper function to create a framework target and an associated unit test target 73 | private func makeKitFrameworkTargets(platform: Platform, spm: [String] = []) -> [Target] { 74 | let kitBundleId = bundleIdMacOS + "Kit" 75 | let name = kitName 76 | 77 | let sources = Target( 78 | name: name, 79 | platform: platform, 80 | product: .framework, 81 | bundleId: kitBundleId, 82 | deploymentTarget: .macOS(targetVersion: "12.0"), 83 | infoPlist: .default, 84 | sources: ["Kit/Sources/**"], 85 | resources: [], 86 | dependencies: spm.map {TargetDependency.package(product: $0) } 87 | ) 88 | let tests = Target( 89 | name: "\(name)Tests", 90 | platform: platform, 91 | product: .unitTests, 92 | bundleId: kitBundleId + "Tests", 93 | deploymentTarget: .macOS(targetVersion: "12.0"), 94 | infoPlist: .default, 95 | sources: ["Kit/Tests/**"], 96 | resources: ["Kit/Tests/Resources/**"], 97 | dependencies: [ 98 | .target(name: name) 99 | ] 100 | ) 101 | return [sources, tests] 102 | } 103 | -------------------------------------------------------------------------------- /Apps/Sources/Utilities/Loadable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Loadable.swift 3 | // CountriesSwiftUI 4 | // 5 | // Created by Alexey Naumov on 23.10.2019. 6 | // Copyright © 2019 Alexey Naumov. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftUI 11 | 12 | typealias LoadableSubject = Binding> 13 | 14 | enum Loadable { 15 | 16 | case notRequested 17 | case isLoading(last: T?, cancelBag: CancelBag) 18 | case loaded(T) 19 | case failed(Swift.Error) 20 | 21 | var value: T? { 22 | switch self { 23 | case let .loaded(value): return value 24 | case let .isLoading(last, _): return last 25 | default: return nil 26 | } 27 | } 28 | var error: Swift.Error? { 29 | switch self { 30 | case let .failed(error): return error 31 | default: return nil 32 | } 33 | } 34 | } 35 | 36 | extension Loadable { 37 | 38 | mutating func setIsLoading(cancelBag: CancelBag) { 39 | self = .isLoading(last: value, cancelBag: cancelBag) 40 | } 41 | 42 | mutating func cancelLoading() { 43 | switch self { 44 | case let .isLoading(last, cancelBag): 45 | cancelBag.cancel() 46 | if let last = last { 47 | self = .loaded(last) 48 | } else { 49 | let error = NSError( 50 | domain: NSCocoaErrorDomain, code: NSUserCancelledError, 51 | userInfo: [NSLocalizedDescriptionKey: NSLocalizedString("Canceled by user", 52 | comment: "")]) 53 | self = .failed(error) 54 | } 55 | default: break 56 | } 57 | } 58 | 59 | func map(_ transform: (T) throws -> V) -> Loadable { 60 | do { 61 | switch self { 62 | case .notRequested: return .notRequested 63 | case let .failed(error): return .failed(error) 64 | case let .isLoading(value, cancelBag): 65 | return .isLoading(last: try value.map { try transform($0) }, 66 | cancelBag: cancelBag) 67 | case let .loaded(value): 68 | return .loaded(try transform(value)) 69 | } 70 | } catch { 71 | return .failed(error) 72 | } 73 | } 74 | } 75 | 76 | protocol SomeOptional { 77 | associatedtype Wrapped 78 | func unwrap() throws -> Wrapped 79 | } 80 | 81 | struct ValueIsMissingError: Swift.Error { 82 | var localizedDescription: String { 83 | NSLocalizedString("Data is missing", comment: "") 84 | } 85 | } 86 | 87 | extension Optional: SomeOptional { 88 | func unwrap() throws -> Wrapped { 89 | switch self { 90 | case let .some(value): return value 91 | case .none: throw ValueIsMissingError() 92 | } 93 | } 94 | } 95 | 96 | extension Loadable where T: SomeOptional { 97 | func unwrap() -> Loadable { 98 | map { try $0.unwrap() } 99 | } 100 | } 101 | 102 | extension Loadable: Equatable where T: Equatable { 103 | static func == (lhs: Loadable, rhs: Loadable) -> Bool { 104 | switch (lhs, rhs) { 105 | case (.notRequested, .notRequested): return true 106 | case let (.isLoading(lhsV, _), .isLoading(rhsV, _)): return lhsV == rhsV 107 | case let (.loaded(lhsV), .loaded(rhsV)): return lhsV == rhsV 108 | case let (.failed(lhsE), .failed(rhsE)): 109 | return lhsE.localizedDescription == rhsE.localizedDescription 110 | default: return false 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test1/Test1/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Test1 4 | // 5 | // Created by Chung Tran on 20/07/2021. 6 | // 7 | 8 | import UIKit 9 | import CoreData 10 | 11 | @main 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | // Override point for customization after application launch. 18 | return true 19 | } 20 | 21 | // MARK: UISceneSession Lifecycle 22 | 23 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 24 | // Called when a new scene session is being created. 25 | // Use this method to select a configuration to create the new scene with. 26 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 27 | } 28 | 29 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 30 | // Called when the user discards a scene session. 31 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 32 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 33 | } 34 | 35 | // MARK: - Core Data stack 36 | 37 | lazy var persistentContainer: NSPersistentContainer = { 38 | /* 39 | The persistent container for the application. This implementation 40 | creates and returns a container, having loaded the store for the 41 | application to it. This property is optional since there are legitimate 42 | error conditions that could cause the creation of the store to fail. 43 | */ 44 | let container = NSPersistentContainer(name: "Test1") 45 | container.loadPersistentStores(completionHandler: { (storeDescription, error) in 46 | if let error = error as NSError? { 47 | // Replace this implementation with code to handle the error appropriately. 48 | // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. 49 | 50 | /* 51 | Typical reasons for an error here include: 52 | * The parent directory does not exist, cannot be created, or disallows writing. 53 | * The persistent store is not accessible, due to permissions or data protection when the device is locked. 54 | * The device is out of space. 55 | * The store could not be migrated to the current model version. 56 | Check the error message to determine what the actual problem was. 57 | */ 58 | fatalError("Unresolved error \(error), \(error.userInfo)") 59 | } 60 | }) 61 | return container 62 | }() 63 | 64 | // MARK: - Core Data Saving support 65 | 66 | func saveContext () { 67 | let context = persistentContainer.viewContext 68 | if context.hasChanges { 69 | do { 70 | try context.save() 71 | } catch { 72 | // Replace this implementation with code to handle the error appropriately. 73 | // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. 74 | let nserror = error as NSError 75 | fatalError("Unresolved error \(nserror), \(nserror.userInfo)") 76 | } 77 | } 78 | } 79 | 80 | } 81 | 82 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test2/Test2/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Test2 4 | // 5 | // Created by Chung Tran on 20/07/2021. 6 | // 7 | 8 | import UIKit 9 | import CoreData 10 | 11 | @main 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | // Override point for customization after application launch. 18 | return true 19 | } 20 | 21 | // MARK: UISceneSession Lifecycle 22 | 23 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 24 | // Called when a new scene session is being created. 25 | // Use this method to select a configuration to create the new scene with. 26 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 27 | } 28 | 29 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 30 | // Called when the user discards a scene session. 31 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 32 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 33 | } 34 | 35 | // MARK: - Core Data stack 36 | 37 | lazy var persistentContainer: NSPersistentContainer = { 38 | /* 39 | The persistent container for the application. This implementation 40 | creates and returns a container, having loaded the store for the 41 | application to it. This property is optional since there are legitimate 42 | error conditions that could cause the creation of the store to fail. 43 | */ 44 | let container = NSPersistentContainer(name: "Test2") 45 | container.loadPersistentStores(completionHandler: { (storeDescription, error) in 46 | if let error = error as NSError? { 47 | // Replace this implementation with code to handle the error appropriately. 48 | // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. 49 | 50 | /* 51 | Typical reasons for an error here include: 52 | * The parent directory does not exist, cannot be created, or disallows writing. 53 | * The persistent store is not accessible, due to permissions or data protection when the device is locked. 54 | * The device is out of space. 55 | * The store could not be migrated to the current model version. 56 | Check the error message to determine what the actual problem was. 57 | */ 58 | fatalError("Unresolved error \(error), \(error.userInfo)") 59 | } 60 | }) 61 | return container 62 | }() 63 | 64 | // MARK: - Core Data Saving support 65 | 66 | func saveContext () { 67 | let context = persistentContainer.viewContext 68 | if context.hasChanges { 69 | do { 70 | try context.save() 71 | } catch { 72 | // Replace this implementation with code to handle the error appropriately. 73 | // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. 74 | let nserror = error as NSError 75 | fatalError("Unresolved error \(nserror), \(nserror.userInfo)") 76 | } 77 | } 78 | } 79 | 80 | } 81 | 82 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test3/Test3/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Test3 4 | // 5 | // Created by Chung Tran on 20/07/2021. 6 | // 7 | 8 | import UIKit 9 | import CoreData 10 | 11 | @main 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | // Override point for customization after application launch. 18 | return true 19 | } 20 | 21 | // MARK: UISceneSession Lifecycle 22 | 23 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 24 | // Called when a new scene session is being created. 25 | // Use this method to select a configuration to create the new scene with. 26 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 27 | } 28 | 29 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 30 | // Called when the user discards a scene session. 31 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 32 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 33 | } 34 | 35 | // MARK: - Core Data stack 36 | 37 | lazy var persistentContainer: NSPersistentContainer = { 38 | /* 39 | The persistent container for the application. This implementation 40 | creates and returns a container, having loaded the store for the 41 | application to it. This property is optional since there are legitimate 42 | error conditions that could cause the creation of the store to fail. 43 | */ 44 | let container = NSPersistentContainer(name: "Test3") 45 | container.loadPersistentStores(completionHandler: { (storeDescription, error) in 46 | if let error = error as NSError? { 47 | // Replace this implementation with code to handle the error appropriately. 48 | // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. 49 | 50 | /* 51 | Typical reasons for an error here include: 52 | * The parent directory does not exist, cannot be created, or disallows writing. 53 | * The persistent store is not accessible, due to permissions or data protection when the device is locked. 54 | * The device is out of space. 55 | * The store could not be migrated to the current model version. 56 | Check the error message to determine what the actual problem was. 57 | */ 58 | fatalError("Unresolved error \(error), \(error.userInfo)") 59 | } 60 | }) 61 | return container 62 | }() 63 | 64 | // MARK: - Core Data Saving support 65 | 66 | func saveContext () { 67 | let context = persistentContainer.viewContext 68 | if context.hasChanges { 69 | do { 70 | try context.save() 71 | } catch { 72 | // Replace this implementation with code to handle the error appropriately. 73 | // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. 74 | let nserror = error as NSError 75 | fatalError("Unresolved error \(nserror), \(nserror.userInfo)") 76 | } 77 | } 78 | } 79 | 80 | } 81 | 82 | -------------------------------------------------------------------------------- /Apps/TestsProjects/Test4/Test4/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Test4 4 | // 5 | // Created by Chung Tran on 20/07/2021. 6 | // 7 | 8 | import UIKit 9 | import CoreData 10 | 11 | @main 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | // Override point for customization after application launch. 18 | return true 19 | } 20 | 21 | // MARK: UISceneSession Lifecycle 22 | 23 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 24 | // Called when a new scene session is being created. 25 | // Use this method to select a configuration to create the new scene with. 26 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 27 | } 28 | 29 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 30 | // Called when the user discards a scene session. 31 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 32 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 33 | } 34 | 35 | // MARK: - Core Data stack 36 | 37 | lazy var persistentContainer: NSPersistentContainer = { 38 | /* 39 | The persistent container for the application. This implementation 40 | creates and returns a container, having loaded the store for the 41 | application to it. This property is optional since there are legitimate 42 | error conditions that could cause the creation of the store to fail. 43 | */ 44 | let container = NSPersistentContainer(name: "Test4") 45 | container.loadPersistentStores(completionHandler: { (storeDescription, error) in 46 | if let error = error as NSError? { 47 | // Replace this implementation with code to handle the error appropriately. 48 | // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. 49 | 50 | /* 51 | Typical reasons for an error here include: 52 | * The parent directory does not exist, cannot be created, or disallows writing. 53 | * The persistent store is not accessible, due to permissions or data protection when the device is locked. 54 | * The device is out of space. 55 | * The store could not be migrated to the current model version. 56 | Check the error message to determine what the actual problem was. 57 | */ 58 | fatalError("Unresolved error \(error), \(error.userInfo)") 59 | } 60 | }) 61 | return container 62 | }() 63 | 64 | // MARK: - Core Data Saving support 65 | 66 | func saveContext () { 67 | let context = persistentContainer.viewContext 68 | if context.hasChanges { 69 | do { 70 | try context.save() 71 | } catch { 72 | // Replace this implementation with code to handle the error appropriately. 73 | // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. 74 | let nserror = error as NSError 75 | fatalError("Unresolved error \(nserror), \(nserror.userInfo)") 76 | } 77 | } 78 | } 79 | 80 | } 81 | 82 | -------------------------------------------------------------------------------- /Apps/Tests/Services/XcodeProjectServiceTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProjectServiceTests.swift 3 | // LocalizationHelperTests 4 | // 5 | // Created by Chung Tran on 20/07/2021. 6 | // 7 | 8 | import XCTest 9 | @testable import LocalizationHelper 10 | import PathKit 11 | 12 | class XcodeProjectServiceTests: XCTestCase { 13 | enum Error: Swift.Error { 14 | case languageCodeNotFoundInKnownRegions 15 | case fileNotFoundInLocalizableStringsGroup 16 | case fileNotFound 17 | case wrongNumberOfLocalizableFiles 18 | } 19 | 20 | func testLocalizeFile1() throws { 21 | try testLocalizeProject(fileName: "Test1", languageCode: "zh", expectedNumberOfLocalizableFile: 3) 22 | } 23 | 24 | func testLocalizeFile2() throws { 25 | try testLocalizeProject(fileName: "Test2", languageCode: "ru", expectedNumberOfLocalizableFile: 3) 26 | } 27 | 28 | func testLocalizeFile3() throws { 29 | try testLocalizeProject(fileName: "Test3", languageCode: "ar", expectedNumberOfLocalizableFile: 3) 30 | } 31 | 32 | func testLocalizeFile4() throws { 33 | try testLocalizeProject(fileName: "Test4", languageCode: "vi", expectedNumberOfLocalizableFile: 1) 34 | } 35 | 36 | func testLocalizeTuistProject() throws { 37 | try testLocalizeProject(fileName: "TestWithTuist", languageCode: "vi", expectedNumberOfLocalizableFile: 1) 38 | } 39 | 40 | // MARK: - DefaultProject 41 | private func testLocalizeProject(fileName: String, languageCode: String, expectedNumberOfLocalizableFile: Int) throws { 42 | // Test using TestProjectRepository 43 | Resolver.main.register {TestProjectRepository(testName: fileName) as ProjectRepositoryType} 44 | 45 | let service = XCodeProjectService() 46 | 47 | // test localize project 48 | let project = try service.openCurrentProject() 49 | try service.localizeProject(project, languageCode: languageCode) 50 | 51 | // checkIfFileWasLocalizedCorrectly 52 | switch project { 53 | case .default(let defaultProject): 54 | try checkIfFileWasLocalizedCorrectly(defaultProject: defaultProject, languageCode: languageCode) 55 | case .tuist(let tuistProject): 56 | try checkIfFileWasLocalizedCorrectly(tuistProject: tuistProject, languageCode: languageCode) 57 | } 58 | 59 | // check number of localizableFile after all 60 | let localizableFiles = try service.getLocalizableFiles(fromProject: project) 61 | if localizableFiles.count != expectedNumberOfLocalizableFile { 62 | throw Error.wrongNumberOfLocalizableFiles 63 | } 64 | } 65 | 66 | private func checkIfFileWasLocalizedCorrectly(defaultProject project: DefaultProject, languageCode: String) throws { 67 | 68 | // checking if knownRegions contains "zh" 69 | if project.rootObject?.knownRegions.contains(languageCode) != true { 70 | throw Error.languageCodeNotFoundInKnownRegions 71 | } 72 | 73 | // check if localization group contains "zh.lproj/Localizable.strings" 74 | let localizationGroup = project.rootObject?.mainGroup 75 | .group(named: LOCALIZABLE_STRINGS, recursively: true) 76 | 77 | if localizationGroup?.children.contains(where: {$0.path == "\(languageCode).lproj/Localizable.strings"}) != true 78 | { 79 | throw Error.fileNotFoundInLocalizableStringsGroup 80 | } 81 | 82 | // check if file exists 83 | let zhFile = localizationGroup?.children.first(where: {$0.path == "\(languageCode).lproj/Localizable.strings"}) 84 | let fullPath = try zhFile?.fullPath(sourceRoot: project.projectFolderPath) 85 | if fullPath?.exists != true { 86 | throw Error.fileNotFound 87 | } 88 | } 89 | 90 | // MARK: - TuistProject 91 | private func checkIfFileWasLocalizedCorrectly(tuistProject project: TuistProject, languageCode: String) throws 92 | { 93 | let localizableFilePath = project.resourcePath + "\(languageCode).lproj" + LOCALIZABLE_STRINGS 94 | if !localizableFilePath.exists || !localizableFilePath.isFile { 95 | throw Error.fileNotFound 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Apps/Sources/Models/Project/DefaultProject.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultProject.swift 3 | // LocalizationHelper 4 | // 5 | // Created by Chung Tran on 21/07/2021. 6 | // 7 | 8 | import Foundation 9 | import XcodeProj 10 | import PathKit 11 | 12 | struct DefaultProject: Equatable { 13 | var pxbproj: XcodeProj 14 | var target: PBXTarget 15 | var path: Path 16 | 17 | var rootObject: PBXProject? { 18 | pxbproj.pbxproj.rootObject 19 | } 20 | 21 | var projectFolderPath: Path { 22 | path.parent() 23 | } 24 | } 25 | 26 | extension DefaultProject { 27 | func localize(fileGenerator: FileGeneratorType, languageCode: String) throws 28 | { 29 | guard let rootObject = rootObject 30 | else { 31 | throw LocalizationHelperError.projectNotFound 32 | } 33 | 34 | // add knownRegions if not exists 35 | if !rootObject.knownRegions.contains(languageCode) { 36 | rootObject.knownRegions.append(languageCode) 37 | } 38 | 39 | // find Localizable.strings group 40 | var localizableStringsGroup = rootObject.mainGroup 41 | .group(named: LOCALIZABLE_STRINGS, recursively: true) 42 | 43 | // Localizable.strings group is not exists 44 | if localizableStringsGroup == nil 45 | { 46 | // create Localizable.strings group 47 | let group = (rootObject.mainGroup.children.first(where: {$0.path == target.name}) as? PBXGroup) ?? 48 | (rootObject.mainGroup.children.first as? PBXGroup)?.group(named: "Resources", recursively: true) ?? 49 | rootObject.mainGroup 50 | 51 | localizableStringsGroup = try group?.addVariantGroup(named: LOCALIZABLE_STRINGS).first 52 | 53 | guard let localizableStringsGroup = localizableStringsGroup 54 | else { 55 | throw LocalizationHelperError.couldNotCreateLocalizableStringsGroup 56 | } 57 | 58 | // add Localizable.strings group to target 59 | let fileBuildPhases = target.buildPhases.first(where: {$0 is PBXSourcesBuildPhase}) 60 | _ = try fileBuildPhases?.add(file: localizableStringsGroup) 61 | } 62 | 63 | // get localizableStringsGroup's path 64 | guard let localizableStringsGroup = localizableStringsGroup 65 | else { 66 | throw LocalizationHelperError.localizableStringsGroupNotFound 67 | } 68 | 69 | guard let localizableStringsGroupFullPath = try localizableStringsGroup.fullPath(sourceRoot: projectFolderPath) 70 | else { 71 | throw LocalizationHelperError.localizableStringsGroupFullPathNotFound 72 | } 73 | 74 | // add .lproj/Localizable.strings to project at localizableStringsGroupPath 75 | try fileGenerator.generateFile(at: localizableStringsGroupFullPath + "\(languageCode).lproj", fileName: LOCALIZABLE_STRINGS, content: .stringsFileHeader) 76 | 77 | let newFileFullPath = localizableStringsGroupFullPath + "\(languageCode).lproj" + LOCALIZABLE_STRINGS 78 | try localizableStringsGroup.addFile(at: newFileFullPath, sourceRoot: projectFolderPath) 79 | 80 | // set flag CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES 81 | let key = "CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED" 82 | target.buildConfigurationList?.buildConfigurations.forEach { 83 | $0.buildSettings[key] = "YES" 84 | } 85 | 86 | // save project 87 | try pxbproj.write(path: path) 88 | } 89 | 90 | func getLocalizableStringsGroupFullPath() -> Path? { 91 | try? rootObject?.mainGroup 92 | .group(named: LOCALIZABLE_STRINGS, recursively: true)? 93 | .fullPath(sourceRoot: projectFolderPath) 94 | } 95 | } 96 | 97 | #if DEBUG 98 | extension DefaultProject { 99 | static var demo: Self { 100 | let xcodeProj = XcodeProj.demoProject.0! 101 | let target = xcodeProj.pbxproj.targets(named: "Test1").first! 102 | let path = XcodeProj.demoProject.1 103 | return .init(pxbproj: xcodeProj, target: target, path: path) 104 | } 105 | } 106 | #endif 107 | -------------------------------------------------------------------------------- /Tuist/ResourceSynthesizers/Strings.stencil: -------------------------------------------------------------------------------- 1 | // swiftlint:disable all 2 | // Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen 3 | 4 | {% if tables.count > 0 %} 5 | {% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %} 6 | import Foundation 7 | 8 | // swiftlint:disable superfluous_disable_command file_length implicit_return 9 | 10 | // MARK: - Strings 11 | 12 | {% macro parametersBlock types %}{% filter removeNewlines:"leading" %} 13 | {% for type in types %} 14 | {% if type == "String" %} 15 | _ p{{forloop.counter}}: Any 16 | {% else %} 17 | _ p{{forloop.counter}}: {{type}} 18 | {% endif %} 19 | {{ ", " if not forloop.last }} 20 | {% endfor %} 21 | {% endfilter %}{% endmacro %} 22 | {% macro argumentsBlock types %}{% filter removeNewlines:"leading" %} 23 | {% for type in types %} 24 | {% if type == "String" %} 25 | String(describing: p{{forloop.counter}}) 26 | {% elif type == "UnsafeRawPointer" %} 27 | Int(bitPattern: p{{forloop.counter}}) 28 | {% else %} 29 | p{{forloop.counter}} 30 | {% endif %} 31 | {{ ", " if not forloop.last }} 32 | {% endfor %} 33 | {% endfilter %}{% endmacro %} 34 | {% macro recursiveBlock table item %} 35 | {% for string in item.strings %} 36 | {% if not param.noComments %} 37 | /// {{string.translation}} 38 | {% endif %} 39 | {% if string.types %} 40 | {{accessModifier}} static func {{string.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}({% call parametersBlock string.types %}) -> String { 41 | return {{enumName}}.tr("{{table}}", "{{string.key}}", {% call argumentsBlock string.types %}) 42 | } 43 | {% elif param.lookupFunction %} 44 | {# custom localization function is mostly used for in-app lang selection, so we want the loc to be recomputed at each call for those (hence the computed var) #} 45 | {{accessModifier}} static var {{string.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}: String { return {{enumName}}.tr("{{table}}", "{{string.key}}") } 46 | {% else %} 47 | {{accessModifier}} static var {{string.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}: String { {{enumName}}.tr("{{table}}", "{{string.key}}") } 48 | {% endif %} 49 | {% endfor %} 50 | {% for child in item.children %} 51 | 52 | {{accessModifier}} enum {{child.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} { 53 | {% filter indent:2 %}{% call recursiveBlock table child %}{% endfilter %} 54 | } 55 | {% endfor %} 56 | {% endmacro %} 57 | // swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length 58 | // swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces 59 | {% set enumName %}{{param.enumName|default:"L10n"}}{% endset %} 60 | {{accessModifier}} enum {{enumName}} { 61 | {% if tables.count > 1 or param.forceFileNameEnum %} 62 | {% for table in tables %} 63 | {{accessModifier}} enum {{table.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} { 64 | {% filter indent:2 %}{% call recursiveBlock table.name table.levels %}{% endfilter %} 65 | } 66 | {% endfor %} 67 | {% else %} 68 | {% call recursiveBlock tables.first.name tables.first.levels %} 69 | {% endif %} 70 | } 71 | // swiftlint:enable explicit_type_interface function_parameter_count identifier_name line_length 72 | // swiftlint:enable nesting type_body_length type_name vertical_whitespace_opening_braces 73 | 74 | // MARK: - Implementation Details 75 | 76 | extension {{enumName}} { 77 | private static func tr(_ table: String, _ key: String, _ args: CVarArg...) -> String { 78 | {% if param.lookupFunction %} 79 | let format = {{ param.lookupFunction }}(key, table) 80 | {% else %} 81 | let format = {{param.bundle|default:"BundleToken.bundle"}}.localizedString(forKey: key, value: nil, table: table) 82 | {% endif %} 83 | return String(format: format, locale: Locale.current, arguments: args) 84 | } 85 | } 86 | {% if not param.bundle and not param.lookupFunction %} 87 | 88 | // swiftlint:disable convenience_type 89 | private final class BundleToken { 90 | static let bundle: Bundle = { 91 | #if SWIFT_PACKAGE 92 | return Bundle.module 93 | #else 94 | return Bundle(for: BundleToken.self) 95 | #endif 96 | }() 97 | } 98 | // swiftlint:enable convenience_type 99 | {% endif %} 100 | {% else %} 101 | // No string found 102 | {% endif %} 103 | -------------------------------------------------------------------------------- /Apps/Sources/Repository/ProjectRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProjectRepository.swift 3 | // LocalizationHelperKit 4 | // 5 | // Created by Chung Tran on 20/07/2021. 6 | // 7 | 8 | import Foundation 9 | import XcodeProj 10 | import PathKit 11 | 12 | protocol ProjectRepositoryType { 13 | func getCurrentProject() -> Project? 14 | func setCurrentProject(_ project: Project) 15 | func clearCurrentProject() 16 | } 17 | 18 | struct UserDefaultsProjectRepository: ProjectRepositoryType { 19 | // MARK: - Keys 20 | // Default project 21 | private let defaultProjectPathKey = "KEYS.DEFAULT_PROJECT.PROJECT_PATH" 22 | private let defaultProjectTargetKey = "KEYS.DEFAULT_PROJECT.TARGET" 23 | // Tuist project 24 | private let tuistProjectPathKey = "KEYS.TUIST_PROJECT.PATH" 25 | private let tuistProjectResourcePathKey = "KEYS.TUIST_PROJECT.RESOURCE_PATH" 26 | private let tuistProjectNameKey = "KEYS.TUIST_PROJECT.PROJECT_NAME" 27 | 28 | // MARK: - Methods 29 | func getCurrentProject() -> Project? { 30 | // default project 31 | if let projectPath = UserDefaults.standard.string(forKey: defaultProjectPathKey), 32 | let targetName = UserDefaults.standard.string(forKey: defaultProjectTargetKey), 33 | let project = try? XcodeProj(pathString: projectPath), 34 | let target = project.pbxproj.targets(named: targetName).first 35 | { 36 | return .default( 37 | .init( 38 | pxbproj: project, 39 | target: target, 40 | path: Path(projectPath) 41 | ) 42 | ) 43 | } 44 | 45 | // tuist project 46 | if let path = UserDefaults.standard.string(forKey: tuistProjectPathKey), 47 | let resourcePath = UserDefaults.standard.string(forKey: tuistProjectResourcePathKey), 48 | let name = UserDefaults.standard.string(forKey: tuistProjectNameKey) 49 | { 50 | let path = Path(path) 51 | let resourcePath = Path(resourcePath) 52 | if path.isDirectory { 53 | return .tuist( 54 | .init(path: path, resourcePath: resourcePath, projectName: name) 55 | ) 56 | } 57 | } 58 | 59 | return nil 60 | } 61 | 62 | func setCurrentProject(_ project: Project) { 63 | switch project { 64 | case .default(let defaultProject): 65 | // set new value 66 | UserDefaults.standard.set(defaultProject.path.string, forKey: defaultProjectPathKey) 67 | UserDefaults.standard.set(defaultProject.target.name, forKey: defaultProjectTargetKey) 68 | // Remove tuist project if exists 69 | UserDefaults.standard.set(nil, forKey: tuistProjectPathKey) 70 | UserDefaults.standard.set(nil, forKey: tuistProjectResourcePathKey) 71 | UserDefaults.standard.set(nil, forKey: tuistProjectNameKey) 72 | case .tuist(let tuistProject): 73 | // set new value 74 | UserDefaults.standard.set(tuistProject.path.string, forKey: tuistProjectPathKey) 75 | UserDefaults.standard.set(tuistProject.resourcePath.string, forKey: tuistProjectResourcePathKey) 76 | UserDefaults.standard.set(tuistProject.projectName, forKey: tuistProjectNameKey) 77 | // Remove default project if exists 78 | UserDefaults.standard.set(nil, forKey: defaultProjectPathKey) 79 | UserDefaults.standard.set(nil, forKey: defaultProjectTargetKey) 80 | } 81 | } 82 | 83 | func clearCurrentProject() { 84 | // Remove tuist project if exists 85 | UserDefaults.standard.set(nil, forKey: tuistProjectPathKey) 86 | UserDefaults.standard.set(nil, forKey: tuistProjectResourcePathKey) 87 | UserDefaults.standard.set(nil, forKey: tuistProjectNameKey) 88 | 89 | // Remove default project if exists 90 | UserDefaults.standard.set(nil, forKey: defaultProjectPathKey) 91 | UserDefaults.standard.set(nil, forKey: defaultProjectTargetKey) 92 | } 93 | } 94 | 95 | #if DEBUG 96 | class InMemoryProjectRepository: ProjectRepositoryType { 97 | private var currentProject: Project? 98 | 99 | init(project: Project?) { 100 | currentProject = project 101 | } 102 | 103 | func getCurrentProject() -> Project? { 104 | currentProject 105 | } 106 | 107 | func setCurrentProject(_ project: Project) { 108 | currentProject = project 109 | } 110 | 111 | func clearCurrentProject() { 112 | currentProject = nil 113 | } 114 | 115 | static var `default`: InMemoryProjectRepository { 116 | .init(project: .default(.demo)) 117 | } 118 | } 119 | #endif 120 | --------------------------------------------------------------------------------