├── .gitignore ├── LICENSE ├── README.md ├── SampleCSVFiles ├── intro.csv ├── login.csv └── register.csv ├── SampleOutput ├── localizables │ ├── en.lproj │ │ └── Localizable.strings │ ├── fr.lproj │ │ └── Localizable.strings │ └── th.lproj │ │ └── Localizable.strings └── struct │ └── Localizables.swift ├── Screen Shot 2559-10-13 at 1.56.55 AM.png ├── Screen Shot 2559-10-13 at 4.05.57 AM.png ├── Scripts ├── csv_localizer.py ├── genstruct.py ├── main.command └── settings.json └── google_sheets_export_csv.txt /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Wirawit Rueopas 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SwiftyLocalization 2 | A simple localization solution for iOS. Google Spreadsheets -> Localizable.strings -> Swift's struct. 3 | 4 | *Note: This project is quite old, it probably doesn't run out of the box (e.g., there will be bugs). You may use it just as an idea for a localization workflow. Or fork and fix it to suit your needs.* 5 | 6 | ## From Google Spreadsheet (Live here: [example sheet](https://docs.google.com/spreadsheets/d/1zB_tPPhUxbjB6sVpLmvgGVXdd-7d5mvfrOaCzgkhHv8/edit#gid=1689234848)): 7 | ![Alt text](https://raw.githubusercontent.com/aunnnn/SwiftyLocalization/master/Screen%20Shot%202559-10-13%20at%201.56.55%20AM.png "Online Sheet") 8 | 9 | ## To struct: 10 | 11 | ![Alt text](https://raw.githubusercontent.com/aunnnn/SwiftyLocalization/master/Screen%20Shot%202559-10-13%20at%204.05.57%20AM.png "Generated Struct") 12 | 13 | Yes, no human deserves to make this struct themselves... 14 | 15 | # Features 16 | - [x] Flexible localization with Google Spreadsheets - comment, color & fonts and much more 17 | - [x] Multiple Spreadsheets support - distinguish between pages/features/domains 18 | - [x] Generate Localizables.string - no more touching these files 19 | - [x] :tada: Generate Localizables.swift - a struct that manages all keys, decode and return localized String. **No more wrong keys out of nowhere, the compiler catches them for you!** 20 | 21 |
22 | For Android: 23 | - [x] Generate value-lang/strings.xml (not thoroughly tested yet) 24 | - [ ] Generate a counterpart of Localizables.swift - a helper to decode and return localized String. 25 | 26 | # Overview 27 | 28 | - Edit Google Spreadsheets 29 | - Spreadsheets -> csv files (Google App Script) 30 | - csv files -> local csv files (Synced by Google Drive app, or manually download) 31 | - local csv files -> Localizable.strings & Localizables.struct (Python script) 32 | 33 | 34 | # Steps 35 | 1. Edit Google Spreadsheets. Look at a guideline [here.](https://docs.google.com/spreadsheets/d/1zB_tPPhUxbjB6sVpLmvgGVXdd-7d5mvfrOaCzgkhHv8/edit?usp=sharing) 36 | 2. Export them to csv files with [Michael Derazon's Google script.](https://www.drzon.net/export-all-google-sheets-to-csv/) A slightly modified version is included here (google_sheets_export_csv.txt). Just copy to script editor and run it. After this, a folder named 'csvFiles' will waiting in your Drive folder. It contains all csv files from all sheets (sheetname.csv). Change the code, e.g. for folder names, as you need. 37 | 3. (Optional) For quick development iteration: 38 | 1. Sync 'csvFiles' with [Google Drive app for mac](https://www.google.com/drive/download/). So that everytime you run script to export csv, 'csvFiles' will be available locally on your local Drive's folder. 39 | 2. softlink (ln -s) your local Google Drive with: 40 | ````bash 41 | ln -s {path-to-your-csvFiles-in-local-Drive} {anywhere-near-your-xcode-project} 42 | ```` 43 | So that your project has kind of shortcut to 'csvFiles'. 44 | 4. Get csvFiles (either by 3. or download manually from Google Drive). 45 | 5. Generate Localizable.strings & Localizables.swift. 46 | 47 | Set your paths & how to decode a key to a localized string at settings.json. Then run this script: 48 | 49 | ````python 50 | python csv_localizer.py 51 | ```` 52 | Or you can just double-click on main.command (for mac), it will run this on terminal for you. 53 | 54 | 55 | **HOW IT WORKS** 56 | 57 | **TL;DR**: *Take a look at /SampleOutput/struct/Localizables.swift. Then you may not need to know about this.* 58 | 59 | What the script do is that it will find input path to csvFiles (path/name from settings.json), then it aggregates every .csv files inside that folder **recursively.** Then it generates {lang}.lproj/Localizable.strings to your output path. It prepends each key with a sheet name, so {sheetname}_{key} is the actual key inside our string files. 60 | 61 | 62 | The generated struct will be named Localizables.swift. Nested structs will have same name as sheet_name. Each has key as in your Spreadsheets. Each variable inside is a computed static var (so that everytime we get its value, it returns a localized string that reflects current in-app language). **For the struct to work, you need to specify how to decode a localized string given a key in settings.json.** For example, you can use NSLocalizedString. I use [this great library](https://github.com/marmelroy/Localize-Swift) to help manage in-app language setting. It has a String extension that allows me to use "key".localized() to return a string. I set "\"{key}\".localized()" in settings.json. 63 | 64 | 65 | To use. Suppose you have a sheet named 'login' with a key 'button_title_register' inside, you can retrieve it like: 66 | ````Swift 67 | Localizables.login.button_title_register 68 | ```` 69 | 70 | 71 | Settings.json 72 | --- 73 | 74 | - BASE_STRING_PATH: a base path 75 | - IN_PATH: a folder of csv files, relative to the base path 76 | - OUT_PATH: a folder to output localizables, relative to the base path 77 | - PLATFORM: ios or android (android will generate strings.xml) 78 | - LANG_KEYS: array of valid language codes. e.g. ["en", "fr", "th"]. It will be used as {lang}.lproj for ios, and values-{lang} for android. 79 | - GEN_STRUCT_IF_IOS: whether to generate Localizables.swift or not (true or false) 80 | - GEN_STRUCT_BASE_PATH: a base path for struct generation 81 | - GEN_STRUCT_OUT_PATH: a folder to put a struct 82 | - GEN_STRUCT_FILE_NAME: a name of struct file. e.g. Localizables.swift 83 | - GEN_STRUCT_STRUCT_NAME: name of struct. e.g. Localizables 84 | - **GEN_STRUCT_VALUE_RETRIEVAL: specify a way to decode localized string from a key.** E.g. if you have a function "key".localized() to get a value. You can set this field to "\"{key}\".localized()". Or use plain NSLocalizedString like "NSLocalizedString(\"{key}\", tableName: nil, bunedle: bundle)" 85 | 86 | Benefits 87 | --- 88 | * Format Google Spreadsheets as you like. As an idea, fill unfinished translation cell with red color. Add comments. Add border to visually group a set of related keys. Make some important keys with bigger fonts. Etc. 89 | * No out-of-sync between different language files since we don't touch Localizable.strings anymore. 90 | * Having struct as an interface to localized strings allows us to: 91 | * Use autocomplete 92 | * Xcode won't compile if there's a wrong key in the project. 93 | 94 | --- 95 | :sweat_smile: Phews! That looks complicated and feels like home-made solution. That's the nature any free solutions. But trust me, it's really worth the hassles. Imagine this, that moment when you add a new key-value in a spreadsheet, click export csvFiles. Wait for them to sync on your local folder. And double click on main.command to update all files & struct with the latest version. Boom, that new key is avaiable instantly through Xcode Autocomplete! 96 | 97 | Contribution 98 | --- 99 | All suggestions/helps are welcome! 100 | -------------------------------------------------------------------------------- /SampleCSVFiles/intro.csv: -------------------------------------------------------------------------------- 1 | key,en,th,fr,,,,,, 2 | ,,,,,,,,, 3 | //Alert,,,,,,,,, 4 | alert_title_success,Successfully logged in.,คุณได้ล็อกอินเรียบร้อย,connecté avec succès,,,,,, 5 | alert_title_ok,Ok,โอเค,D'accord,,,,,, 6 | alert_title_close,Close,ปิด,Fermer,,,,,, 7 | ,,,,,,,,, 8 | ,,,,,,,,, 9 | button_title_register,Register,สมัครสมาชิก,registre,,,,,, 10 | ,,,,,,,,, 11 | ,,,,,,,,, 12 | Rules,,,,,,,,, 13 | "// 0. First of all, Take a quick look at 'register' and 'login' sheet to get the feeling, then come back for more details",,,,,,,,, 14 | ,,,,,,,,, 15 | "1. You can comment anywhere as long as you don't fill in all columns. (The python script will ignore incomplete rows, thus allows us to use as comments)",,,,,,,,, 16 | (incomplete row = a row that has empty column),,,,,,,,, 17 | ,,,,,,,,, 18 | ...So you can do something like:,,,,,,,,, 19 | 1.1. This,,,,,,,,, 20 | ,1.2 Also this,,,,,,,,Sky is the limit... 21 | ,,,,1.3 Even this...,,,,, 22 | ,,,,,,,,, 23 | "2. From rule 1., It means you can have many empty rows as you want!",,,,,,,,, 24 | ,,,,,,,,, 25 | 3. We can use any format & any background colors,,,,,,,,, 26 | "3.1 Like this,",,,,,,,,, 27 | ,,and this.,,,,,,, 28 | "3.2 This makes it ideals to remind us of unfinished translation, so we could do something like this in case you don't know how to say hello in france yet:",,,,,,,,, 29 | ,,,,,,,,, 30 | text_hello,Hello,สวัสดี,Hello(?),,,,,, 31 | ,,,,,,,,, 32 | "// 4. IMPORTANT: If your comment (even in the first column) has many commas (','), It could mess up generated csv files. E.g. Those comments could be considered as valid row.",,,,,,,,, 33 | "SOLUTION: Use // On the first column to force ignore. However, just restrain yourself from using it. Just use short, simple term without comma",,,,,,,,, 34 | "// Like this, will be, ok, even if, it has, many commas, alright.",,,,,,,,, 35 | "// So, from now on, I will use // for verbose sentences with a lot of commas to prevent it messing up with my actual keys",,,,,,,,, 36 | "// 5. Different sheets can be used as different domains/features in your app. E.g. home, register, login, messaging, setting, profile, alert, others.",,,,,,,,, 37 | // 5.1 You can have duplicated keys between different sheets. Because all keys will finally be prepended with sheet name. E.g. text_hello in this sheet is finally ---> intro_text_hello,,,,,,,,, 38 | "// 5.2 Thus, name a sheet without special characters!",,,,,,,,, 39 | // 5.3 Don't duplicate keys in the same sheet. Prepend by subdomain to distinguish them.,,,,,,,,, 40 | ,,,,,,,,, 41 | ,,,,,,,,, 42 | // Guidelines For developers:,,,,,,,,, 43 | // 1. Just remember that each sheet will eventually be turned into plain .csv files.,,,,,,,,, 44 | "// 2. In setting.json, we specify LANG_KEYS as ["en", "th", "fr"]",,,,,,,,, 45 | "// 3. Our python scripts will split csv into arrays and look up language in that order: so index 0 => key, index 1 => en, 2 => th, 3 => fr",,,,,,,,, 46 | "// 4. It will then check for value at index 1, 2, 3. If any of them is empty, that row is completely ignored.",,,,,,,,, 47 | ,,,,,,,,, 48 | ----------------------------------------------------------------------------------------------------------------,,,,,,,,, 49 | Thanks for your interests in this project. Any questions could direct to the github repo,,,,,,,,, 50 | https://github.com/aunnnn/SwiftyLocalization,,,,,,,,, 51 | Wirawit Rueopas (aunnn),,,,,,,,, -------------------------------------------------------------------------------- /SampleCSVFiles/login.csv: -------------------------------------------------------------------------------- 1 | key,en,th,fr, 2 | ,,,, 3 | vc_title,Login,เข้าสู่ระบบ,s'identifier,<- You can also use highlight for unsure translation 4 | vc_detail,Fill username & password,กรอกข้อมูลให้ครบ,remplir le nom d'utilisateur et mot de passe, 5 | ,,,, 6 | // Notice that you can use same key in different sheet (vc_title is both in 'register' and 'login') Because a generated key will be prepended by sheet name.,,,, -------------------------------------------------------------------------------- /SampleCSVFiles/register.csv: -------------------------------------------------------------------------------- 1 | key,en,th,fr,,(Note: I simply use google translate for fr) 2 | ,,,,, 3 | VC,,,,,<-- you can use any row as a comment as long as some column is empty 4 | vc_title,Register here,สมัครสมาชิกที่นี่,Inscrivez-vous ici,, 5 | vc_detail,Please provide all required information.,กรุณากรอกข้อมูลให้ครบทุกช่อง,S'il vous plaît fournir toutes les informations requises.,, 6 | ,,,,, 7 | ,,,,, 8 | ,,,,, 9 | ,,,,,<-- you can have any empty rows as you needed 10 | ,,,,, 11 | ,,,,, 12 | FORM,,,,,<-- you can use any font/color 13 | form_title_first_name,First name,ชื่อ,Prénom,, 14 | form_title_last_name,Last name,นามสกุล,nom de famille,, 15 | form_title_phone,Phone,เบอร์ติดต่อ,numéro de téléphone,, 16 | ,,,,, 17 | form_button_register,Register,ลงทะเบียน,registre,, 18 | ,,,,, 19 | form_alert_required_field,Required Field:,กรุณากรอก:,champs requis:,, 20 | form_alert_button_close,Close,ปิด,Fermer,, 21 | ,,,,, 22 | form_hud_loading,Loading,กำลังโหลด,en cours,, 23 | ,,,,, 24 | form_hud_account_ready,Your account is ready!,บัญชีของคุณพร้อมใช้งานแล้ว!,Your account is ready (in france),,<-- you can use highlight to remind unfinished rows 25 | ,,,,, 26 | form_hud_please_wait,Please wait,โปรดรอสักครู่,,,"<-- or just leave an empty value, so that this row is completely ignored by python script" -------------------------------------------------------------------------------- /SampleOutput/localizables/en.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | /* AUTO-GENERATED: 2016-10-13, 01:20 */ 5 | 6 | 7 | 8 | 9 | /* intro */ 10 | 11 | "intro_alert_title_success" = "Successfully logged in."; 12 | "intro_alert_title_ok" = "Ok"; 13 | "intro_alert_title_close" = "Close"; 14 | "intro_button_title_register" = "Register"; 15 | "intro_text_hello" = "Hello"; 16 | 17 | 18 | 19 | /* login */ 20 | 21 | "login_vc_title" = "Login"; 22 | "login_vc_detail" = "Fill username & password"; 23 | 24 | 25 | 26 | /* register */ 27 | 28 | "register_vc_title" = "Register here"; 29 | "register_vc_detail" = "Please provide all required information."; 30 | "register_form_title_first_name" = "First name"; 31 | "register_form_title_last_name" = "Last name"; 32 | "register_form_title_phone" = "Phone"; 33 | "register_form_button_register" = "Register"; 34 | "register_form_alert_required_field" = "Required Field:"; 35 | "register_form_alert_button_close" = "Close"; 36 | "register_form_hud_loading" = "Loading"; 37 | "register_form_hud_account_ready" = "Your account is ready!"; 38 | -------------------------------------------------------------------------------- /SampleOutput/localizables/fr.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | /* AUTO-GENERATED: 2016-10-13, 01:20 */ 5 | 6 | 7 | 8 | 9 | /* intro */ 10 | 11 | "intro_alert_title_success" = "connecté avec succès"; 12 | "intro_alert_title_ok" = "D'accord"; 13 | "intro_alert_title_close" = "Fermer"; 14 | "intro_button_title_register" = "registre"; 15 | "intro_text_hello" = "Hello(?)"; 16 | 17 | 18 | 19 | /* login */ 20 | 21 | "login_vc_title" = "s'identifier"; 22 | "login_vc_detail" = "remplir le nom d'utilisateur et mot de passe"; 23 | 24 | 25 | 26 | /* register */ 27 | 28 | "register_vc_title" = "Inscrivez-vous ici"; 29 | "register_vc_detail" = "S'il vous plaît fournir toutes les informations requises."; 30 | "register_form_title_first_name" = "Prénom"; 31 | "register_form_title_last_name" = "nom de famille"; 32 | "register_form_title_phone" = "numéro de téléphone"; 33 | "register_form_button_register" = "registre"; 34 | "register_form_alert_required_field" = "champs requis:"; 35 | "register_form_alert_button_close" = "Fermer"; 36 | "register_form_hud_loading" = "en cours"; 37 | "register_form_hud_account_ready" = "Your account is ready (in france)"; 38 | -------------------------------------------------------------------------------- /SampleOutput/localizables/th.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | /* AUTO-GENERATED: 2016-10-13, 01:20 */ 5 | 6 | 7 | 8 | 9 | /* intro */ 10 | 11 | "intro_alert_title_success" = "คุณได้ล็อกอินเรียบร้อย"; 12 | "intro_alert_title_ok" = "โอเค"; 13 | "intro_alert_title_close" = "ปิด"; 14 | "intro_button_title_register" = "สมัครสมาชิก"; 15 | "intro_text_hello" = "สวัสดี"; 16 | 17 | 18 | 19 | /* login */ 20 | 21 | "login_vc_title" = "เข้าสู่ระบบ"; 22 | "login_vc_detail" = "กรอกข้อมูลให้ครบ"; 23 | 24 | 25 | 26 | /* register */ 27 | 28 | "register_vc_title" = "สมัครสมาชิกที่นี่"; 29 | "register_vc_detail" = "กรุณากรอกข้อมูลให้ครบทุกช่อง"; 30 | "register_form_title_first_name" = "ชื่อ"; 31 | "register_form_title_last_name" = "นามสกุล"; 32 | "register_form_title_phone" = "เบอร์ติดต่อ"; 33 | "register_form_button_register" = "ลงทะเบียน"; 34 | "register_form_alert_required_field" = "กรุณากรอก:"; 35 | "register_form_alert_button_close" = "ปิด"; 36 | "register_form_hud_loading" = "กำลังโหลด"; 37 | "register_form_hud_account_ready" = "บัญชีของคุณพร้อมใช้งานแล้ว!"; 38 | -------------------------------------------------------------------------------- /SampleOutput/struct/Localizables.swift: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | /* AUTO-GENERATED: 2016-10-13, 01:20 */ 5 | 6 | struct Localizables { 7 | struct intro { 8 | static var alert_title_success: String { 9 | return "intro_alert_title_success".localized() 10 | } 11 | static var alert_title_ok: String { 12 | return "intro_alert_title_ok".localized() 13 | } 14 | static var alert_title_close: String { 15 | return "intro_alert_title_close".localized() 16 | } 17 | static var button_title_register: String { 18 | return "intro_button_title_register".localized() 19 | } 20 | static var text_hello: String { 21 | return "intro_text_hello".localized() 22 | } 23 | } 24 | 25 | struct login { 26 | static var vc_title: String { 27 | return "login_vc_title".localized() 28 | } 29 | static var vc_detail: String { 30 | return "login_vc_detail".localized() 31 | } 32 | } 33 | 34 | struct register { 35 | static var vc_title: String { 36 | return "register_vc_title".localized() 37 | } 38 | static var vc_detail: String { 39 | return "register_vc_detail".localized() 40 | } 41 | static var form_title_first_name: String { 42 | return "register_form_title_first_name".localized() 43 | } 44 | static var form_title_last_name: String { 45 | return "register_form_title_last_name".localized() 46 | } 47 | static var form_title_phone: String { 48 | return "register_form_title_phone".localized() 49 | } 50 | static var form_button_register: String { 51 | return "register_form_button_register".localized() 52 | } 53 | static var form_alert_required_field: String { 54 | return "register_form_alert_required_field".localized() 55 | } 56 | static var form_alert_button_close: String { 57 | return "register_form_alert_button_close".localized() 58 | } 59 | static var form_hud_loading: String { 60 | return "register_form_hud_loading".localized() 61 | } 62 | static var form_hud_account_ready: String { 63 | return "register_form_hud_account_ready".localized() 64 | } 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /Screen Shot 2559-10-13 at 1.56.55 AM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aunnnn/SwiftyLocalization/b30d5f2b8318a26ee01629c154490dd95101947d/Screen Shot 2559-10-13 at 1.56.55 AM.png -------------------------------------------------------------------------------- /Screen Shot 2559-10-13 at 4.05.57 AM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aunnnn/SwiftyLocalization/b30d5f2b8318a26ee01629c154490dd95101947d/Screen Shot 2559-10-13 at 4.05.57 AM.png -------------------------------------------------------------------------------- /Scripts/csv_localizer.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import os 3 | import sys 4 | import csv 5 | import json 6 | import genstruct 7 | 8 | 9 | NOW = datetime.datetime.now().strftime("%Y-%m-%d, %H:%M") 10 | CURRENT_DIR = os.path.dirname(__file__) 11 | 12 | def main(): 13 | print "Localizing..." 14 | with open(os.path.join(CURRENT_DIR, "settings.json")) as setting_file: 15 | setting = json.load(setting_file) 16 | 17 | PLATFORM = setting["PLATFORM"] 18 | IN_PATH = setting["IN_PATH"] 19 | OUT_PATH = setting["OUT_PATH"] 20 | LANG_KEYS = setting["LANG_KEYS"] 21 | BASE_STRINGS_PATH = setting["BASE_STRINGS_PATH"] 22 | if BASE_STRINGS_PATH == "currentdir": 23 | print "Detect currentdir -> \n " + CURRENT_DIR 24 | BASE_STRINGS_PATH = CURRENT_DIR 25 | 26 | GEN_STRUCT_IF_IOS = setting["GEN_STRUCT_IF_IOS"] 27 | 28 | print "Platform: {0}".format(PLATFORM) 29 | print "In path: {0}".format(IN_PATH) 30 | print "Out path: {0}".format(OUT_PATH) 31 | print "Lang keys: {0}".format(LANG_KEYS) 32 | print "------------------------------------\nSTART..." 33 | 34 | if PLATFORM == "ios": 35 | localize_ios(BASE_STRINGS_PATH, IN_PATH, OUT_PATH, LANG_KEYS) 36 | else: 37 | localize_android(BASE_STRINGS_PATH, IN_PATH, OUT_PATH, LANG_KEYS) 38 | 39 | print 'DONE LOCALIZING.' 40 | print '\n\n' 41 | 42 | if PLATFORM.lower() == "ios" and GEN_STRUCT_IF_IOS: 43 | genstruct.main() 44 | 45 | def localize_ios(BASE_PATH, IN_PATH, OUT_PATH, LANG_KEYS): 46 | 47 | base_out_dir = os.path.join(BASE_PATH, OUT_PATH) 48 | # top most 49 | if not os.path.exists(base_out_dir): 50 | os.makedirs(base_out_dir) 51 | 52 | # each languages 53 | for lang in LANG_KEYS: 54 | lang_path = os.path.join(base_out_dir, "{0}.lproj/".format(lang)) 55 | if not os.path.exists(lang_path): 56 | os.makedirs(lang_path) 57 | 58 | 59 | full_out_paths = [os.path.join(base_out_dir, "{0}.lproj/".format(langKey) + "Localizable.strings") for langKey in LANG_KEYS] 60 | allwrites = [open(out_path, 'w') for out_path in full_out_paths] 61 | 62 | for dirname, dirnames, filenames in os.walk(os.path.join(CURRENT_DIR, IN_PATH)): 63 | 64 | [fwrite.write('\n\n\n/* AUTO-GENERATED: {timestamp} */\n\n'.format(timestamp=NOW)) for fwrite in allwrites] 65 | 66 | for f in filenames: 67 | filename, ext = os.path.splitext(f) 68 | if ext != '.csv': 69 | continue 70 | 71 | fullpath = os.path.join(dirname, f) 72 | print 'Localizing: ' + filename + ' ...' 73 | 74 | with open(fullpath, 'rb') as csvfile: 75 | [fwrite.write('\n\n\n/* {0} */\n\n'.format(filename)) for fwrite in allwrites] 76 | 77 | reader = csv.reader(csvfile, delimiter=',') 78 | 79 | iterrows = iter(reader); 80 | next(iterrows) # skip first line (it is header). 81 | 82 | for row in iterrows: 83 | row_key = row[0].replace(" ", "") 84 | # comment 85 | if row_key[:2] == '//': 86 | continue 87 | 88 | row_values = [row[i+1] for i in range(len(LANG_KEYS))] 89 | 90 | # if any row is empty, skip it! 91 | if any([value == "" for value in row_values]): 92 | continue 93 | [fwrite.write('"{domain}_{key}" = "{lang}";\n'.format(domain=filename, key=row_key, lang=row_values[idx])) for idx, fwrite in enumerate(allwrites)] 94 | [fwrite.close() for fwrite in allwrites] 95 | 96 | 97 | def localize_android(BASE_PATH, IN_PATH, OUT_PATH, LANG_KEYS): 98 | base_out_dir = os.path.join(BASE_PATH, OUT_PATH) 99 | # top most 100 | if not os.path.exists(base_out_dir): 101 | os.makedirs(base_out_dir) 102 | 103 | # each languages 104 | for lang in LANG_KEYS: 105 | lang_path = os.path.join(base_out_dir, "values-{0}/".format(lang)) 106 | if not os.path.exists(lang_path): 107 | os.makedirs(lang_path) 108 | 109 | full_out_paths = [os.path.join(base_out_dir, "values-{0}/".format(langKey) + "strings.xml") for langKey in LANG_KEYS] 110 | allwrites = [open(out_path, 'w') for out_path in full_out_paths] 111 | 112 | for dirname, dirnames, filenames in os.walk(os.path.join(CURRENT_DIR, IN_PATH)): 113 | 114 | [fwrite.write('\n') for fwrite in allwrites] 115 | [fwrite.write('') for fwrite in allwrites] 116 | 117 | for f in filenames: 118 | filename, ext = os.path.splitext(f) 119 | if ext != '.csv': 120 | continue 121 | fullpath = os.path.join(dirname, f) 122 | print 'Localizing: ' + filename + ' ...' 123 | 124 | with open(fullpath, 'rb') as csvfile: 125 | [fwrite.write('\n\n\n\n\n'.format(filename)) for fwrite in allwrites] 126 | 127 | reader = csv.reader(csvfile, delimiter=',') 128 | 129 | iterrows = iter(reader); 130 | next(iterrows) # skip first line (it is header). 131 | for row in iterrows: 132 | row_key = row[0] 133 | 134 | # comment 135 | if row_key[:2] == '//': 136 | continue 137 | 138 | row_values = [row[i+1] for i in range(len(LANG_KEYS))] 139 | 140 | # if any row is empty, skip it! 141 | if any([value == "" for value in row_values]): 142 | continue 143 | 144 | [fwrite.write('\t{lang}\n'.format(domain=filename, key=row_key, lang=row_values[idx])) for idx, fwrite in enumerate(allwrites)] 145 | [fwrite.write('') for fwrite in allwrites] 146 | [fwrite.close() for fwrite in allwrites] 147 | 148 | 149 | if __name__ == '__main__': 150 | main() -------------------------------------------------------------------------------- /Scripts/genstruct.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import os 3 | import csv 4 | import json 5 | 6 | NOW = datetime.datetime.now().strftime("%Y-%m-%d, %H:%M") 7 | CURRENT_DIR = os.path.dirname(__file__) 8 | 9 | def main(): 10 | print "Generating struct for iOS..." 11 | with open(os.path.join(CURRENT_DIR,"settings.json")) as setting_file: 12 | setting = json.load(setting_file) 13 | 14 | IN_PATH = setting["IN_PATH"] 15 | LANG_KEYS = setting["LANG_KEYS"] 16 | 17 | GEN_STRUCT_BASE_PATH = setting["GEN_STRUCT_BASE_PATH"] 18 | GEN_STRUCT_OUT_PATH = setting["GEN_STRUCT_OUT_PATH"] 19 | GEN_STRUCT_FILENAME = setting["GEN_STRUCT_FILENAME"] 20 | GEN_STRUCT_VALUE_RETRIEVAL = setting["GEN_STRUCT_VALUE_RETRIEVAL"] 21 | GEN_STRUCT_STRUCT_NAME = setting["GEN_STRUCT_STRUCT_NAME"] 22 | 23 | 24 | if GEN_STRUCT_BASE_PATH == "currentdir": 25 | print "Detect currentdir -> \n " + CURRENT_DIR 26 | GEN_STRUCT_BASE_PATH = CURRENT_DIR 27 | 28 | struct_path = os.path.join(GEN_STRUCT_BASE_PATH, GEN_STRUCT_OUT_PATH, GEN_STRUCT_FILENAME) 29 | folder_path = os.path.join(GEN_STRUCT_BASE_PATH, GEN_STRUCT_OUT_PATH) 30 | 31 | if not os.path.exists(folder_path): 32 | os.makedirs(folder_path) 33 | 34 | fwrite = open(struct_path, 'w') 35 | 36 | for dirname, dirnames, filenames in os.walk(os.path.join(CURRENT_DIR, IN_PATH)): 37 | 38 | fwrite.write("\n\n\n/* AUTO-GENERATED: {timestamp} */\n\n".format(timestamp=NOW)) 39 | fwrite.write("struct {0} {{\n".format(GEN_STRUCT_STRUCT_NAME)) 40 | # for each .csv files 41 | for f in filenames: 42 | filename, ext = os.path.splitext(f) 43 | if ext != '.csv': 44 | continue 45 | fullpath = os.path.join(dirname, f) 46 | print 'Localizing: ' + filename + ' ...' 47 | 48 | fwrite.write('\tstruct {0} {{\n'.format(filename)) 49 | 50 | with open(fullpath, 'rb') as csvfile: 51 | 52 | reader = csv.reader(csvfile, delimiter=',') 53 | 54 | iterrows = iter(reader); 55 | next(iterrows) # skip first line (it is header). 56 | 57 | # for each line 58 | for row in iterrows: 59 | row_key = row[0].replace(" ", "") 60 | # comment 61 | if row_key[:2] == '//': 62 | continue 63 | 64 | row_values = [row[i+1] for i in range(len(LANG_KEYS))] 65 | 66 | # if any row is empty, skip it! 67 | if any([value == "" for value in row_values]): 68 | continue 69 | 70 | full_key = "{domain}_{key}".format(domain=filename, key=row_key) 71 | 72 | fwrite.write('\t\tstatic var {0}: String {{\n'.format(row_key)) 73 | fwrite.write('\t\t\treturn {0}'.format(GEN_STRUCT_VALUE_RETRIEVAL).format(key=full_key) + '\n') 74 | fwrite.write('\t\t}\n') 75 | 76 | 77 | fwrite.write('\t}\n\n') # e.g., home_care 78 | 79 | fwrite.write('}\n') # HSLocalization 80 | 81 | fwrite.close() 82 | 83 | print 'DONE GENERATE STRUCT FOR IOS.' 84 | 85 | if __name__ == '__main__': 86 | main() -------------------------------------------------------------------------------- /Scripts/main.command: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | import csv_localizer 6 | 7 | if __name__ == '__main__': 8 | csv_localizer.main() -------------------------------------------------------------------------------- /Scripts/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "BASE_STRINGS_PATH": "currentdir", 3 | "IN_PATH": "../SampleCSVFiles", 4 | "OUT_PATH": "../SampleOutput/localizables", 5 | 6 | "PLATFORM": "ios", 7 | "LANG_KEYS": ["en", "th", "fr"], 8 | 9 | 10 | "GEN_STRUCT_IF_IOS": true, 11 | 12 | "GEN_STRUCT_BASE_PATH": "currentdir", 13 | "GEN_STRUCT_OUT_PATH": "../SampleOutput/struct", 14 | "GEN_STRUCT_FILENAME": "Localizables.swift", 15 | 16 | "GEN_STRUCT_STRUCT_NAME": "Localizables", 17 | 18 | "GEN_STRUCT_VALUE_RETRIEVAL": "\"{key}\".localized()" 19 | } 20 | -------------------------------------------------------------------------------- /google_sheets_export_csv.txt: -------------------------------------------------------------------------------- 1 | /* 2 | * script to export data in all sheets in the current spreadsheet as individual csv files 3 | * files will be named according to the name of the sheet 4 | * author: Michael Derazon 5 | */ 6 | 7 | /* 8 | * Adapted by Aunn: 9 | * 10 | * To use, click on 'Run' menu, then select 'saveAsCSV'. 11 | * 12 | * Zip file of all CSVs will be at google drive folder. 13 | * 14 | */ 15 | 16 | 17 | 18 | function onOpen() { 19 | var ss = SpreadsheetApp.getActiveSpreadsheet(); 20 | var csvMenuEntries = [{name: "export as csv files", functionName: "saveAsCSV"}]; 21 | ss.addMenu("csv", csvMenuEntries); 22 | }; 23 | 24 | /* 25 | function removeOlderFile() { 26 | var files = DriveApp.getFiles(); 27 | var fileName = "csvFiles"; 28 | while(files.hasNext()){ 29 | var file = files.next(); 30 | if(file.getName() === "csvFiles"){ 31 | DriveApp.removeFile(file); 32 | break; 33 | } 34 | } 35 | } 36 | */ 37 | function saveAsCSV() { 38 | var ss = SpreadsheetApp.getActiveSpreadsheet(); 39 | var sheets = ss.getSheets(); 40 | // create a folder from the name of the spreadsheet 41 | // var folder = DriveApp.createFolder(ss.getName().toLowerCase().replace(/ /g,'_') + '_csv_' + new Date().getTime()); 42 | // Firstly, clear older folder. 43 | // removeOlderFile(); 44 | 45 | // var folder = DriveApp.createFolder('csvFiles'); 46 | var folder = DriveApp.getFoldersByName("csvFiles").next(); 47 | 48 | for (var i = 0 ; i < sheets.length ; i++) { 49 | var sheet = sheets[i]; 50 | // append ".csv" extension to the sheet name 51 | fileName = sheet.getName() + ".csv"; 52 | 53 | // remove older file 54 | var olderFiles = DriveApp.getFilesByName(fileName); 55 | while(olderFiles.hasNext()) { 56 | olderFiles.next().setTrashed(true); 57 | } 58 | 59 | // convert all available sheet data to csv format 60 | var csvFile = convertRangeToCsvFile_(fileName, sheet); 61 | // create a file in the Docs List with the given name and the csv data 62 | folder.createFile(fileName, csvFile); 63 | } 64 | Browser.msgBox('Files are waiting in a folder named ' + folder.getName()); 65 | } 66 | 67 | function convertRangeToCsvFile_(csvFileName, sheet) { 68 | // get available data range in the spreadsheet 69 | var activeRange = sheet.getDataRange(); 70 | try { 71 | var data = activeRange.getValues(); 72 | var csvFile = undefined; 73 | 74 | // loop through the data in the range and build a string with the csv data 75 | if (data.length > 1) { 76 | var csv = ""; 77 | for (var row = 0; row < data.length; row++) { 78 | for (var col = 0; col < data[row].length; col++) { 79 | if (data[row][col].toString().indexOf(",") != -1) { 80 | data[row][col] = "\"" + data[row][col] + "\""; 81 | } 82 | } 83 | 84 | // join each row's columns 85 | // add a carriage return to end of each row, except for the last one 86 | if (row < data.length-1) { 87 | csv += data[row].join(",") + "\r\n"; 88 | } 89 | else { 90 | csv += data[row]; 91 | } 92 | } 93 | csvFile = csv; 94 | } 95 | return csvFile; 96 | } 97 | catch(err) { 98 | Logger.log(err); 99 | Browser.msgBox(err); 100 | } 101 | } --------------------------------------------------------------------------------