├── Images ├── download_translations.png ├── generate_strings.png ├── project_properties.png └── upload_strings.png ├── OneSkyPlugin.xcplugin └── Contents │ ├── Info.plist │ ├── MacOS │ └── OneSkyPlugin │ └── Resources │ ├── OSDownloadTranslationsView.nib │ ├── OSGenerateStringsFilesView.nib │ ├── OSProgressWindow.nib │ ├── OSProjectPropertiesView.nib │ ├── OSUploadFilesView.nib │ ├── en.lproj │ └── InfoPlist.strings │ ├── icon144.png │ └── merge_files.py └── README.md /Images/download_translations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onesky/plugin-xcode/8b5cdefe477ccf43eed28496f30621abae57e124/Images/download_translations.png -------------------------------------------------------------------------------- /Images/generate_strings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onesky/plugin-xcode/8b5cdefe477ccf43eed28496f30621abae57e124/Images/generate_strings.png -------------------------------------------------------------------------------- /Images/project_properties.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onesky/plugin-xcode/8b5cdefe477ccf43eed28496f30621abae57e124/Images/project_properties.png -------------------------------------------------------------------------------- /Images/upload_strings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onesky/plugin-xcode/8b5cdefe477ccf43eed28496f30621abae57e124/Images/upload_strings.png -------------------------------------------------------------------------------- /OneSkyPlugin.xcplugin/Contents/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildMachineOSBuild 6 | 15E65 7 | CFBundleDevelopmentRegion 8 | English 9 | CFBundleExecutable 10 | OneSkyPlugin 11 | CFBundleIdentifier 12 | com.oneskyapp.OneSkyPlugin 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | OneSkyPlugin 17 | CFBundlePackageType 18 | BNDL 19 | CFBundleShortVersionString 20 | 1.8.8 21 | CFBundleSignature 22 | ???? 23 | CFBundleSupportedPlatforms 24 | 25 | MacOSX 26 | 27 | CFBundleVersion 28 | 67 29 | DTCompiler 30 | com.apple.compilers.llvm.clang.1_0 31 | DTPlatformBuild 32 | 7D1014 33 | DTPlatformVersion 34 | GM 35 | DTSDKBuild 36 | 15E60 37 | DTSDKName 38 | macosx10.11 39 | DTXcode 40 | 0731 41 | DTXcodeBuild 42 | 7D1014 43 | DVTPlugInCompatibilityUUIDs 44 | 45 | FEC992CC-CA4A-4CFD-8881-77300FCB848A 46 | C4A681B0-4A26-480E-93EC-1218098B9AA0 47 | A2E4D43F-41F4-4FB9-BB94-7177011C9AED 48 | AD68E85B-441B-4301-B564-A45E4919A6AD 49 | 63FC1C47-140D-42B0-BB4D-A10B2D225574 50 | 37B30044-3B14-46BA-ABAA-F01000C27B63 51 | 640F884E-CE55-4B40-87C0-8869546CAB7A 52 | 992275C1-432A-4CF7-B659-D84ED6D42D3F 53 | A16FF353-8441-459E-A50C-B071F53F51B7 54 | 9F75337B-21B4-4ADC-B558-F9CADF7073A7 55 | 992275C1-432A-4CF7-B659-D84ED6D42D3F 56 | E969541F-E6F9-4D25-8158-72DC3545A6C6 57 | 8DC44374-2B35-4C57-A6FE-2AD66A36AAD9 58 | F41BD31E-2683-44B8-AE7F-5F09E919790E 59 | 7FDF5C7A-131F-4ABB-9EDC-8C5F8F0B8A90 60 | AABB7188-E14E-4433-AD3B-5CD791EAD9A3 61 | 0420B86A-AA43-4792-9ED0-6FE0F2B16A13 62 | 7265231C-39B4-402C-89E1-16167C4CC990 63 | ACA8656B-FEA8-4B6D-8E4A-93F4C95C362C 64 | 65 | NSPrincipalClass 66 | OneSkyPlugin 67 | XC4Compatible 68 | 69 | XC5Compatible 70 | 71 | XCGCReady 72 | 73 | XCPluginHasUI 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /OneSkyPlugin.xcplugin/Contents/MacOS/OneSkyPlugin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onesky/plugin-xcode/8b5cdefe477ccf43eed28496f30621abae57e124/OneSkyPlugin.xcplugin/Contents/MacOS/OneSkyPlugin -------------------------------------------------------------------------------- /OneSkyPlugin.xcplugin/Contents/Resources/OSDownloadTranslationsView.nib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onesky/plugin-xcode/8b5cdefe477ccf43eed28496f30621abae57e124/OneSkyPlugin.xcplugin/Contents/Resources/OSDownloadTranslationsView.nib -------------------------------------------------------------------------------- /OneSkyPlugin.xcplugin/Contents/Resources/OSGenerateStringsFilesView.nib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onesky/plugin-xcode/8b5cdefe477ccf43eed28496f30621abae57e124/OneSkyPlugin.xcplugin/Contents/Resources/OSGenerateStringsFilesView.nib -------------------------------------------------------------------------------- /OneSkyPlugin.xcplugin/Contents/Resources/OSProgressWindow.nib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onesky/plugin-xcode/8b5cdefe477ccf43eed28496f30621abae57e124/OneSkyPlugin.xcplugin/Contents/Resources/OSProgressWindow.nib -------------------------------------------------------------------------------- /OneSkyPlugin.xcplugin/Contents/Resources/OSProjectPropertiesView.nib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onesky/plugin-xcode/8b5cdefe477ccf43eed28496f30621abae57e124/OneSkyPlugin.xcplugin/Contents/Resources/OSProjectPropertiesView.nib -------------------------------------------------------------------------------- /OneSkyPlugin.xcplugin/Contents/Resources/OSUploadFilesView.nib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onesky/plugin-xcode/8b5cdefe477ccf43eed28496f30621abae57e124/OneSkyPlugin.xcplugin/Contents/Resources/OSUploadFilesView.nib -------------------------------------------------------------------------------- /OneSkyPlugin.xcplugin/Contents/Resources/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onesky/plugin-xcode/8b5cdefe477ccf43eed28496f30621abae57e124/OneSkyPlugin.xcplugin/Contents/Resources/en.lproj/InfoPlist.strings -------------------------------------------------------------------------------- /OneSkyPlugin.xcplugin/Contents/Resources/icon144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onesky/plugin-xcode/8b5cdefe477ccf43eed28496f30621abae57e124/OneSkyPlugin.xcplugin/Contents/Resources/icon144.png -------------------------------------------------------------------------------- /OneSkyPlugin.xcplugin/Contents/Resources/merge_files.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # ------------------------------------------------------------------------------ 5 | # @author Markus Chmelar 6 | # @date 2012-12-23 7 | # @version 1 8 | # ------------------------------------------------------------------------------ 9 | 10 | ''' 11 | Copyright (c) 2012 Markus Chmelar / Innovaptor OG 12 | 13 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 18 | 19 | ''' 20 | # -- Import -------------------------------------------------------------------- 21 | # Regular Expressions 22 | import re 23 | # Operation Systems and Path Operations 24 | import os 25 | # System Utilities 26 | import sys 27 | # Creating and using Temporal File 28 | import tempfile 29 | # Running Commands on the Commandline 30 | import subprocess 31 | # Opening Files with different Encodings 32 | import codecs 33 | # Commandline Options parser 34 | import optparse 35 | # High Level File Operations 36 | import shutil 37 | # Logging 38 | import logging 39 | # Doc-Tests 40 | import doctest 41 | 42 | # -- Class --------------------------------------------------------------------- 43 | 44 | 45 | class LocalizedStringLineParser(object): 46 | ''' Parses single lines and creates LocalizedString objects from them''' 47 | def __init__(self): 48 | # Possible Parsing states indicating what is waited for 49 | self.ParseStates = {'COMMENT': 1, 'STRING': 2, 'TRAILING_COMMENT': 3, 50 | 'STRING_MULTILINE': 4, 'COMMENT_MULTILINE' :5} 51 | # The parsing state indicates what the last parsed thing was 52 | self.parse_state = self.ParseStates['COMMENT'] 53 | self.key = None 54 | self.value = None 55 | self.comment = None 56 | 57 | def parse_line(self, line): 58 | ''' Parses a single line. Keeps track of the current state and creates 59 | LocalizedString objects as appropriate 60 | 61 | Keyword arguments: 62 | 63 | line 64 | The next line to be parsed 65 | 66 | Examples 67 | 68 | >>> parser = LocalizedStringLineParser() 69 | >>> string = parser.parse_line(' ') 70 | >>> string 71 | 72 | >>> string = parser.parse_line('/* Comment1 */') 73 | >>> string 74 | 75 | >>> string = parser.parse_line(' ') 76 | >>> string 77 | 78 | >>> string = parser.parse_line('"key1" = "value1";') 79 | >>> string.key 80 | 'key1' 81 | >>> string.value 82 | 'value1' 83 | >>> string.comment 84 | 'Comment1' 85 | 86 | >>> string = parser.parse_line('/* Comment2 */') 87 | >>> string 88 | 89 | >>> string = parser.parse_line('"key2" = "value2";') 90 | >>> string.key 91 | 'key2' 92 | >>> string.value 93 | 'value2' 94 | >>> string.comment 95 | 'Comment2' 96 | 97 | 98 | >>> parser = LocalizedStringLineParser() 99 | >>> string = parser.parse_line('"KEY3" = "VALUE3"; /* Comment3 */') 100 | >>> string.key 101 | 'KEY3' 102 | >>> string.value 103 | 'VALUE3' 104 | >>> string.comment 105 | 'Comment3' 106 | 107 | 108 | 109 | >>> parser = LocalizedStringLineParser() 110 | >>> string = parser.parse_line('/* Comment4 */') 111 | >>> string 112 | 113 | >>> string = parser.parse_line('"KEY4" = "VALUE4') 114 | >>> string 115 | 116 | >>> string = parser.parse_line('VALUE4_LINE2";') 117 | >>> string.key 118 | 'KEY4' 119 | >>> string.value 120 | 'VALUE4\\nVALUE4_LINE2' 121 | 122 | >>> parser = LocalizedStringLineParser() 123 | >>> string = parser.parse_line('/* Line 1') 124 | 125 | >>> string = parser.parse_line(' Line 2') 126 | 127 | >>> string = parser.parse_line(' Line 3 */') 128 | 129 | >>> string = parser.parse_line('"key" = "value";') 130 | 131 | >>> string.key 132 | 'key' 133 | >>> string.value 134 | 'value' 135 | >>> string.comment 136 | 'Line 1\\n Line 2\\n Line 3 ' 137 | ''' 138 | if self.parse_state == self.ParseStates['COMMENT']: 139 | (self.key, self.value, self.comment) = LocalizedString.parse_trailing_comment(line) 140 | if self.key is not None and self.value is not None and self.comment is not None: 141 | return self.build_localizedString() 142 | self.comment = LocalizedString.parse_comment(line) 143 | if self.comment is not None: 144 | self.parse_state = self.ParseStates['STRING'] 145 | return None 146 | # Maybe its a multiline comment 147 | self.comment_partial = LocalizedString.parse_multiline_comment_start(line) 148 | if self.comment_partial is not None: 149 | self.parse_state = self.ParseStates['COMMENT_MULTILINE'] 150 | return None 151 | 152 | elif self.parse_state == self.ParseStates['COMMENT_MULTILINE']: 153 | comment_end = LocalizedString.parse_multiline_comment_end(line) 154 | if comment_end is not None: 155 | self.comment = self.comment_partial + '\n' + comment_end 156 | self.comment_partial = None 157 | self.parse_state = self.ParseStates['STRING'] 158 | return None 159 | # Or its just an intermediate line 160 | comment_line = LocalizedString.parse_multiline_comment_line(line) 161 | if comment_line is not None: 162 | self.comment_partial = self.comment_partial + '\n' + comment_line 163 | return None 164 | 165 | elif self.parse_state == self.ParseStates['TRAILING_COMMENT']: 166 | self.comment = LocalizedString.parse_comment(line) 167 | if self.comment is not None: 168 | self.parse_state = self.ParseStates['COMMENT'] 169 | return self.build_localizedString() 170 | return None 171 | 172 | elif self.parse_state == self.ParseStates['STRING']: 173 | (self.key, self.value) = LocalizedString.parse_localized_pair( 174 | line 175 | ) 176 | if self.key is not None and self.value is not None: 177 | self.parse_state = self.ParseStates['COMMENT'] 178 | return self.build_localizedString() 179 | # Otherwise, try if the Value is multi-line 180 | (self.key, self.value_partial) = LocalizedString.parse_multiline_start( 181 | line 182 | ) 183 | if self.key is not None and self.value_partial is not None: 184 | self.parse_state = self.ParseStates['STRING_MULTILINE'] 185 | self.value = None 186 | return None 187 | elif self.parse_state == self.ParseStates['STRING_MULTILINE']: 188 | value_part = LocalizedString.parse_multiline_end(line) 189 | if value_part is not None: 190 | self.value = self.value_partial + '\n' + value_part 191 | self.value_partial = None 192 | self.parse_state = self.ParseStates['COMMENT'] 193 | return self.build_localizedString() 194 | value_part = LocalizedString.parse_multiline_line(line) 195 | if value_part is not None: 196 | self.value_partial = self.value_partial + '\n' + value_part 197 | return None 198 | 199 | 200 | def build_localizedString(self): 201 | localizedString = LocalizedString( 202 | self.key, 203 | self.value, 204 | self.comment 205 | ) 206 | self.key = None 207 | self.value = None 208 | self.comment = None 209 | return localizedString 210 | 211 | class LocalizedString(object): 212 | ''' A localizes string entry with key, value and comment''' 213 | COMMENT_EXPR = re.compile( 214 | # Line start 215 | '^\w*' 216 | # Comment 217 | '/\* (?P.+) \*/' 218 | # End of line 219 | '\w*$' 220 | ) 221 | COMMENT_MULTILINE_START = re.compile( 222 | # Line start 223 | '^\w*' 224 | # Comment 225 | '/\* (?P.+)' 226 | # End of line 227 | '\w*$' 228 | ) 229 | COMMENT_MULTILINE_LINE = re.compile( 230 | # Line start 231 | '^' 232 | # Value 233 | '(?P.+)' 234 | # End of line 235 | '$' 236 | ) 237 | COMMENT_MULTILINE_END = re.compile( 238 | # Line start 239 | '^' 240 | # Comment 241 | '(?P.+)\*/' 242 | # End of line 243 | '\s*$' 244 | ) 245 | LOCALIZED_STRING_EXPR = re.compile( 246 | # Line start 247 | '^' 248 | # Key 249 | '"(?P.+)"' 250 | # Equals 251 | ' ?= ?' 252 | # Value 253 | '"(?P.+)"' 254 | # Whitespace 255 | ';' 256 | # End of line 257 | '$' 258 | ) 259 | LOCALIZED_STRING_MULTILINE_START_EXPR = re.compile( 260 | # Line start 261 | '^' 262 | # Key 263 | '"(?P.+)"' 264 | # Equals 265 | ' ?= ?' 266 | # Value 267 | '"(?P.+)' 268 | # End of line 269 | '$' 270 | ) 271 | LOCALIZED_STRING_MULTILINE_LINE_EXPR = re.compile( 272 | # Line start 273 | '^' 274 | # Value 275 | '(?P.+)' 276 | # End of line 277 | '$' 278 | ) 279 | LOCALIZED_STRING_MULTILINE_END_EXPR = re.compile( 280 | # Line start 281 | '^' 282 | # Value 283 | '(?P.+)"' 284 | # Whitespace 285 | ' ?; ?' 286 | # End of line 287 | '$' 288 | ) 289 | LOCALIZED_STRING_TRAILING_COMMENT_EXPR = re.compile( 290 | # Line start 291 | '^' 292 | # Key 293 | '"(?P.+)"' 294 | # Equals 295 | ' ?= ?' 296 | # Value 297 | '"(?P.+)"' 298 | # Whitespace 299 | ' ?; ?' 300 | # Comment 301 | '/\* (?P.+) \*/' 302 | # End of line 303 | '$' 304 | 305 | ) 306 | 307 | @classmethod 308 | def parse_multiline_start(cls, line): 309 | ''' Parse the beginning of a multi-line entry, "KEY"="VALUE_LINE1 310 | 311 | Keyword arguments: 312 | 313 | line 314 | The line to be parsed 315 | 316 | Returns 317 | ``tuple`` with key, value and comment 318 | ``None`` when the line was no comment 319 | 320 | Examples 321 | 322 | >>> line = '"key" = "value4' 323 | >>> LocalizedString.parse_multiline_start(line) 324 | ('key', 'value4') 325 | 326 | ''' 327 | result = cls.LOCALIZED_STRING_MULTILINE_START_EXPR.match(line) 328 | if result is not None: 329 | return (result.group('key'), 330 | result.group('value')) 331 | else: 332 | return (None, None) 333 | 334 | @classmethod 335 | def parse_multiline_line(cls, line): 336 | ''' Parse an intermediate line of a multi-line entry, only value 337 | 338 | Keyword arguments: 339 | 340 | line 341 | The line to be parsed 342 | 343 | Returns 344 | ``String`` with the value 345 | ``None`` when the line was no comment 346 | 347 | Examples 348 | 349 | >>> line = 'value4, maybe something else' 350 | >>> LocalizedString.parse_multiline_line(line) 351 | 'value4, maybe something else' 352 | ''' 353 | result = cls.LOCALIZED_STRING_MULTILINE_LINE_EXPR.match(line) 354 | if result is not None: 355 | return result.group('value') 356 | return None 357 | 358 | 359 | @classmethod 360 | def parse_multiline_end(cls, line): 361 | ''' Parse an end line of a multi-line entry, only value 362 | 363 | Keyword arguments: 364 | 365 | line 366 | The line to be parsed 367 | 368 | Returns 369 | ``String`` the value 370 | ``None`` when the line was no comment 371 | 372 | Examples 373 | 374 | >>> line = 'value4, maybe something else";' 375 | >>> LocalizedString.parse_multiline_end(line) 376 | 'value4, maybe something else' 377 | ''' 378 | result = cls.LOCALIZED_STRING_MULTILINE_END_EXPR.match(line) 379 | if result is not None: 380 | return result.group('value') 381 | return None 382 | 383 | 384 | @classmethod 385 | def parse_trailing_comment(cls, line): 386 | '''Extract the content of a line with a trailing comment. 387 | 388 | Keyword arguments: 389 | 390 | line 391 | The line to be parsed 392 | 393 | Returns 394 | ``tuple`` with key, value and comment 395 | ``None`` when the line was no comment 396 | 397 | Examples 398 | 399 | >>> line = '"key3" = "value3";/* Bla */' 400 | >>> LocalizedString.parse_trailing_comment(line) 401 | ('key3', 'value3', 'Bla') 402 | ''' 403 | result = cls.LOCALIZED_STRING_TRAILING_COMMENT_EXPR.match(line) 404 | if result is not None: 405 | return ( 406 | result.group('key'), 407 | result.group('value'), 408 | result.group('comment') 409 | ) 410 | else: 411 | return (None, None, None) 412 | 413 | @classmethod 414 | def parse_multiline_comment_start(cls, line): 415 | ''' 416 | Example: 417 | 418 | >>> LocalizedString.parse_multiline_comment_start('/* Hello ') 419 | 'Hello ' 420 | ''' 421 | result = cls.COMMENT_MULTILINE_START.match(line) 422 | if result is not None: 423 | return result.group('comment') 424 | else: 425 | return None 426 | 427 | 428 | @classmethod 429 | def parse_multiline_comment_line(cls, line): 430 | ''' 431 | Example: 432 | 433 | >>> LocalizedString.parse_multiline_comment_line(' Line ') 434 | ' Line ' 435 | ''' 436 | result = cls.COMMENT_MULTILINE_LINE.match(line) 437 | if result is not None: 438 | return result.group('comment') 439 | else: 440 | return None 441 | 442 | 443 | @classmethod 444 | def parse_multiline_comment_end(cls, line): 445 | ''' 446 | Example: 447 | 448 | >>> LocalizedString.parse_multiline_comment_end(' End */ ') 449 | ' End ' 450 | ''' 451 | result = cls.COMMENT_MULTILINE_END.match(line) 452 | if result is not None: 453 | return result.group('comment') 454 | else: 455 | return None 456 | 457 | @classmethod 458 | def parse_comment(cls, line): 459 | '''Extract the content of a comment line from a line. 460 | 461 | Keyword arguments: 462 | 463 | line 464 | The line to be parsed 465 | 466 | Returns 467 | ``string`` with the Comment or 468 | ``None`` when the line was no comment 469 | 470 | Examples 471 | 472 | >>> LocalizedString.parse_comment('This line is no comment') 473 | >>> LocalizedString.parse_comment('') 474 | >>> LocalizedString.parse_comment('/* Comment */') 475 | 'Comment' 476 | ''' 477 | result = cls.COMMENT_EXPR.match(line) 478 | if result is not None: 479 | return result.group('comment') 480 | else: 481 | return None 482 | 483 | @classmethod 484 | def parse_localized_pair(cls, line): 485 | '''Extract the content of a key/value pair from a line. 486 | 487 | Keyword arguments: 488 | 489 | line 490 | The line to be parsed 491 | 492 | Returns 493 | ``tupple`` with key and value as strings 494 | ``tupple`` (None, None) when the line was no match 495 | 496 | Examples 497 | 498 | >>> LocalizedString.parse_localized_pair('Some Line') 499 | (None, None) 500 | >>> LocalizedString.parse_localized_pair('/* Comment */') 501 | (None, None) 502 | >>> LocalizedString.parse_localized_pair('"key1" = "value1";') 503 | ('key1', 'value1') 504 | ''' 505 | result = cls.LOCALIZED_STRING_EXPR.match(line) 506 | if result is not None: 507 | return ( 508 | result.group('key'), 509 | result.group('value') 510 | ) 511 | else: 512 | return (None, None) 513 | 514 | def __eq__(self, other): 515 | '''Tests Equality of two LocalizedStrings 516 | 517 | >>> s1 = LocalizedString('key1', 'value1', 'comment1') 518 | >>> s2 = LocalizedString('key1', 'value1', 'comment1') 519 | >>> s3 = LocalizedString('key1', 'value2', 'comment1') 520 | >>> s4 = LocalizedString('key1', 'value1', 'comment2') 521 | >>> s5 = LocalizedString('key1', 'value2', 'comment2') 522 | >>> s1 == s2 523 | True 524 | >>> s1 == s3 525 | False 526 | >>> s1 == s4 527 | False 528 | >>> s1 == s5 529 | False 530 | ''' 531 | if isinstance(other, LocalizedString): 532 | return (self.key == other.key and self.value == other.value and 533 | self.comment == other.comment) 534 | else: 535 | return NotImplemented 536 | 537 | def __neq__(self, other): 538 | result = self.__eq__(other) 539 | if(result is NotImplemented): 540 | return result 541 | return not result 542 | 543 | def __init__(self, key, value=None, comment=None): 544 | super(LocalizedString, self).__init__() 545 | self.key = key 546 | self.value = value 547 | self.comment = comment 548 | 549 | def is_raw(self): 550 | ''' 551 | Return True if the localized string has not been translated. 552 | 553 | Examples 554 | >>> l1 = LocalizedString('key1', 'valye1', 'comment1') 555 | >>> l1.is_raw() 556 | False 557 | >>> l2 = LocalizedString('key2', 'key2', 'comment2') 558 | >>> l2.is_raw() 559 | True 560 | ''' 561 | return self.value == self.key 562 | 563 | def __str__(self): 564 | if self.comment: 565 | return '/* %s */\n"%s" = "%s";\n' % ( 566 | self.comment, self.key or '', self.value or '', 567 | ) 568 | else: 569 | return '"%s" = "%s";\n' % (self.key or '', self.value or '') 570 | 571 | # -- Methods ------------------------------------------------------------------- 572 | 573 | ENCODINGS = ['utf16', 'utf8'] 574 | 575 | 576 | def merge_strings(old_strings, new_strings, keep_comment=False, replace_value=False): 577 | '''Merges two dictionarys, one with the old strings and one with the new 578 | strings. 579 | Old strings keep their value but their comment will be updated. Only if 580 | the string is 'raw' which means its value is equal to its key, the value 581 | will be replaced by the new one. 582 | But because the method has to work with NSLocalizedStringWithDefaultValue 583 | as well it is not possible to detect untranslated strings with default value 584 | so if the default value changes this will not be updated! 585 | 586 | Keyword arguments: 587 | 588 | old_strings 589 | Dictionary with the Strings that were already there 590 | 591 | new_strings 592 | Dictionary with the new Strings 593 | 594 | keep_comment 595 | If True, the old comment will be kept. This is necessary for 596 | translating Storyboard files because they have generated comments 597 | which are not very helpful 598 | 599 | replace_value 600 | If True, the old value will be replaced. This is necessary for 601 | translating IB files because they are never `raw` strings 602 | 603 | Returns 604 | 605 | Merged Dictionary 606 | 607 | Examples: 608 | 609 | >>> old_dict = {} 610 | >>> old_dict['key1'] = LocalizedString('key1', 'value1', 'comment1') 611 | >>> old_dict['key2'] = LocalizedString('key2', 'value2', 'comment2') 612 | >>> old_dict['key3'] = LocalizedString('key3', 'key3', 'comment3') 613 | >>> new_dict = {} 614 | >>> new_dict['key1'] = LocalizedString('key1', 'key1', 'comment1') 615 | >>> new_dict['key2'] = LocalizedString('key2', 'key2', 'comment2_new') 616 | >>> new_dict['key4'] = LocalizedString('key4', 'key4', 'comment4') 617 | >>> new_dict['key3'] = LocalizedString('key3', 'value3', 'comment3') 618 | >>> merge_dict = merge_strings(old_dict, new_dict) 619 | >>> merge_dict['key1'].value 620 | 'value1' 621 | >>> merge_dict['key1'].comment 622 | 'comment1' 623 | >>> merge_dict['key2'].value 624 | 'value2' 625 | >>> merge_dict['key2'].comment 626 | 'comment2_new' 627 | >>> merge_dict['key3'].value 628 | 'value3' 629 | >>> merge_dict['key3'].comment 630 | 'comment3' 631 | >>> merge_dict['key4'].value 632 | 'key4' 633 | >>> merge_dict['key4'].comment 634 | 'comment4' 635 | 636 | >>> old_dict_2 = {} 637 | >>> new_dict_2 = {} 638 | >>> old_dict_2['key1'] = LocalizedString('key1', 'value1', 'comment1') 639 | >>> new_dict_2['key1'] = LocalizedString('key1', 'value1', 'comment2') 640 | >>> merged_1 = merge_strings(old_dict_2, new_dict_2, keep_comment=False) 641 | >>> merged_1['key1'].value 642 | 'value1' 643 | >>> merged_1['key1'].comment 644 | 'comment2' 645 | >>> old_dict_2['key1'] = LocalizedString('key1', 'value1', 'comment1') 646 | >>> new_dict_2['key1'] = LocalizedString('key1', 'value1', 'comment2') 647 | >>> merged_2 = merge_strings(old_dict_2, new_dict_2, keep_comment=True) 648 | >>> merged_2['key1'].value 649 | 'value1' 650 | >>> merged_2['key1'].comment 651 | 'comment1' 652 | ''' 653 | merged_strings = {} 654 | for key, old_string in old_strings.iteritems(): 655 | if key in new_strings: 656 | new_string = new_strings[key] 657 | if old_string.is_raw() or replace_value: 658 | # if the old string is raw just take the new string 659 | if keep_comment: 660 | new_string.comment = old_string.comment 661 | merged_strings[key] = new_string 662 | else: 663 | # otherwise take the value of the old string but the comment of the new string 664 | new_string.value = old_string.value 665 | if keep_comment: 666 | new_string.comment = old_string.comment 667 | merged_strings[key] = new_string 668 | # remove the string from the new strings 669 | del new_strings[key] 670 | else: 671 | # If the String is not in the new Strings anymore it has been removed 672 | # TODO: Include option to not remove old keys! 673 | pass 674 | # All strings that are still in the new_strings dict are really new and can be copied 675 | for key, new_string in new_strings.iteritems(): 676 | merged_strings[key] = new_string 677 | 678 | return merged_strings 679 | 680 | 681 | def parse_file(file_path, encoding='utf16'): 682 | ''' Parses a file and creates a dictionary containing all LocalizedStrings 683 | elements in the file 684 | 685 | Keyword arguments: 686 | 687 | file_path 688 | path to the file that should be parsed 689 | 690 | encoding 691 | encoding of the file 692 | 693 | Returns: ``dict`` 694 | ''' 695 | 696 | with codecs.open(file_path, mode='r', encoding=encoding) as file_contents: 697 | logging.debug("Parsing File: {}".format(file_path)) 698 | parser = LocalizedStringLineParser() 699 | localized_strings = {} 700 | try: 701 | for line in file_contents: 702 | localized_string = parser.parse_line(line) 703 | if localized_string is not None: 704 | localized_strings[localized_string.key] = localized_string 705 | except UnicodeError: 706 | logging.debug("Failed to open file as UTF16, Trying UTF8") 707 | file_contents = codecs.open(file_path, mode='r', encoding='utf8') 708 | for line in file_contents: 709 | localized_string = parser.parse_line(line) 710 | if localized_string is not None: 711 | localized_strings[localized_string.key] = localized_string 712 | return localized_strings 713 | 714 | 715 | def write_file(file_path, strings, encoding='utf16'): 716 | '''Writes the strings to the given file 717 | ''' 718 | with codecs.open(file_path, 'w', encoding) as output: 719 | for string in sort_strings(strings): 720 | output.write('%s\n' % string) 721 | 722 | 723 | def sort_strings(strings): 724 | '''Returns an array that contains all LocalizedStrings objects of the 725 | dictionary, sorted alphabetically 726 | ''' 727 | keys = strings.keys() 728 | keys.sort() 729 | 730 | values = [] 731 | for key in keys: 732 | values.append(strings[key]) 733 | 734 | return values 735 | 736 | 737 | def merge_files(new_file_path, old_file_path, keep_comment=False, replace_value=False): 738 | '''Scans the Strings in both files, merges them together and writes the 739 | result to the old file 740 | 741 | Keyword Arguments 742 | 743 | new_file_path 744 | Path to the new generated strings file 745 | 746 | old_file_path 747 | Path to the existing strings file 748 | ''' 749 | new_strings = parse_file(new_file_path) 750 | logging.debug('Current File: {}'.format(old_file_path)) 751 | old_strings = parse_file(old_file_path) 752 | final_strings = merge_strings(old_strings, new_strings, keep_comment, replace_value) 753 | write_file(old_file_path, final_strings) 754 | 755 | 756 | def main(): 757 | ''' Parse the command line and execute the programm with the parameters ''' 758 | 759 | parser = optparse.OptionParser( 760 | 'usage: %prog [options] [output folder] [source folders] [ignore patterns]' 761 | ) 762 | parser.add_option( 763 | '-o', 764 | '--old_path', 765 | action='store', 766 | dest='old_path', 767 | default='.', 768 | help='Old file path for merging' 769 | ) 770 | parser.add_option( 771 | '-n', 772 | '--new_path', 773 | action='store', 774 | dest='new_path', 775 | default='.', 776 | help='New file path for merging' 777 | ) 778 | parser.add_option( 779 | '-v', 780 | '--verbose', 781 | action='store_true', 782 | dest='verbose', 783 | default=False, 784 | help='Show debug messages' 785 | ) 786 | 787 | parser.add_option( 788 | '-r', 789 | '--replace_value', 790 | action='store_true', 791 | dest='replace_value', 792 | default=False, 793 | help='Force replace string values with ones in new file' 794 | ) 795 | 796 | parser.add_option( 797 | '-k', 798 | '--keep_comment', 799 | action='store_true', 800 | dest='keep_comment', 801 | default=True, 802 | help='Keep comment from the old string' 803 | ) 804 | 805 | (options, args) = parser.parse_args() 806 | 807 | # Create Logger 808 | logging.basicConfig( 809 | format='%(message)s', 810 | level=options.verbose and logging.DEBUG or logging.INFO 811 | ) 812 | 813 | merge_files(options.new_path, options.old_path, options.keep_comment, options.replace_value) 814 | return 0 815 | 816 | if __name__ == '__main__': 817 | doctest.testmod() 818 | sys.exit(main()) 819 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | OneSky Xcode Plugin 2 | ======================= 3 | 4 | This plugin lets you sync localizable string resources and translations with OneSky server without logging on to OneSky web admin. 5 | 6 | *Note this plugin is not supported in Xcode 8. See [ here ] (https://support.oneskyapp.com/hc/en-us/articles/227128928-Is-the-Xcode-plugin-compatible-with-Xcode-8-) for more information. 7 | 8 | 9 | Installation 10 | ------------ 11 | 12 | 1. Download [`OneSkyPlugin.zip`](https://github.com/onesky/plugin-xcode/releases/download/1.8.8/OneSkyPlugin.zip) in the release tab and unzip the folder into `~/Library/Application Support/Developer/Shared/Xcode/Plug-ins/`. If this is the first Plug-ins that you use in Xcode, the Plug-ins directory does not exist. In this case, creating the directory manually would do. Then, Relaunch Xcode. 13 | 2. To uninstall, just remove the plugin from there (and restart Xcode) and the project property cache file in `~/Library/Application Support/OneSky/OneSkyProperties.plist`. 14 | 15 | Project Settings 16 | ---------------- 17 | 18 | 1. Go to **Menu > Editor > OneSky > Project Properties...** 19 | 2. Key in your `API Key`, `API Secret`, these parameters can be found on your OneSky web admin dashboard. 20 | 3. Select the target `Project` from the project list. 21 | 4. Select the `Base Language` of your project. 22 | 23 | ![project_properties.png](/Images/project_properties.png) 24 | 25 | Generate/Update Strings 26 | ------------ 27 | This tool generates .strings files in base language for both Interface Builder and Source (.m, .c) files, new strings will be merged into existing files. New files will be added to the project and target automatically. 28 | 29 | 1. Go to **Menu > Editor > OneSky > Generate/Update Strings Files...** 30 | 2. Select the files for strings generation and press `Generate`. 31 | 32 | ![generate_strings.png](/Images/generate_strings.png) 33 | 34 | 35 | Upload Strings 36 | ------------ 37 | 38 | 1. Go to **Menu > Editor > OneSky > Upload Strings...** 39 | 2. Select the files to upload to OneSky and press `Upload`. 40 | 41 | ![upload_strings.png](/Images/upload_strings.png) 42 | 43 | Download Translations 44 | ----------------- 45 | 46 | 1. Go to **Menu > Editor > OneSky > Download Translations...** 47 | 2. Select the translations to download from OneSky and press `Download`. 48 | 49 | ![download_translations.png](/Images/download_translations.png) 50 | 51 | Support 52 | ------- 53 | http://support.oneskyapp.com/ 54 | 55 | Helpful articles 56 | ------- 57 | [ How to support OneSky Xcode plugin in Xcode 8 ] (https://support.oneskyapp.com/hc/en-us/articles/227128928-Is-the-Xcode-plugin-compatible-with-Xcode-8-) 58 | 59 | [ How to find API key ](http://support.oneskyapp.com/hc/en-us/articles/206887797-How-to-find-your-API-keys-) 60 | 61 | [ Supported languages in iOS ](http://support.oneskyapp.com/hc/en-us/articles/206217438-Languages-supported-by-iOS-) 62 | 63 | [More from here](http://support.oneskyapp.com/hc/en-us/sections/201079608-API-and-plugins) 64 | --------------------------------------------------------------------------------