├── .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 | 
8 |
9 | ## To struct:
10 |
11 | 
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 | }
--------------------------------------------------------------------------------