├── 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 | 
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 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------