├── README.md ├── StringsGenerator ├── localization-swift5.stencil ├── swiftgen └── update-localization.sh /README.md: -------------------------------------------------------------------------------- 1 | # Xcode Localization Helper 2 | A shell script to download a CSV file from Google Sheets and parse it into .lproj files, as well as generating a L10n-Constants.swift file containing constants for localized strings, making it easy to reference strings in your Swift code. The script also supports runtime language switching, allowing you to change the app's language on the fly without needing to restart it. 3 | 4 | This Xcode Localization Helper script is designed to streamline the process of adding localization files to your Xcode project. The script performs the following tasks: 5 | - Downloads a CSV from the Google Sheets file containing localization strings for multiple languages. 6 | - Parses the CSV file into separate .lproj files for each language. 7 | - Generates a L10n-Constants.swift file containing constants for localized strings using the SwiftGen library, making it easy to reference strings in your Swift code. 8 | 9 | By automating these tasks, this script makes it easy to keep your Xcode project's localization files up-to-date and well-organized. Simply run the script whenever you need to update your localization files based on the latest CSV file. This will save you time and ensure that your Xcode project remains properly localized across all supported languages. 10 | 11 | ## **How to use:** 12 | 13 | **Note: You can skip steps 1-4 if you are using a public Google Sheet.** 14 | 15 | Before using the Xcode Localization Helper script, you need to set up the Google Cloud API for your project. Follow these steps to set up the API: 16 | 17 | 1. Go to the [Google Cloud Console](https://console.cloud.google.com/) and select your project. 18 | 19 | 2. **Enable the Google Drive API:** 20 | 21 | a. Click on the hamburger menu (three horizontal lines) in the top left corner of the Google Cloud Console. 22 | 23 | b. Navigate to "APIs & Services" > "Library". 24 | 25 | c. In the search bar, search for "Google Drive API" and activate it. 26 | 27 | 3. **Create a service account and download the `credentials.json` file:** 28 | 29 | a. Go to the [Google Cloud Console IAM & Admin](https://console.cloud.google.com/iam-admin/serviceaccounts). 30 | 31 | b. Select your project from the project dropdown menu. 32 | 33 | c. Click on "Create Service Account". 34 | 35 | d. Fill in the service account details and click "Create". 36 | 37 | e. On the "Service Accounts" page, find your newly created service account and click on it to edit. 38 | 39 | f. Go to the "Keys" tab and click on "Add Key". Select "JSON" as the key type. This will download the `credentials.json` file. 40 | 41 | g. Move the `credentials.json` file to the same directory as the script. 42 | 43 | 4. **Grant access to the Google Sheet for the service account:** 44 | 45 | a. Open the Google Sheet you want to use for localization. 46 | 47 | b. Click the "Share" button in the top right corner of the sheet. 48 | 49 | c. In the "Share with people and groups" dialog, enter the email address of the service account you created in step 3 This email address should look like `@.iam.gserviceaccount.com`. 50 | 51 | d. Grant the service account "Viewer" access and click "Done" to save the changes. 52 | 53 | 5. **Set up the script:** 54 | 55 | a. Clone or download the script from the GitHub repository. 56 | 57 | b. Open the script in a text editor and replace {SHEET_ID} with your Google Sheets ID. 58 | 59 | c. Optionaly you can add your table id. 60 | 61 | d. Update the languages array with the language codes you want to use. 62 | 63 | 6. **Run sript:** 64 | 65 | sh update-localization.sh 66 | 67 | 6. **Runtime language switching support:** 68 | 69 | The script also enables you to change the app's language at runtime using the L10n.languageProvider.selectedLanguage property. This allows you to switch languages within the app without having to restart it. 70 | 71 | 72 | 73 | ## **Google Sheet structure:** 74 | 75 | To use the script correctly, it is important to structure your Google Sheet properly. Follow these guidelines to fill up the Google Sheet: 76 | 77 | a. The first row should contain language codes (e.g., en, ge, uk) in each column, starting from the second column. The first column should contain the string keys. 78 | 79 | b. Each row after the first row should represent a unique string key for your localization. 80 | 81 | c. The corresponding translated strings for each language should be placed in the columns under the respective language codes. 82 | 83 | Example structure: 84 | 85 | | key | en | uk | 86 | |------------|--------------|--------------| 87 | | hello | Hello | Вітаю | 88 | | welcome | Welcome | Ласкаво просимо | 89 | 90 | ## **Acknowledgments:** 91 | 92 | The parsing script used in this project is based on the [LocalizationDemo](https://github.com/vivek-jl/LocalizationDemo) repository by Vivek Joshi. A big thank you to the author for providing a helpful script for the community. 93 | 94 | This project also integrates the [SwiftGen](https://github.com/SwiftGen/SwiftGen) library to generate a `L10n-Constants.swift` file containing constants for localized strings. A big thank you to the authors and contributors of SwiftGen for creating such a useful library for the Swift community. 95 | -------------------------------------------------------------------------------- /StringsGenerator: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idapgroup/xcode-localization-parser/ecbf4b123ef036601cc3db19b55b05a693fe3ef8/StringsGenerator -------------------------------------------------------------------------------- /localization-swift5.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 prefer_self_in_static_references 9 | 10 | // MARK: - Strings 11 | 12 | {% macro parametersBlock types %} 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 | {% endmacro %} 22 | {% macro argumentsBlock types %} 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 | {% endmacro %} 34 | {% macro recursiveBlock table item %} 35 | {% for string in item.strings %} 36 | {% if not param.noComments %} 37 | {% for line in string.comment|default:string.translation|split:"\n" %} 38 | /// {{line}} 39 | {% endfor %} 40 | {% endif %} 41 | {% set translation string.translation|replace:'"','\"'|replace:' ','\t' %} 42 | {% if string.types %} 43 | {{accessModifier}} static func {{string.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}({% call parametersBlock string.types %}) -> String { 44 | return {{enumName}}.tr("{{table}}", "{{string.key}}", {%+ call argumentsBlock string.types %}, fallback: "{{translation}}") 45 | } 46 | {% elif param.lookupFunction %} 47 | {{accessModifier}} static var {{string.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}: String { return {{enumName}}.tr("{{table}}", "{{string.key}}", fallback: "{{translation}}") } 48 | {% else %} 49 | {{accessModifier}} static var {{string.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}: String { {{enumName}}.tr("{{table}}", "{{string.key}}", fallback: "{{translation}}") } 50 | {% endif %} 51 | {% endfor %} 52 | {% for child in item.children %} 53 | {{accessModifier}} enum {{child.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} { 54 | {% filter indent:2," ",true %}{% call recursiveBlock table child %}{% endfilter %} 55 | } 56 | {% endfor %} 57 | {% endmacro %} 58 | // swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length 59 | // swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces 60 | {% set enumName %}{{param.enumName|default:"L10n"}}{% endset %} 61 | {{accessModifier}} enum {{enumName}} { 62 | private(set) public static var languageProvider = LanguageProvider() 63 | {% if tables.count > 1 or param.forceFileNameEnum %} 64 | {% for table in tables %} 65 | {{accessModifier}} enum {{table.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} { 66 | {% filter indent:2," ",true %}{% call recursiveBlock table.name table.levels %}{% endfilter %} 67 | } 68 | {% endfor %} 69 | {% else %} 70 | {% call recursiveBlock tables.first.name tables.first.levels %} 71 | {% endif %} 72 | } 73 | // swiftlint:enable explicit_type_interface function_parameter_count identifier_name line_length 74 | // swiftlint:enable nesting type_body_length type_name vertical_whitespace_opening_braces 75 | 76 | // MARK: - Implementation Details 77 | 78 | public enum Languages: String, CaseIterable { 79 | case {{param.languages}} 80 | } 81 | 82 | public class LanguageProvider: ObservableObject { 83 | 84 | let constant = "L10nLanguageProvider-IDAP-language" 85 | let defaults = UserDefaults.standard 86 | 87 | public var selectedLanguage: Languages? { 88 | didSet { 89 | defaults.set(selectedLanguage?.rawValue ?? "", forKey: constant) 90 | defaults.synchronize() 91 | } 92 | } 93 | 94 | public init() { 95 | var currentLanguage = Languages(rawValue: defaults.string(forKey: constant) ?? "") 96 | 97 | if currentLanguage == nil { 98 | for language in Locale.preferredLanguages { 99 | let languageID = String(language.prefix(2)) 100 | if let findedLanguage = Languages(rawValue: languageID) { 101 | currentLanguage = findedLanguage 102 | break 103 | } 104 | } 105 | } 106 | 107 | self.selectedLanguage = currentLanguage ?? Languages.allCases.first 108 | } 109 | } 110 | 111 | extension {{enumName}} { 112 | private static func tr(_ table: String, _ key: String, _ args: CVarArg..., fallback value: String) -> String { 113 | guard let selectedLanguage = self.languageProvider.selectedLanguage?.rawValue else { 114 | return key 115 | } 116 | guard let path = BundleToken.bundle.path(forResource: selectedLanguage, ofType: "lproj"), 117 | let bundle = Bundle(path: path) else { 118 | return key 119 | } 120 | let format = NSLocalizedString(key, tableName: table, bundle: bundle, value: "", comment: "") 121 | return String(format: format, locale: Locale.current, arguments: args) 122 | } 123 | } 124 | {% if not param.bundle and not param.lookupFunction %} 125 | 126 | // swiftlint:disable convenience_type 127 | private final class BundleToken { 128 | static let bundle: Bundle = { 129 | #if SWIFT_PACKAGE 130 | return Bundle.module 131 | #else 132 | return Bundle(for: BundleToken.self) 133 | #endif 134 | }() 135 | } 136 | // swiftlint:enable convenience_type 137 | {% endif %} 138 | {% else %} 139 | // No string found 140 | {% endif %} 141 | -------------------------------------------------------------------------------- /swiftgen: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idapgroup/xcode-localization-parser/ecbf4b123ef036601cc3db19b55b05a693fe3ef8/swiftgen -------------------------------------------------------------------------------- /update-localization.sh: -------------------------------------------------------------------------------- 1 | languages=("en" "uk") 2 | 3 | SHEET_ID={SHEET_ID} 4 | TABLE_ID=0 5 | 6 | rm -rf localization.csv 7 | rm -rf Generated/ 8 | 9 | mkdir Generated 10 | 11 | comma_separated_languages="" 12 | 13 | for lang in "${languages[@]}"; do 14 | rm -r "${lang}.lproj" 15 | comma_separated_languages+="${lang}," 16 | done 17 | 18 | comma_separated_languages=${comma_separated_languages%,} 19 | 20 | if [ -e "./credentials.json" ]; then 21 | if command -v gcloud > /dev/null 2>&1; then 22 | echo "gcloud is installed." 23 | else 24 | curl https://sdk.cloud.google.com | bash 25 | exec -l $SHELL 26 | fi 27 | 28 | gcloud auth activate-service-account --key-file=credentials.json 29 | 30 | ACCESS_TOKEN=$(gcloud auth print-access-token --scopes=https://www.googleapis.com/auth/drive.readonly) 31 | 32 | curl -L -H "Authorization: Bearer $ACCESS_TOKEN" "https://docs.google.com/spreadsheets/d/$SHEET_ID/export?exportFormat=csv&gid=$TABLE_ID" > localization.csv 33 | else 34 | curl -L "https://docs.google.com/spreadsheets/d/$SHEET_ID/export?exportFormat=csv&gid=$TABLE_ID" > localization.csv 35 | fi 36 | 37 | ./StringsGenerator localization.csv "${comma_separated_languages}" 38 | ./swiftgen strings en.lproj/Localizable.strings -o Generated/L10n-Constants.swift --templatePath localization-swift5.stencil --param publicAccess=true --param languages=${comma_separated_languages} 39 | --------------------------------------------------------------------------------