Compare commits

...

212 commits

Author SHA1 Message Date
9ed9c04c53 Remove Apple unnecessary empty lines
Signed-off-by: Roman Tsisyk <roman@tsisyk.com>
2025-02-23 11:36:43 +00:00
Dwayne Bailey
89dd0a6b35 Add magic Android newlines 2024-09-17 22:30:20 +01:00
Dwayne Bailey
ebe50c3adc Use tab indents in iPhone plural stringdict files
2*sp -> \t

Remove the confusingly named tab() that indents with spaces and use \t
directly
2024-08-15 18:49:45 +01:00
Dwayne Bailey
e7215ccba2 Break serialising strings on \n for readabilty 2024-08-15 18:49:45 +01:00
Dwayne Bailey
1aeee66812 Consistent space indents for Android strings
Indent with 4 spaces and fix the broken plural \t\s*2 indentation
2024-08-15 18:49:45 +01:00
Dwayne Bailey
4cfda06f87 Remove Apple Twine headers 2024-08-15 11:02:45 +03:00
Dwayne Bailey
9c6143efe5 Remove Android Twine header 2024-08-15 11:02:45 +03:00
Dwayne Bailey
860c79f3a6 Sort plurals forms in CLDR order
Instead of alphabetical, sort these in the expect CLDR order
zero...other

Only sort when retreiving the values for formatter consumption, this
should leave Twine files in their default order.

Signed-off-by: Dwayne Bailey <dwayne@translatehouse.co.uk>
2024-08-14 21:38:52 +02:00
Alexander Borsuk
a9a97d19c5 Use custom om suffix for twine version 2024-03-05 00:41:26 +02:00
Alexander Borsuk
f724a8fe7a Remove twine version from apple plurals file 2024-03-05 00:41:07 +02:00
Alexander Borsuk
b5d723caf5 Replace Android %s with iOS %@ in generated strings
Signed-off-by: Alexander Borsuk <me@alex.bio>
2023-05-19 22:58:32 +02:00
Alexander Borsuk
457c5bbda5 Do not print Twine version in generated files
It creates a lot of unnecessary noise after upgrading Twine

Signed-off-by: Alexander Borsuk <me@alex.bio>
2022-12-31 19:18:33 +01:00
Alexander Borsuk
c9bcdcd00b
Merge pull request #5 from organicmaps/fixes
Fixed jquery json language extraction and updated to the latest Twine
2022-12-22 12:59:49 +01:00
Alexander Borsuk
1a140cbdf8 Properly extract language from json files
Signed-off-by: Alexander Borsuk <me@alex.bio>
2022-12-22 12:14:46 +01:00
Alexander Borsuk
50e7eb95cc Fixed generic language fallback
Signed-off-by: Alexander Borsuk <me@alex.bio>
2022-12-22 11:59:18 +01:00
Alexander Borsuk
6917052e54 Merged with upstream twine changes
Merge branch 'main' into organicmaps
2022-12-22 11:58:53 +01:00
Sebastian Celis
55a140a44c Prepare 1.1.2 for release 2022-11-15 08:14:09 -06:00
Sebastian Celis
a3418dea9c Update required Ruby version to 2.6 2022-11-15 08:11:36 -06:00
Sebastian Celis
7a6d18559b Remove old Circle CI configuration. 2022-11-15 08:05:41 -06:00
Sebastian Celis
39aafe4784
Merge pull request #312 from scelis/ruby3
Add Ruby 3 support
2022-11-15 08:03:02 -06:00
Sebastian Celis
ca95b9ed02 Add rexml as a runtime dependency 2022-11-15 07:55:19 -06:00
Sebastian Celis
b10fe933f5 Add ruby 3.0 and 3.1 to the test matrix 2022-11-15 07:50:54 -06:00
Sebastian Celis
26c0562936
Merge pull request #310 from scelis/github-actions
Add Test workflow for GitHub Actions
2022-08-25 16:22:54 -05:00
Sebastian Celis
2a872b8b71 Drop support for Ruby 2.4 and 2.5 2022-08-25 16:21:03 -05:00
Sebastian Celis
6bf9a2ddde Remove CircleCI badge 2022-08-25 16:18:44 -05:00
Sebastian Celis
f331423475
Add Test workflow for GitHub Actions 2022-08-25 16:16:16 -05:00
Sebastian Celis
d2ec00d57b
Update formatters.md
Add sample plugins section.
2022-08-25 15:29:18 -05:00
David Martinez
ccf5f38d6f Add rexml dependency
Signed-off-by: David Martinez <47610359+dvdmrtnz@users.noreply.github.com>
2022-04-14 13:43:15 +02:00
David Martinez
6cd569fa49
Added regional dialect fallback to generic language (#3)
* Added regional dialect fallback to generic language

Signed-off-by: David Martinez <47610359+dvdmrtnz@users.noreply.github.com>

* Reorder generic_language to mantain fallback_mapping priority

Signed-off-by: David Martinez <47610359+dvdmrtnz@users.noreply.github.com>

* Update lib/twine/output_processor.rb

Signed-off-by: David Martinez <47610359+dvdmrtnz@users.noreply.github.com>

Co-authored-by: Alexander Borsuk <170263+biodranik@users.noreply.github.com>

Co-authored-by: Alexander Borsuk <170263+biodranik@users.noreply.github.com>
2022-03-23 13:00:10 +01:00
Alexander Borsuk
96e0e2c3cd Fixed invalid stringdicts XML
Xcode didn't parse XML files started with a comment line

Signed-off-by: Alexander Borsuk <me@alex.bio>
2022-01-12 16:10:03 +01:00
Alexander Borsuk
13f5b7b088 Fixed Hebrew language code for Android
Signed-off-by: Alexander Borsuk <me@alex.bio>
2022-01-11 22:12:48 +01:00
Alexander Borsuk
649967b71e Fixed plural escaping 2021-08-07 18:53:38 +02:00
greshilov
38f1761ac9 Add plural localizations 2021-08-07 15:03:37 +02:00
Ilya Zverev
792b2492dc Modifications for Organic Maps 2021-08-07 14:54:50 +02:00
Sebastian Celis
304b3ec63f
Merge pull request #304 from apascual/main
Fixed fallback language support and included simplified Chinese
2021-07-12 09:38:00 -05:00
Abel Pascual
790e3f7b3e Added fallback support for simplified chinese. 2021-07-09 09:33:58 +02:00
Abel Pascual
df60ce8e68 Fix fallback chain. 2021-07-09 09:33:09 +02:00
Sebastian Celis
7a77a55776
Merge pull request #303 from striezel-stash/fix-typos
chore: fix some typos
2021-07-06 08:42:22 -05:00
Dirk Stolle
fd326d0029 chore: fix some typos 2021-07-03 15:12:25 +02:00
Sebastian Celis
67a856d10a
Merge pull request #302 from bfreiband/patch-1
chore: fix typo in formatters.md
2021-06-17 17:11:09 -05:00
Ben Freiband
ad9b8d504b
chore: fix typo in formatters.md
s/bundeled/bundled in formatters.md
2021-06-15 16:50:52 -04:00
Sebastian Celis
b09c83229d Prepare 1.1.1 for release 2021-01-28 15:44:51 -06:00
Sebastian Celis
7be9e4549d
Merge pull request #300 from dgmltn/android-mulitline-comments
Slurp Android mulitline comments as single-line
2021-01-28 15:28:29 -06:00
Doug Melton
4d0f6326fb Slurp Android mulitline comments as single-line 2021-01-28 13:17:15 -08:00
Sebastian Celis
1662f131a9
Update version in the README to 1.1 2020-12-16 11:16:57 -06:00
Sebastian Celis
e220f9cc9e Words matter 2020-07-20 12:49:47 -05:00
Sebastian Celis
54072398dc Update version to 1.1 2020-07-09 13:58:50 -05:00
Sebastian Celis
4dffd16ec8
Merge pull request #297 from scelis/update-rubyzip-2-0
Update rubyzip to latest version
2020-07-09 13:25:43 -05:00
Sebastian Celis
6e70c05235 Update minimum ruby version to 2.4
Also add CircleCI tests for Ruby 2.6 and 2.7
2020-07-09 13:01:31 -05:00
Sebastian Celis
1eb18d8726 Update rubyzip to ~> 2.0 2020-07-09 12:55:36 -05:00
Sebastian Celis
edee7bbd11
Merge pull request #294 from scelis/update-rake-gemspec
Update rake to 13.0 or higher
2020-03-17 10:27:33 -05:00
Sebastian Celis
dc124b6b0e Update rake to 13.0 or higher
12.3.2 and lower have a security vulnerability.

https://github.com/advisories/GHSA-jppv-gw3r-w3q8
2020-03-17 10:25:54 -05:00
Sebastian Celis
aa99d02fc0
Merge pull request #289 from sebastianludwig/286-287-django
Django formatter improvements
2019-10-15 08:53:49 -05:00
Sebastian Ludwig
b216a2db01 Add validation warning if a translation contains Python only placeholders (fixes #287) 2019-10-03 17:33:17 +02:00
Sebastian Ludwig
42895063e1 Ignore commented out lines in Django .PO files (fixes #286) 2019-09-23 17:01:54 +02:00
Sebastian Ludwig
8566a0b70e Remove wrong indentation from Django header
to be closer to the specification
2019-09-23 16:59:44 +02:00
Sebastian Celis
9c70cb5638
Merge pull request #282 from sebastianludwig/281-escape-all-tags
Add --escape-all-tags flag
2019-07-01 13:29:00 -05:00
Sebastian Ludwig
3fa675dd84
Update CHANGELOG.md
Co-Authored-By: Sebastian Celis <sebastian@sebastiancelis.com>
2019-07-01 20:26:05 +02:00
Sebastian Celis
085cdb2585
Merge pull request #283 from sebastianludwig/ruby-2.5
Add Ruby 2.5 to CircleCI
2019-07-01 13:25:28 -05:00
Sebastian Ludwig
053aa14d03 Add Ruby 2.5 to CircleCI 2019-07-01 20:21:22 +02:00
Sebastian Ludwig
59c2f23064 Add --escape-all-tags flag to force the Android formatter to always escape styling tags.
Closes #281
2019-07-01 20:19:57 +02:00
Sebastian Celis
b3b8c395d7 Update version to 1.0.6 2019-05-28 08:17:28 -05:00
Sebastian Celis
a0fa380a84
Merge pull request #279 from doganov/more-android-tags
Support more Android styling tags
2019-05-23 15:57:50 -05:00
Kaloian Doganov
5e7a9c9be3 Support more Android styling tags 2019-05-23 23:28:30 +03:00
Sebastian Celis
bc4bd7daf0
Merge pull request #276 from jonasrottmann/fix/android-output-path
Android output path for default language
2019-05-14 14:21:07 -05:00
Jonas Rottmann
2e19dccd74 Make android formatter respect developer language
The output path of the android formatter will be `values` (no language
selector) of the language matches the developer language.
2019-04-18 23:46:58 +02:00
Sebastian Celis
408670e48d Update version to 1.0.5 2019-02-24 11:26:05 -06:00
Sebastian Celis
a8a9980d2f Update CHANGELOG 2019-02-24 11:22:50 -06:00
Sebastian Celis
28825fcf78
Merge pull request #268 from scelis/fix/escape-gettext-quotes
Escape quotes in the gettext formatter
2019-02-24 11:10:06 -06:00
Sebastian Celis
692c41460c
Merge pull request #269 from scelis/fix/rubygems-flag
Replace -rubygems with -rrubygems
2019-02-24 11:09:37 -06:00
Sebastian Celis
ac5573ca7a Replace -rubygems with -rrubygems
The old one was a thing before Ruby 2.0.
2019-02-24 11:07:01 -06:00
Sebastian Celis
e1ca68f6b9 Escape quotes in the gettext formatter
Fixes #267
2019-02-21 15:56:02 -06:00
Sebastian Celis
5a5a263fec
Merge pull request #264 from danl3v/fix-warnings
Fix Warnings When Running Rake Test
2018-12-04 09:27:07 -06:00
Daniel Levy
97106cdd1f Fix Warnings When Running Rake Test 2018-12-03 13:29:44 -08:00
Sebastian Celis
b0474a6f87
Merge pull request #262 from danl3v/fix-deprecation-warnings
Fix Deprecation Warnings
2018-11-29 10:28:31 -06:00
Sebastian Celis
62a6f7889d
Merge pull request #263 from danl3v/update-to-circleci-2.0
Update to CircleCI 2.0
2018-11-29 10:27:15 -06:00
Daniel Levy
b9135c20ef Update to CircleCI 2.0 2018-11-28 15:20:08 -08:00
Daniel Levy
2cb9639047 Fix Deprecation Warnings 2018-11-27 15:02:42 -08:00
sebastianludwig
5ce4ac934c
Merge pull request #260 from tomgs/patch-1
Fixed first run description
2018-09-05 19:58:17 +02:00
Tom Granot-Scalosub
3b99a796f1
Fixed first run description
Doesn't run without the --format part - added all possible format possibilities.
2018-08-31 13:51:30 +03:00
Sebastian Celis
a186d37481
Merge pull request #257 from sebastianludwig/255-any-lang
Deduct language from filename
2018-06-17 17:45:28 -05:00
Sebastian Ludwig
079065da31 Add hints to error messages that might help to solve the error. 2018-06-17 16:23:25 +02:00
Sebastian Ludwig
68b59b9e0f Fixes #255. While fixing #250 094ba47 introduced a regression that the language was no longer deducted from the given filename. 2018-06-17 16:17:22 +02:00
sebastianludwig
b1f629061a
Merge pull request #254 from aleks2a/doubleQuotes
Fixing issue 252: Loc Drop Converts Double Quotes to Single Quotes
2018-06-07 10:19:34 +02:00
Aliaksei Piatrouski
a0a12ad1c1 Fixxing issue 252: Generating and Consuming Loc Drop Converts Double Quotes to Single Quotes 2018-06-06 12:58:11 -07:00
Sebastian Celis
0eefb31ba1
Merge pull request #251 from sebastianludwig/250-language-deduction
Language deduction
2018-06-05 08:23:04 -05:00
Sebastian Ludwig
cd0735b39d Remove unnecessary '\n' from the generated gettext localization files. 2018-06-04 21:22:09 +02:00
Sebastian Ludwig
094ba47ac8 Only recognize two letter language codes (with optional two letter region codes) as languages when deducting the language from filenames.
Also add the logic to recognize a language code folder name in the Abstract formatter to mirror its output_path_for_language implementation.

Fixes 250.
2018-06-04 21:22:09 +02:00
Sebastian Celis
9345cdf26e
Merge pull request #249 from scelis/feature/jruby-documentation
Add documentation for Android and jruby
2018-05-30 11:24:30 -05:00
Sebastian Celis
04cb7f66cd Update CircleCI links to point to the correct repository 2018-05-30 11:17:45 -05:00
Sebastian Celis
394fd019f6 Update version to 1.0.4 2018-05-30 11:15:31 -05:00
Sebastian Celis
2878050d18 Cleanup android and jruby section 2018-05-30 11:07:28 -05:00
tobilarscheid
b92ce136cc
Update README.md 2018-05-22 16:13:43 +02:00
Sebastian Celis
a5edde0511
Merge pull request #247 from sebastianludwig/244-consume-tags
Consume child HTML tags in Android formatter
2018-05-22 08:56:54 -05:00
Sebastian Celis
890d461eb9
Merge pull request #246 from sebastianludwig/243-consume-all-errors
Let consume-localization-archive fail
2018-05-22 08:56:07 -05:00
Sebastian Celis
4354775577
Merge pull request #245 from sebastianludwig/236-quite
Add quiet mode
2018-05-22 08:55:03 -05:00
Sebastian Ludwig
5b2ddf3135 Fixes a regression introduced in ea58bd1: child HTML tags in strings like <b> were ignored when consuming strings. Closes #244. 2018-05-21 14:26:30 +02:00
Sebastian Ludwig
d0dc544023 Let consume-localization-archive fail if one file could not be consumed. Fixes #243. 2018-05-21 13:43:57 +02:00
Sebastian Ludwig
937c713b71 Add --quiet option. Closes #236. 2018-05-21 13:11:27 +02:00
Sebastian Ludwig
8eccb7fa57 Use stderr strictly for errors and stdout for all other output (#236) 2018-05-21 12:34:44 +02:00
tobilarscheid
b610e30065
use jruby for android 2018-05-04 13:04:27 +02:00
Sebastian Celis
b52de34ce9 Update version to 1.0.3 2018-01-26 11:41:22 -06:00
Sebastian Celis
01ee6d4eef
Update CHANGELOG.md 2018-01-26 11:40:59 -06:00
Sebastian Celis
15006424ed
Merge pull request #235 from scelis/fix/234_complex_placeholders
Fix an error caused by combining %@ with other placeholders
2018-01-26 11:39:42 -06:00
Sebastian Celis
cdec8c2109
Update CHANGELOG.md 2018-01-26 08:04:04 -06:00
Sebastian Celis
5cda6ace82
Merge pull request #237 from eclair4151/master
Fix safe yaml crash
2018-01-26 08:02:50 -06:00
Tomer Shemesh
e72f661b75
Fix safe yaml crash
Fix Safe Yaml crash

In some cases safe yaml will be loaded and cause it to crash when ever it is run before date class has been loaded. This can be seen in a few cases and makes it crash when ever twine is called. 

https://github.com/dtao/safe_yaml/issues/80
https://github.com/test-kitchen/test-kitchen/issues/1327
https://github.com/jekyll/jekyll/issues/3201

This commit fixes that issue
2018-01-26 08:57:52 -05:00
Sebastian Celis
c6b3c9c875 Fix an error caused by combining %@ with other placeholders
Fixes #234
2018-01-24 08:56:04 -06:00
Sebastian Celis
72098611ba Update version to 1.0.2 2018-01-20 10:38:24 -06:00
Sebastian Celis
4891736b1b Clean up android styling documentation 2017-12-30 15:38:07 -06:00
Sebastian Celis
7a7ca59c2d
Merge pull request #228 from sebastianludwig/212-improvement
Improved Android HTML escaping
2017-12-30 15:36:40 -06:00
Sebastian Celis
c2b517707a
Merge pull request #230 from sebastianludwig/223-unit-tests-for-222
Unit tests for regression fixed in #222
2017-12-30 15:29:28 -06:00
Sebastian Ludwig
77a7b49a18 Correct broken link. 2017-12-12 18:26:25 +01:00
Sebastian Ludwig
e838dcc8fd Add unit tests for regression fixed in #222. Closes #223. 2017-12-12 17:27:08 +01:00
Sebastian Ludwig
b5cd295e3a Escape angle brackets in Android formatter, if the string contains a placeholder to enable users to use the official way to retrieve these strings. Improves #212. 2017-12-12 15:26:38 +01:00
Sebastian Celis
0688167c59 Merge pull request #224 from scelis/1.0.1-release
Update version to 1.0.1
2017-10-18 08:35:32 -05:00
Sebastian Celis
30ac0d566d Update version to 1.0.1 2017-10-18 08:32:48 -05:00
Sebastian Celis
6f947b076d Merge pull request #222 from scelis/fix/prefer-formatter-in-args
Always prefer the formatter passed in
2017-10-18 08:29:58 -05:00
Sebastian Celis
bd37ebf582 Always prefer the formatter passed in
This fixes #221
2017-10-18 08:22:21 -05:00
Sebastian Celis
993557a7e2 Merge pull request #220 from scelis/1.0-release
Update version to 1.0
2017-10-16 15:24:32 -05:00
Sebastian Celis
4fa59d6205 Update version to 1.0 2017-10-16 15:22:27 -05:00
Sebastian Celis
8e3170ccd7 Move changelog and clean it up a bit 2017-10-16 15:13:35 -05:00
Sebastian Celis
041fe7d5cb Update CHANGELOG.txt 2017-10-16 15:03:44 -05:00
Sebastian Celis
960431ce52 Merge pull request #219 from philippeauriach/apple-base-language-option
use developer-language option for apple Base folder
2017-10-13 16:53:16 -05:00
Philippe Auriach
86cf20478b developer_language options only for one unit test 2017-10-13 23:29:49 +02:00
Philippe Auriach
f5af8cf670 Added test for base folder resolved to developer_language option value 2017-10-13 11:49:56 +02:00
Philippe Auriach
21fdd84682 ignore .DS_Store file 2017-10-13 11:30:34 +02:00
Philippe Auriach
8781528429 more readable if 2017-10-13 11:13:01 +02:00
Philippe Auriach
3043a48131 use developer-language option for apple Base folder 2017-10-10 16:16:32 +02:00
Sebastian Celis
922f1a72a1 Merge pull request #216 from scelis/212-android-tags
Android: Allow basic styling
2017-09-19 09:50:31 -05:00
Sebastian Ludwig
d621e25750 Add changelog. 2017-09-18 17:21:06 +02:00
Sebastian Ludwig
f7092c7605 Close #212: Change Android escaping to preserve basic styling tags and anything inside CDATA. 2017-09-18 12:26:07 +02:00
Sebastian Celis
9dc3845cae Merge pull request #215 from scelis/213-twine-version
Fix #213: Add --version again which got lost in c619dd6.
2017-09-14 07:48:21 -05:00
Sebastian Ludwig
f106e2e272 Fix #213: Add --version again which got lost in c619dd6. 2017-09-13 19:38:49 +02:00
Sebastian Ludwig
968d796389 Add GitHub issue template. 2017-08-27 14:17:03 -04:00
Sebastian Ludwig
545c106b44 Fail twine commands if there’s more than one formatter candidate. Fixes #201. 2017-08-23 19:32:26 -04:00
Sebastian Celis
f57c8db096 Merge pull request #207 from scelis/argument_spaces
Support arguments with spaces
2017-08-08 13:10:23 -05:00
Sebastian Ludwig
81f8f15f1d Remove double space. 2017-08-08 20:03:37 +02:00
Sebastian Ludwig
8bda06bf80 Support arguments with spaces by wrapping the complete parameter string in double quotes. Fixes #206. 2017-08-08 19:55:24 +02:00
Sebastian Celis
1789a59bc1 Merge pull request #203 from scelis/fix/android-output-folder-name
Fix android output folder name
2017-08-04 08:13:49 -05:00
Sebastian Celis
9d75f104df Add unit test 2017-08-04 08:12:08 -05:00
Sebastian Celis
7f2d75a1ca Merge pull request #202 from jporsay/master
Android output folders should have an "r" preceding the region code
2017-08-04 08:10:46 -05:00
Juan Pablo Orsay
e1984e06c0 Fix indentation 2017-08-03 17:20:49 -03:00
Juan Pablo Orsay
c6a9424f5a Android folder should have the region code preceded by lowercase "r" 2017-08-03 17:19:07 -03:00
Sebastian Celis
cd28717f92 Merge pull request #200 from scelis/fix-po-language-detection
Fix language detection for .po formatters
2017-07-31 10:19:24 -05:00
Sebastian Celis
1270ef2767 Fix language detection for .po formatters
The old regular expressions were only supporting two character language
identifiers. This commit allows languages to scale to things like
"en-GB".

Fixes #199
2017-07-31 10:17:16 -05:00
Sebastian Ludwig
f7e74392fa Fix typos. 2017-07-31 12:55:37 +02:00
Sebastian Celis
a165b98e1e Add .ruby-version to .gitignore 2017-07-27 09:14:51 -05:00
Sebastian Celis
6686a64743 Remove twine_ui link
The project is old and no longer maintained.
2017-06-28 09:13:42 -05:00
Sebastian Celis
a4f4e4fbb7 Merge pull request #195 from Zverik/q
Add 'q' placeholder modifier for android strings
2017-06-23 08:47:47 -05:00
Ilya Zverev
8a92e73971 Add 'q' placeholder modifier for android strings 2017-06-20 11:59:13 +03:00
Sebastian Celis
b7405f9b3c Merge pull request #193 from sebastianludwig/fix-boolean-cli-options
Fix #191 - `--no` variation for boolean options
2017-05-24 09:36:49 -05:00
Sebastian Ludwig
7b599e5f92 Fix #191 by using the parsed boolean value for CLI options, not always true. 2017-05-22 14:40:45 +02:00
Sebastian Celis
19b53343c6 Update readme 2017-05-20 16:35:37 -05:00
Sebastian Celis
e863aa6081 Add Circle CI badge 2017-04-26 10:42:58 -05:00
Sebastian Celis
efd637fb1b Merge pull request #189 from teespring/bogdan/continuous-integration
Setup continuous integration with Circle CI
2017-04-24 11:04:44 -05:00
Bogdan Vitoc
fc18caa77e "Setup continuous integration with Circle CI."
Test on multiple rubies: system (2.0.0), 2.1.*, 2.2.*, 2.3.*
Runs builds on macOS in preperation for Xcode integration testing.
2017-03-23 15:22:33 -07:00
Sebastian Celis
fe7e1a7d92 Update version to 0.10.1 2017-01-19 08:54:07 -06:00
Sebastian Celis
76bcd5becd Merge pull request #185 from sebastianludwig/184-fix-xcode-console
Fixed #184 by checking if IO.console is defined
2017-01-19 08:53:29 -06:00
Sebastian Ludwig
ae190eb8bd Fixed #184 by checking if IO.console is defined. 2017-01-19 14:56:53 +01:00
Sebastian Celis
ff17d05e14 Update version to 0.10.0 2017-01-17 09:07:25 -06:00
Sebastian Celis
35991b077a Merge pull request #183 from sebastianludwig/182-rename-drop-commands
Renamed loc-drop commands
2017-01-17 09:05:43 -06:00
Sebastian Ludwig
2f81399a7c Fixed #182: Renamed command generate-loc-drop to generate-localization-archive and consume-loc-drop to consume-localization-archive. 2017-01-16 23:32:02 +01:00
Sebastian Celis
c473ff6b9e Merge pull request #181 from sebastianludwig/cli
Improved CLI
2017-01-11 10:00:56 -06:00
Sebastian Ludwig
c619dd61e4 Improved CLI and only showing applicable options per command. 2016-12-20 00:47:14 +01:00
Sebastian Celis
b33d8425f8 Merge pull request #177 from sebastianludwig/fix-169-153
Implements #169 and resolves #153
2016-12-19 12:34:03 -06:00
Sebastian Celis
fdda09ca3c Merge pull request #178 from sebastianludwig/can-handle-directory-default-and-formatting
Minor improvements and a little cleanup
2016-12-14 17:13:25 -06:00
Sebastian Celis
9fc741c261 Merge pull request #180 from sebastianludwig/fix-179-android-string-placeholder
Fixed #179 by converting placeholders before escaping @ signs.
2016-12-14 17:12:37 -06:00
Sebastian Ludwig
e2c400ea6d Fixed #179 by converting placeholders before escaping @ signs. 2016-12-14 23:13:05 +01:00
Sebastian Ludwig
47e95e6db7 Added can_handle_directory? default implementation to Abstract. 2016-12-08 10:21:37 +01:00
Sebastian Ludwig
9dcc909335 Corrected source code formatting. 2016-12-08 10:21:37 +01:00
Sebastian Ludwig
ecefba0511 Fixed #153 by removing command deprecation warning. 2016-12-08 10:20:01 +01:00
Sebastian Ludwig
f9720a67d7 Implemented determine_language_given_path for Flash formatter. 2016-12-08 10:19:47 +01:00
Sebastian Ludwig
280914bdc5 Fixed #169 by adding proper placeholder handling to Flash formatter. 2016-12-08 10:18:15 +01:00
Sebastian Ludwig
9034da11e2 Removed unnecessary return statements. 2016-12-08 09:50:37 +01:00
Sebastian Celis
74d7cb1d85 Merge pull request #174 from sebastianludwig/fix-164-printf-space
Fixed #165 by disallowing single spaces as valid printf flags.
2016-11-21 15:16:46 -06:00
Sebastian Celis
8de511a099 Merge pull request #173 from sebastianludwig/fix-172-language-mappings
Fixed #172 by removing language mappings
2016-11-21 15:15:44 -06:00
Sebastian Ludwig
43b83cc8e6 Fixed #165 by disallowing single spaces as valid printf flags. 2016-11-21 22:05:25 +01:00
Sebastian Ludwig
37f1d11859 Fixed #172 by removing language mappings. 2016-11-21 21:44:56 +01:00
Sebastian Celis
c19b88d429 Merge pull request #170 from barinali/master
Update broken links in documentation
2016-08-19 15:18:16 -05:00
Ali Barın
f601b2c259 Update broken links in documentation 2016-08-19 21:10:18 +03:00
Sebastian Celis
b34a5d0357 Merge pull request #168 from sebastianludwig/advanced_tags
Added AND and NOT logic for tags.
2016-06-21 08:38:16 -05:00
Sebastian Ludwig
7c84dbb418 Added AND and NOT logic for tags. 2016-06-21 15:28:04 +02:00
Sebastian Celis
762196050e Merge pull request #161 from sebastianludwig/fix_117_xliff_tags
Fix 117 xliff tags
2016-05-16 11:19:58 -05:00
Sebastian Celis
a45bfba0ba Merge pull request #162 from sebastianludwig/empty-jquery-sections
Empty jquery sections
2016-05-04 09:50:09 -05:00
Sebastian Ludwig
d8387e55f4 Added unit test to verify empty sections don't contribute to jQuery formatter output. 2016-05-03 23:25:44 +02:00
Matthew Abbott
06cd167f2b Remote empty sections before joining them together
Previously, a comma was being printed on the last line of every section, even if
that section did not contribute any rows to the formatted output (e.g., no rows
within that section had the tags needed to be output during this run). When such
a zero-row section happens, it results in an line with only a comma on it, which
is not valid json. This can make incremental adjustment of the jquery files
rather tedious, even when using gui tools to quickly reject changes.
2016-05-03 23:11:47 +02:00
Sebastian Ludwig
a53384c5d7 Added test to verify the android formatter unescapes values properly. 2016-05-03 22:59:22 +02:00
Sebastian Ludwig
60b0eb2adf Allowing xliff tags in android values and escaping special characters as recommended by Android docs. 2016-05-03 22:59:22 +02:00
Sebastian Celis
50b1e90f8f Merge pull request #158 from sebastianludwig/test_improvements
Test improvements
2016-05-03 15:54:14 -05:00
Sebastian Ludwig
2e283d7057 Got rid of the Case in test class names. 2016-05-03 22:44:22 +02:00
Sebastian Celis
b779e112ff Merge pull request #160 from PaulWagener/django-utf8
Add UTF-8 Content-Type to the Django formatter
2016-05-03 15:09:54 -05:00
Paul Wagener
111840ed70 Add UTF-8 Content-Type to the Django formatter 2016-05-03 21:53:12 +02:00
Sebastian Celis
be5bb1f4b6 Merge pull request #159 from sebastianludwig/fix_81
Fixed #81
2016-04-27 09:11:45 -05:00
Sebastian Ludwig
ea58bd10ca Properly parsing multiline strings in Android formatter by using REXML instead of regexps. Fixes #81. 2016-04-27 08:35:52 +02:00
Sebastian Celis
0a96b11099 Merge pull request #157 from sebastianludwig/fix_155
Fixed #155 by removing mapping from zh to zh-Hans in Android formatter.
2016-04-26 15:37:28 -05:00
Sebastian Ludwig
f4cbf08122 Fixed #155 by removing mapping from zh to zh-Hans in Android formatter. 2016-04-26 21:11:34 +02:00
Sebastian Celis
b47ab9369b Improve tags section.
This fixes #152.
2016-04-05 07:45:49 -05:00
Sebastian Celis
a03c6b5e58 Merge pull request #149 from sebastianludwig/documentation
Added documentation assets.
2016-04-04 11:39:30 -05:00
Sebastian Ludwig
8d4246b8b1 Added formatter documentation as markdown file. 2016-04-03 23:08:18 +02:00
Sebastian Celis
1e1b73fda2 Merge pull request #147 from sebastianludwig/naming
Better naming
2016-04-03 14:06:54 -04:00
Sebastian Ludwig
ce5c9a1828 Incorporated PR feedback. 2016-04-02 10:59:53 +02:00
Sebastian Celis
035afe3df3 Be explicit when requiring formatters.
I think I prefer this explicitness over the prior
automatic importing code. It feels safer (it won't
load files that are placed into the formatters
folder that shouldn't be there) and it avoids the
issue we had in #145 where formatters can load in
arbitrary order.
2016-04-01 15:58:57 -04:00
Sebastian Celis
ae31911a62 Update version to 0.9.1 2016-04-01 15:31:32 -04:00
Sebastian Ludwig
8a8e2d992f Added documentation assets. 2016-03-31 22:48:08 +02:00
Sebastian Ludwig
ef84dd322c Renamed commands to get rid of more 'string' ambiguities. 2016-03-31 22:36:06 +02:00
Sebastian Ludwig
b1b59f4f62 Updated README 2016-03-31 22:36:06 +02:00
Sebastian Ludwig
594fbfddcc Renamed StringsFile to TwineFile, StringsSection to TwineSection and StringsRow to TwineDefinition. 2016-03-31 22:36:06 +02:00
Sebastian Celis
043836e84f Merge pull request #148 from sebastianludwig/ubuntu_fix
Fixed #145
2016-03-31 10:04:44 -04:00
Sebastian Ludwig
00a8907646 Fixed #145 by requiring abstract formatter before requiring any other formatter to ensure it's loaded first, in case files are not required in alphabetical order. 2016-03-31 14:30:55 +02:00
Sebastian Celis
d026c65f44 Merge pull request #146 from paulvi/patch-1
README - colorize example
2016-03-22 09:23:57 -05:00
Paul Verest
014a6a36ca README - colorize example 2016-03-22 09:26:29 +08:00
63 changed files with 7011 additions and 1555 deletions

37
.github/workflows/test.yml vendored Normal file
View file

@ -0,0 +1,37 @@
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
# This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake
# For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby
name: Test
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
ruby-version: ['2.6', '2.7', '3.0', '3.1']
steps:
- uses: actions/checkout@v3
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby-version }}
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
- name: Install dependencies
run: bundle install
- name: Run tests
run: bundle exec rake test

2
.gitignore vendored
View file

@ -1,3 +1,5 @@
*.gem
.idea/
*.lock
.ruby-version
.DS_Store

59
CHANGELOG.md Normal file
View file

@ -0,0 +1,59 @@
# 1.1.2 (2022-11-15)
- Bugfix: Fixed a runtime error caused by a missing rexml dependency in Ruby 3 (#312)
# 1.1.1 (2021-01-28)
- Bugfix: Properly parse multiline comments in Android XML files (#300)
# 1.1 (2020-07-09)
- Feature: Add --escape-all-tags option to force escaping of Android styling tags (#281)
- Improvement: Twine now requires Ruby 2.4 or greater and rubyzip 2.0 or greater (#297)
- Bugfix: Fix issues with the Django formatter (#289)
# 1.0.6 (2019-05-28)
- Improvement: Support more Android styling tags (#278)
- Improvement: Update Android output path for default language (#276)
# 1.0.5 (2019-02-24)
- Bugfix: Incorrect language detection when reading localization files (#251)
- Bugfix: Double quotes in Android files could be converted to single quotes (#254)
- Bugfix: Properly escape quotes when writing gettext files (#268)
# 1.0.4 (2018-05-30)
- Feature: Add a --quiet option (#245)
- Bugfix: Consume child HTML tags in Android formatter (#247)
- Bugfix: Let consume-localization-archive return a non-zero status (#246)
# 1.0.3 (2018-01-26)
- Bugfix: Workaround a possible crash in safe_yaml (#237)
- Bugfix: Fix an error caused by combining %@ with other placeholders (#235)
# 1.0.2 (2018-01-20)
- Improvement: Better support for placeholders in HTML styled Android strings (#212)
# 1.0.1 (2017-10-17)
- Bugfix: Always prefer the passed-in formatter (#221)
# 1.0 (2017-10-16)
- Feature: Fail twine commands if there's more than one formatter candidate (#201)
- Feature: In the Apple formatter, use developer language for the base localization (#219)
- Bugfix: Preserve basic HTML styling for Android strings (#212)
- Bugfix: `twine --version` reports "unknown" (#213)
- Bugfix: Support spaces in command line arguments (#206)
- Bugfix: 'r' missing before region code in Android output folder (#203)
- Bugfix: .po formatter language detection (#199)
- Bugfix: Add 'q' placeholder for android strings (#194)
- Bugfix: Boolean command line parameters are always true (#191)
# 0.10.1 (2017-01-19)
- Bugfix: Xcode integration (#184)

6
ISSUE_TEMPLATE.md Normal file
View file

@ -0,0 +1,6 @@
If you're reporting a bug, please do the following:
- Mention the Twine version you're using
- Provide a minimal but complete input file causing the error (inline with triple ` is fine)
- Quote the exact twine command that's being run
- Provide the complete _expected_ output (again, inline is fine)

261
README.md
View file

@ -1,39 +1,28 @@
# Twine
Twine is a command line tool for managing your strings and their translations. These strings are all stored in a master text file and then Twine uses this file to import and export strings in a variety of file types, including iOS and Mac OS X `.strings` files, Android `.xml` files, gettext `.po` files, and [jquery-localize][jquerylocalize] `.json` files. This allows individuals and companies to easily share strings across multiple projects, as well as export strings in any format the user wants.
Twine is a command line tool for managing your strings and their translations. These are all stored in a single text file and then Twine uses this file to import and export localization files in a variety of types, including iOS and Mac OS X `.strings` files, Android `.xml` files, gettext `.po` files, and [jquery-localize][jquerylocalize] `.json` files. This allows individuals and companies to easily share translations across multiple projects, as well as export localization files in any format the user wants.
## Install
### As a Gem
Twine is most easily installed as a Gem.
$ gem install twine
### From Source
## Twine File Format
You can also run Twine directly from source. However, it requires [rubyzip][rubyzip] in order to create and read standard zip files.
Twine stores everything in a single file, the Twine data file. The format of this file is a slight variant of the [Git][git] config file format, which itself is based on the old [Windows INI file][INI] format. The entire file is broken up into sections, which are created by placing the section name between two pairs of square brackets. Sections are optional, but they are the recommended way of grouping your definitions into smaller, more manageable chunks.
$ gem install rubyzip
$ git clone git://github.com/mobiata/twine.git
$ cd twine
$ ./twine --help
Make sure you run the `twine` executable at the root of the project as it properly sets up your Ruby library path. The `bin/twine` executable does not.
## String File Format
Twine stores all of its strings in a single file. The format of this file is a slight variant of the [Git][git] config file format, which itself is based on the old [Windows INI file][INI] format. The entire file is broken up into sections, which are created by placing the section name between two pairs of square brackets. Sections are optional, but they are a recommended way of breaking your strings into smaller, more manageable chunks.
Each grouping section contains N string definitions. These string definitions start with the string key placed within a single pair of square brackets. This string definition then contains a number of key-value pairs, including a comment, a comma-separated list of tags (which are used by Twine to select a subset of strings), and all of the translations.
Each grouping section contains N definitions. These definitions start with the key placed within a single pair of square brackets. It then contains a number of key-value pairs, including a comment, a comma-separated list of tags and all of the translations.
### Placeholders
Twine supports [`printf` style placeholders](https://en.wikipedia.org/wiki/Printf_format_string) with one peculiarity: `@` is used for strings instead of `s`. This is because Twine started out as a tool for iOS and OS X projects.
Twine supports [`printf` style placeholders][printf] with one peculiarity: `@` is used for strings instead of `s`. This is because Twine started out as a tool for iOS and OS X projects.
### Tags
Tags are used by Twine as a way to only work with a subset of your strings at any given point in time. Each string can be assigned zero or more tags which are separated by commas. Tags are optional, though highly recommended. You can get a list of all strings currently missing tags by executing the `validate-strings-file` command.
Tags are used by Twine as a way to only work with a subset of your definitions at any given point in time. Each definition can be assigned zero or more tags which are separated by commas. Tags are optional, though highly recommended. You can get a list of all definitions currently missing tags by executing the [`validate-twine-file`](#validate-twine-file) command with the `--pedantic` option.
When generating a localization file, you can specify which definitions should be included using the `--tags` option. Provide a comma separated list of tags to match all definitions that contain any of the tags (`--tags tag1,tag2` matches all definitions tagged with `tag1` _or_ `tag2`). Provide multiple `--tags` options to match definitions containing all specified tags (`--tags tag1 --tags tag2` matches all definitions tagged with `tag1` _and_ `tag2`). You can match definitions _not_ containing a tag by prefixing the tag with a tilde (`--tags ~tag1` matches all definitions _not_ tagged with `tag1`). All three options are combinable.
### Whitespace
@ -41,116 +30,125 @@ Whitepace in this file is mostly ignored. If you absolutely need to put spaces a
### References
If you want a key to inherit the values of another key, you can use a reference. Any property not specified for a key will be taken from the reference.
If you want a definition to inherit the values of another definition, you can use a reference. Any property not specified for a definition will be taken from the reference.
### Example
[[General]]
[yes]
en = Yes
es = Sí
fr = Oui
ja = はい
[no]
en = No
fr = Non
ja = いいえ
```ini
[[General]]
[yes]
en = Yes
es = Sí
fr = Oui
ja = はい
[no]
en = No
fr = Non
ja = いいえ
[[Errors]]
[path_not_found_error]
en = The file '%@' could not be found.
tags = app1,app6
comment = An error describing when a path on the filesystem could not be found.
[network_unavailable_error]
en = The network is currently unavailable.
tags = app1
comment = An error describing when the device can not connect to the internet.
[dismiss_error]
ref = yes
en = Dismiss
[[Errors]]
[path_not_found_error]
en = The file '%@' could not be found.
tags = app1,app6
comment = An error describing when a path on the filesystem could not be found.
[network_unavailable_error]
en = The network is currently unavailable.
tags = app1
comment = An error describing when the device can not connect to the internet.
[dismiss_error]
ref = yes
en = Dismiss
[[Escaping Example]]
[list_item_separator]
en = `, `
tags = mytag
comment = A string that should be placed between multiple items in a list. For example: Red, Green, Blue
[grave_accent_quoted_string]
en = ``%@``
tags = myothertag
comment = This string will evaluate to `%@`.
[[Escaping Example]]
[list_item_separator]
en = `, `
tags = mytag
comment = A string that should be placed between multiple items in a list. For example: Red, Green, Blue
[grave_accent_quoted_string]
en = ``%@``
tags = myothertag
comment = This string will evaluate to `%@`.
```
## Supported Output Formats
Twine currently supports the following formats for outputting strings:
Twine currently supports the following output formats:
* [iOS and OS X String Resources][applestrings] (format: apple)
* [Android String Resources][androidstrings] (format: android)
* HTML tags will be escaped by replacing `<` with `&lt`
* Tags inside `<![CDATA[` won't be escaped.
* Supports [basic styling][androidstyling] according to [Android documentation](https://developer.android.com/guide/topics/resources/string-resource.html#StylingWithHTML). All of the documented tags are supported, in addition to `<a>` links.
* These tags will *not* be escaped if the string doesn't contain placeholders. You can reference them directly in your layouts or by using [`getText()`](https://developer.android.com/reference/android/content/res/Resources.html#getText(int)) to read them programmatically.
* These tags *will* be escaped if the string contains placeholders. You can use [`getString()`](https://developer.android.com/reference/android/content/res/Resources.html#getString(int,%20java.lang.Object...)) combined with [`fromHtml`](https://developer.android.com/reference/android/text/Html.html#fromHtml(java.lang.String)) as shown in the [documentation][androidstyling] to display them.
* See [\#212](https://github.com/scelis/twine/issues/212) for details.
* [Gettext PO Files][gettextpo] (format: gettext)
* [jquery-localize Language Files][jquerylocalize] (format: jquery)
* [Django PO Files][djangopo] (format: django)
* [Tizen String Resources][tizen] (format: tizen)
* [Flash/Flex Properties][flash] (format: flash)
If you would like to enable twine to create language files in another format, create an appropriate formatter in `lib/twine/formatters`.
If you would like to enable Twine to create localization files in another format, read the wiki page on how to create an appropriate formatter.
## Usage
Usage: twine COMMAND STRINGS_FILE [INPUT_OR_OUTPUT_PATH] [--lang LANG1,LANG2...] [--tags TAG1,TAG2,TAG3...] [--format FORMAT]
Usage: twine COMMAND TWINE_FILE [INPUT_OR_OUTPUT_PATH] [--lang LANG1,LANG2...] [--tags TAG1,TAG2,TAG3...] [--format FORMAT]
### Commands
#### `generate-string-file`
#### `generate-localization-file`
This command creates an Apple or Android strings file from the master strings data file. If the output file would not contain any translations, twine will exit with an error.
This command creates a localization file from the Twine data file. If the output file would not contain any translations, Twine will exit with an error.
$ twine generate-string-file /path/to/strings.txt values-ja.xml --tags common,app1
$ twine generate-string-file /path/to/strings.txt Localizable.strings --lang ja --tags mytag
$ twine generate-string-file /path/to/strings.txt all-english.strings --lang en
$ twine generate-localization-file /path/to/twine.txt values-ja.xml --tags common,app1
$ twine generate-localization-file /path/to/twine.txt Localizable.strings --lang ja --tags mytag
$ twine generate-localization-file /path/to/twine.txt all-english.strings --lang en
#### `generate-all-string-files`
#### `generate-all-localization-files`
This command is a convenient way to call `generate-string-file` multiple times. It uses standard Mac OS X, iOS, and Android conventions to figure out exactly which files to create given a parent directory. For example, if you point it to a parent directory containing `en.lproj`, `fr.lproj`, and `ja.lproj` subdirectories, Twine will create a `Localizable.strings` file of the appropriate language in each of them. However, files that would not contain any translations will not be created; instead warnings will be logged to `stderr`. This is often the command you will want to execute during the build phase of your project.
This command is a convenient way to call [`generate-localization-file`](#generate-localization-file) multiple times. It uses standard conventions to figure out exactly which files to create given a parent directory. For example, if you point it to a parent directory containing `en.lproj`, `fr.lproj`, and `ja.lproj` subdirectories, Twine will create a `Localizable.strings` file of the appropriate language in each of them. However, files that would not contain any translations will not be created; instead warnings will be logged to `stderr`. This is often the command you will want to execute during the build phase of your project.
$ twine generate-all-string-files /path/to/strings.txt /path/to/project/locales/directory --tags common,app1
$ twine generate-all-localization-files /path/to/twine.txt /path/to/project/locales/directory --tags common,app1
#### `consume-string-file`
#### `consume-localization-file`
This command slurps all of the strings from a `.strings` or `.xml` file and incorporates the translated text into the master strings data file. This is a simple way to incorporate any changes made to a single file by one of your translators. It will only identify strings that already exist in the master data file.
This command slurps all of the translations from a localization file and incorporates the translated strings into the Twine data file. This is a simple way to incorporate any changes made to a single file by one of your translators. It will only identify definitions that already exist in the data file.
$ twine consume-string-file /path/to/strings.txt fr.strings
$ twine consume-string-file /path/to/strings.txt Localizable.strings --lang ja
$ twine consume-string-file /path/to/strings.txt es.xml
$ twine consume-localization-file /path/to/twine.txt fr.strings
$ twine consume-localization-file /path/to/twine.txt Localizable.strings --lang ja
$ twine consume-localization-file /path/to/twine.txt es.xml
#### `consume-all-string-files`
#### `consume-all-localization-files`
This command reads in a folder containing many `.strings` or `.xml` files. These files should be in a standard folder hierarchy so that twine knows the language of each file. When combined with the `--developer-language`, `--consume-comments`, and `--consume-all` flags, this command is a great way to create your initial strings data file from an existing iOS or Android project. Just make sure that you create a blank strings.txt file, first!
This command reads in a folder containing many localization files. These files should be in a standard folder hierarchy so that Twine knows the language of each file. When combined with the `--developer-language`, `--consume-comments`, and `--consume-all` flags, this command is a great way to create your initial Twine data file from an existing project. Just make sure that you create a blank Twine data file first!
$ twine consume-all-string-files strings.txt Resources/Locales --developer-language en --consume-all --consume-comments
$ twine consume-all-localization-files twine.txt Resources/Locales --developer-language en --consume-all --consume-comments
#### `generate-loc-drop`
#### `generate-localization-archive`
This command is a convenient way to generate a zip file containing files created by the `generate-string-file` command. If a file would not contain any translated strings, it is skipped and a warning is logged to `stderr`. This command can be used to create a single zip containing a large number of strings in all languages which you can then hand off to your translation team.
This command is a convenient way to generate a zip file containing files created by the [`generate-localization-file`](#generate-localization-file) command. If a file would not contain any translated strings, it is skipped and a warning is logged to `stderr`. This command can be used to create a single zip containing a large number of translations in all languages which you can then hand off to your translation team.
$ twine generate-loc-drop /path/to/strings.txt LocDrop1.zip
$ twine generate-loc-drop /path/to/strings.txt LocDrop2.zip --lang en,fr,ja,ko --tags common,app1
$ twine generate-localization-archive /path/to/twine.txt LocDrop1.zip
$ twine generate-localization-archive /path/to/twine.txt LocDrop2.zip --lang en,fr,ja,ko --tags common,app1
#### `consume-loc-drop`
#### `consume-localization-archive`
This command is a convenient way of taking a zip file and executing the `consume-string-file` command on each file within the archive. It is most often used to incorporate all of the changes made by the translation team after they have completed work on a localization drop.
This command is a convenient way of taking a zip file and executing the [`consume-localization-file`](#consume-localization-file) command on each file within the archive. It is most often used to incorporate all of the changes made by the translation team after they have completed work on a localization archive.
$ twine consume-loc-drop /path/to/strings.txt LocDrop2.zip
$ twine consume-localization-archive /path/to/twine.txt LocDrop2.zip
#### `validate-strings-file`
#### `validate-twine-file`
This command validates that the strings file can be parsed, contains no duplicate keys, and that all strings have at least one tag. It will exit with a non-zero status code if any of those criteria are not met.
This command validates that the Twine data file can be parsed, contains no duplicate keys, and that no key contains invalid characters. It will exit with a non-zero status code if any of those criteria are not met.
$ twine validate-strings-file /path/to/strings.txt
$ twine validate-twine-file /path/to/twine.txt
## Creating Your First strings.txt File
## Creating Your First Twine Data File
The easiest way to create your first strings.txt file is to run the `consume-all-string-files` command. The one caveat is to first create a blank strings.txt file to use as your starting point. Then, just point the `consume-all-string-files` command at a directory in your project containing all of your iOS, OS X, or Android strings files.
The easiest way to create your first Twine data file is to run the [`consume-all-localization-files`](#consume-all-localization-files) command. The one caveat is to first create a blank file to use as your starting point. Then, just point the `consume-all-localization-files` command at a directory in your project containing all of your localization files.
$ touch strings.txt
$ twine consume-all-string-files strings.txt Resources/Locales --developer-language en --consume-all --consume-comments
$ touch twine.txt
$ twine consume-all-localization-files twine.txt Resources/Locales --developer-language en --consume-all --consume-comments --format apple/android/gettext/jquery/django/tizen/flash
## Twine and Your Build Process
@ -159,12 +157,12 @@ The easiest way to create your first strings.txt file is to run the `consume-all
It is easy to incorporate Twine right into your iOS and OS X app build processes.
1. In your project folder, create all of the `.lproj` directories that you need. It does not really matter where they are. We tend to put them in `Resources/Locales/`.
2. Run the `generate-all-string-files` command to create all of the string files you need in these directories. For example,
2. Run the [`generate-all-localization-files`](#generate-all-localization-files) command to create all of the `.strings` files you need in these directories. For example,
$ twine generate-all-string-files strings.txt Resources/Locales/ --tags tag1,tag2
$ twine generate-all-localization-files twine.txt Resources/Locales/ --tags tag1,tag2
Make sure you point Twine at your strings data file, the directory that contains all of your `.lproj` directories, and the tags that describe the strings you want to use for this project.
3. Drag the `Resources/Locales/` directory to the Xcode project navigator so that Xcode knows to include all of these strings files in your build.
Make sure you point Twine at your data file, the directory that contains all of your `.lproj` directories, and the tags that describe the definitions you want to use for this project.
3. Drag the `Resources/Locales/` directory to the Xcode project navigator so that Xcode knows to include all of these `.strings` files in your build.
4. In Xcode, navigate to the "Build Phases" tab of your target.
5. Click on the "Add Build Phase" button and select "Add Run Script".
6. Drag the new "Run Script" build phase up so that it runs earlier in the build process. It doesn't really matter where, as long as it happens before the resources are copied to your bundle.
@ -174,46 +172,66 @@ Now, whenever you build your application, Xcode will automatically invoke Twine
### Android Studio/Gradle
Add the following task at the top level in app/build.gradle:
#### Standard
Add the following code to `app/build.gradle`:
```
task generateStrings {
String script = 'if hash twine 2>/dev/null; then twine generate-string-file strings.txt ./src/main/res/values/generated_strings.xml; fi'
exec {
executable "sh"
args '-c', script
}
task generateLocalizations {
String script = 'if hash twine 2>/dev/null; then twine generate-localization-file twine.txt ./src/main/res/values/generated_strings.xml; fi'
exec {
executable "sh"
args '-c', script
}
}
preBuild {
dependsOn generateLocalizations
}
```
Now every time you build your app the strings are generated from the twine file.
#### Using [jruby](http://jruby.org)
With this approach, developers do not need to manually install ruby, gem, or twine.
Add the following code to `app/build.gradle`:
```
buildscript {
repositories { jcenter() }
dependencies {
/* NOTE: Set your preferred version of jruby here. */
classpath "com.github.jruby-gradle:jruby-gradle-plugin:1.5.0"
}
}
apply plugin: 'com.github.jruby-gradle.base'
dependencies {
/* NOTE: Set your preferred version of twine here. */
jrubyExec 'rubygems:twine:1.1'
}
task generateLocalizations (type: JRubyExec) {
dependsOn jrubyPrepare
jrubyArgs '-S'
script "twine"
scriptArgs 'generate-localization-file', 'twine.txt', './src/main/res/values/generated_strings.xml'
}
preBuild {
dependsOn generateLocalizations
}
```
## User Interface
* [Twine TextMate 2 Bundle](https://github.com/mobiata/twine.tmbundle) — This [TextMate 2](https://github.com/textmate/textmate) bundle will make it easier for you to work with Twine strings files. In particular, it lets you use code folding to easily collapse and expand both strings and sections.
* [twine_ui](https://github.com/Daij-Djan/twine_ui) — A user interface for Twine written by [Dominik Pich](https://github.com/Daij-Djan/). Consider using this if you would prefer to use Twine without dropping to a command line.
* [Twine TextMate 2 Bundle](https://github.com/mobiata/twine.tmbundle) — This [TextMate 2](https://github.com/textmate/textmate) bundle will make it easier for you to work with Twine files. In particular, it lets you use code folding to easily collapse and expand both definitions and sections.
## Plugin Support
## Extending Twine
Twine supports a basic plugin infrastructure, allowing third-party code to provide support for additional formatters. Twine will read a yaml config file specifying which plugins to load from three locations.
0. `./twine.yml` The current working directory
0. `~/.twine` The home directory
0. `/etc/twine.yml` The etc directory
Plugins are specified as values for the `gems` key. The following is an example config:
```
gems: appium_twine
```
Multiple gems can also be specfied in the yaml file.
```
gems: [appium_twine, some_other_plugin]
```
[appium_twine](https://github.com/appium/appium_twine) is a sample plugin used to provide a C# formatter.
If there's a format Twine does not yet support and you're keen to change that, check out the [documentation](documentation/formatters.md).
## Contributors
@ -227,7 +245,7 @@ Many thanks to all of the contributors to the Twine project, including:
* [Kevin Wood](https://github.com/kwood)
* [Mohammad Hejazi](https://github.com/MohammadHejazi)
* [Robert Guo](http://www.robertguo.me/)
* [sebastianludwig](https://github.com/sebastianludwig)
* [Sebastian Ludwig](https://github.com/sebastianludwig)
* [Sergey Pisarchik](https://github.com/SergeyPisarchik)
* [Shai Shamir](https://github.com/pichirichi)
@ -235,9 +253,12 @@ Many thanks to all of the contributors to the Twine project, including:
[rubyzip]: http://rubygems.org/gems/rubyzip
[git]: http://git-scm.org/
[INI]: http://en.wikipedia.org/wiki/INI_file
[applestrings]: http://developer.apple.com/documentation/Cocoa/Conceptual/LoadingResources/Strings/Strings.html
[applestrings]: https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/LoadingResources/Strings/Strings.html
[androidstrings]: http://developer.android.com/guide/topics/resources/string-resource.html
[androidstyling]: http://developer.android.com/guide/topics/resources/string-resource.html#FormattingAndStyling
[gettextpo]: http://www.gnu.org/savannah-checkouts/gnu/gettext/manual/html_node/PO-Files.html
[jquerylocalize]: https://github.com/coderifous/jquery-localize
[djangopo]: https://docs.djangoproject.com/en/dev/topics/i18n/translation/
[tizen]: https://developer.tizen.org/documentation/articles/localization
[flash]: http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/mx/resources/IResourceManager.html#getString()
[printf]: https://en.wikipedia.org/wiki/Printf_format_string

File diff suppressed because it is too large Load diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

View file

@ -0,0 +1,89 @@
# How Formatters Work
It's a formatters job to transform a Twine file into a localization file of a specific format. That task is solved using a hierarchical approach. This document describes the process in detail using the following Twine file as an example.
```
[[General]]
[yes]
en = Yes
[no]
en = No
[[Messages]]
[success]
en = All good
comment = Everything worked
```
A Twine file consists of multiple _sections_, each _section_ contains _keys_ with translated _values_, _comments_ and so on. If we highlight the components of the example file, it looks like this:
![Highlighted Twine file components](assets/formatter_1.png)
A formatter takes each of these components (and a few more) and formats them, working it's way from the outside inwards. First the method `format_file` is called, which doesn't do much more than calling `format_sections` which in turn calls `format_section` for each section and so on. To get an overview which method calls which, look at the next picture, where each method is represented by a block that is called by its surrounding block - read it from top to bottom and from the outside in.
![formatting method calling structure](assets/formatter_2.png)
If we map the _input_ for each method to the example file, it looks like this
![highlighted formatter method input](assets/formatter_3.png)
As stated at the beginning, the output produced by a formatter depends on the formatter. The output of the Apple formatter would for example be
```
/********** General **********/
"yes" = "Yes";
"no" = "No";
/********** Messages **********/
/* Everything worked */
"success" = "All good";
```
Or, highlighted by the method that produces each piece of the output
![highlighted formatter method output](assets/formatter_4.png)
The process described above is implemented by the structure giving formatter base class [`Abstract`](/lib/twine/formatters/abstract.rb). To generate a desired output format, a formatter overwrites just enough methods to gain as much control as it needs - it's basically the [Template Method Pattern](https://en.wikipedia.org/wiki/Template_method_pattern) applied again and again.
# Write a Formatter
Formatters inherit from [`Abstract`](/lib/twine/formatters/abstract.rb) and need to specify some information about the format they are supporting like the format name, the default file name, if they can handle a directory of localization files and so on - just take a look at the class and the [other formatters](/lib/twine/formatters) to get an idea.
The `Abstract` formatter also specifies two utility methods to be used when read a localization file, `set_translation_for_key` and `set_comment_for_key`, however the actual parsing is too formatter specific and must be implemented in the `read` method of a formatter.
Which methods to overwrite to produce the desired output depends pretty much on the format. Again, looking at the [bundled formatters](/lib/twine/formatters) will provide some insight.
Finally, to make a formatter available, it needs to be added to the list of formatters
```
Twine::Formatters.formatters << MyWickedFormatter.new
```
# Plugins
Once a formatter has been developed, it can be distributed as a gem and loaded as a plugin. Twine will read a yaml config file specifying which plugins to load from three locations.
0. `./twine.yml` The current working directory
0. `~/.twine` The home directory
0. `/etc/twine.yml` The etc directory
Plugins are specified as values for the `gems` key. The following is an example config:
```
gems: wicked_twine
```
Multiple gems can also be specified in the yaml file.
```
gems: [wicked_twine, some_other_plugin]
```
## Sample Plugins
* [appium-twine](https://github.com/appium/appium_twine)
* [twine-flutter](https://github.com/tiknil/twine-flutter)

View file

@ -21,13 +21,22 @@ module Twine
class Error < StandardError
end
require 'twine/version'
require 'twine/plugin'
require 'twine/cli'
require 'twine/stringsfile'
require 'twine/twine_file'
require 'twine/encoding'
require 'twine/output_processor'
require 'twine/placeholders'
require 'twine/formatters'
require 'twine/formatters/abstract'
require 'twine/formatters/android'
require 'twine/formatters/apple'
require 'twine/formatters/apple_plural'
require 'twine/formatters/django'
require 'twine/formatters/flash'
require 'twine/formatters/gettext'
require 'twine/formatters/jquery'
require 'twine/formatters/tizen'
require 'twine/runner'
require 'twine/version'
require 'twine/cli'
end

View file

@ -1,184 +1,433 @@
require 'optparse'
require 'io/console'
module Twine
module CLI
NEEDED_COMMAND_ARGUMENTS = {
'generate-string-file' => 3,
'generate-all-string-files' => 3,
'consume-string-file' => 3,
'consume-all-string-files' => 3,
'generate-loc-drop' => 3,
'consume-loc-drop' => 3,
'validate-strings-file' => 2
ALL_FORMATS = Formatters.formatters.map(&:format_name).map(&:downcase)
OPTIONS = {
consume_all: {
switch: ['-a', '--[no-]consume-all'],
description: 'Normally Twine will ignore any translation keys that do not exist in your Twine file.',
boolean: true
},
consume_comments: {
switch: ['-c', '--[no-]consume-comments'],
description: <<-DESC,
Normally Twine will ignore all comments in the file. With this flag set, any
comments encountered will be read and parsed into the Twine data file. This is especially useful
when creating your first Twine data file from an existing project.
DESC
boolean: true
},
create_folders: {
switch: ['-r', '--[no-]create-folders'],
description: <<-DESC,
This flag may be used to create output folders for all languages, if they don't exist yet.
As a result all languages will be exported, not only the ones where an output folder already exists.
DESC
boolean: true
},
developer_language: {
switch: ['-d', '--developer-language LANG'],
description: <<-DESC,
When writing the Twine data file, set the specified language as the "developer language". In
practice, this just means that this language will appear first in the Twine data file. When
generating files this language will be used as default language and its translations will be
used if a definition is not localized for the output language.
DESC
},
encoding: {
switch: ['-e', '--encoding ENCODING'],
description: <<-DESC,
Twine defaults to encoding all output files in UTF-8. This flag will tell Twine to use an alternate
encoding for these files. For example, you could use this to write Apple .strings files in UTF-16.
When reading files, Twine does its best to determine the encoding automatically. However, if the
files are UTF-16 without BOM, you need to specify if it's UTF-16LE or UTF16-BE.
DESC
},
escape_all_tags: {
switch: ['--[no-]escape-all-tags'],
description: <<-DESC,
Always escape all HTML tags. By default the Android formatter will ONLY escape styling tags, if a
string also contains placeholders. This flag enforces that styling tags are escaped regardless of
placeholders.
DESC
},
file_name: {
switch: ['-n', '--file-name FILE_NAME'],
description: 'This flag may be used to overwrite the default file name of the format.'
},
format: {
switch: ['-f', '--format FORMAT', ALL_FORMATS],
description: <<-DESC,
The file format to read or write: (#{ALL_FORMATS.join(', ')}). Additional formatters can be placed in the formats/ directory.
DESC
},
:include => {
switch: ['-i', '--include SET', [:all, :translated, :untranslated]],
description: <<-DESC,
This flag will determine which definitions are included. It's possible values are:
all: All definitions both translated and untranslated for the specified language are included.
This is the default value.
translated: Only definitions with translation for the specified language are included.
untranslated: Only definitions without translation for the specified language are included.
DESC
default: :all
},
languages: {
switch: ['-l', '--lang LANGUAGES', Array],
description: 'Comma separated list of language codes to use for the specified action.'
},
output_path: {
switch: ['-o', '--output-file OUTPUT_FILE'],
description: 'Write a new Twine file at this location instead of replacing the original file.'
},
pedantic: {
switch: ['-p', '--[no-]pedantic'],
description: 'When validating a Twine file, perform additional checks that go beyond pure validity (like presence of tags).'
},
quiet: {
switch: ['-q', '--[no-]quiet'],
description: 'Suppress all console output except error messages.'
},
tags: {
switch: ['-t', '--tags TAG1,TAG2,TAG3', Array],
description: <<-DESC,
Only definitions with ANY of the specified tags will be processed. Specify this option multiple
times to only include definitions with ALL of the specified tags. Prefix a tag with ~ to include
definitions NOT containing that tag. Omit this option to match all definitions in the Twine data file.
DESC
repeated: true
},
untagged: {
switch: ['-u', '--[no-]untagged'],
description: <<-DESC,
If you have specified tags using the --tags flag, then only those tags will be selected. If you also
want to select all definitions that are untagged, then you can specify this option to do so.
DESC
},
validate: {
switch: ['--[no-]validate'],
description: 'Validate the Twine file before formatting it.'
}
}
COMMANDS = {
'generate-localization-file' => {
description: 'Generates a localization file in a certain LANGUAGE given a particular FORMAT. This script will attempt to guess both the language and the format given the filename and extension. For example, "ko.xml" will generate a Korean language file for Android.',
arguments: [:twine_file, :output_path],
optional_options: [
:developer_language,
:encoding,
:escape_all_tags,
:format,
:include,
:languages,
:quiet,
:tags,
:untagged,
:validate
],
option_validation: Proc.new { |options|
if options[:languages] and options[:languages].length > 1
raise Twine::Error.new 'specify only a single language for the `generate-localization-file` command.'
end
},
example: 'twine generate-localization-file twine.txt ko.xml --tags FT'
},
'generate-all-localization-files' => {
description: 'Generates all the localization files necessary for a given project. The parent directory to all of the locale-specific directories in your project should be specified as the INPUT_OR_OUTPUT_PATH. This command will most often be executed by your build script so that each build always contains the most recent translations.',
arguments: [:twine_file, :output_path],
optional_options: [
:create_folders,
:developer_language,
:encoding,
:escape_all_tags,
:file_name,
:format,
:include,
:quiet,
:tags,
:untagged,
:validate
],
example: 'twine generate-all-localization-files twine.txt Resources/Locales/ --tags FT,FB'
},
'generate-localization-archive' => {
description: 'Generates a zip archive of localization files in a given format. The purpose of this command is to create a very simple archive that can be handed off to a translation team. The translation team can unzip the archive, translate all of the strings in the archived files, zip everything back up, and then hand that final archive back to be consumed by the consume-localization-archive command.',
arguments: [:twine_file, :output_path],
required_options: [
:format
],
optional_options: [
:developer_language,
:encoding,
:escape_all_tags,
:include,
:quiet,
:tags,
:untagged,
:validate
],
example: 'twine generate-localization-archive twine.txt LocDrop5.zip --tags FT,FB --format android --lang de,en,en-GB,ja,ko'
},
'consume-localization-file' => {
description: 'Slurps all of the translations from a localization file into the specified TWINE_FILE. If you have some files returned to you by your translators you can use this command to incorporate all of their changes. This script will attempt to guess both the language and the format given the filename and extension. For example, "ja.strings" will assume that the file is a Japanese iOS strings file.',
arguments: [:twine_file, :input_path],
optional_options: [
:consume_all,
:consume_comments,
:developer_language,
:encoding,
:format,
:languages,
:output_path,
:quiet,
:tags
],
option_validation: Proc.new { |options|
if options[:languages] and options[:languages].length > 1
raise Twine::Error.new 'specify only a single language for the `consume-localization-file` command.'
end
},
example: 'twine consume-localization-file twine.txt ja.strings'
},
'consume-all-localization-files' => {
description: 'Slurps all of the translations from a directory into the specified TWINE_FILE. If you have some files returned to you by your translators you can use this command to incorporate all of their changes. This script will attempt to guess both the language and the format given the filename and extension. For example, "ja.strings" will assume that the file is a Japanese iOS strings file.',
arguments: [:twine_file, :input_path],
optional_options: [
:consume_all,
:consume_comments,
:developer_language,
:encoding,
:format,
:output_path,
:quiet,
:tags
],
example: 'twine consume-all-localization-files twine.txt Resources/Locales/ --developer-language en --tags DefaultTag1,DefaultTag2'
},
'consume-localization-archive' => {
description: 'Consumes an archive of translated files. This archive should be in the same format as the one created by the generate-localization-archive command.',
arguments: [:twine_file, :input_path],
optional_options: [
:consume_all,
:consume_comments,
:developer_language,
:encoding,
:format,
:output_path,
:quiet,
:tags
],
example: 'twine consume-localization-archive twine.txt LocDrop5.zip'
},
'validate-twine-file' => {
description: 'Validates that the given Twine file is parseable, contains no duplicates, and that no key contains invalid characters. Exits with a non-zero exit code if those criteria are not met.',
arguments: [:twine_file],
optional_options: [
:developer_language,
:pedantic,
:quiet
],
example: 'twine validate-twine-file twine.txt'
}
}
DEPRECATED_COMMAND_MAPPINGS = {
'generate-loc-drop' => 'generate-localization-archive', # added on 17.01.2017 - version 0.10
'consume-loc-drop' => 'consume-localization-archive' # added on 17.01.2017 - version 0.10
}
def self.parse(args)
options = { include: :all }
command = args.select { |a| a[0] != '-' }[0]
args = args.reject { |a| a == command }
parser = OptionParser.new do |opts|
opts.banner = 'Usage: twine COMMAND STRINGS_FILE [INPUT_OR_OUTPUT_PATH] [--lang LANG1,LANG2...] [--tags TAG1,TAG2,TAG3...] [--format FORMAT]'
opts.separator ''
opts.separator 'The purpose of this script is to convert back and forth between multiple data formats, allowing us to treat our strings (and translations) as data stored in a text file. We can then use the data file to create drops for the localization team, consume similar drops returned by the localization team, and create formatted string files to ship with your products.'
opts.separator ''
opts.separator 'Commands:'
opts.separator ''
opts.separator '- generate-string-file'
opts.separator ' Generates a string file in a certain LANGUAGE given a particular FORMAT. This script will attempt to guess both the language and the format given the filename and extension. For example, "ko.xml" will generate a Korean language file for Android.'
opts.separator ''
opts.separator '- generate-all-string-files'
opts.separator ' Generates all the string files necessary for a given project. The parent directory to all of the locale-specific directories in your project should be specified as the INPUT_OR_OUTPUT_PATH. This command will most often be executed by your build script so that each build always contains the most recent strings.'
opts.separator ''
opts.separator '- consume-string-file'
opts.separator ' Slurps all of the strings from a translated strings file into the specified STRINGS_FILE. If you have some files returned to you by your translators you can use this command to incorporate all of their changes. This script will attempt to guess both the language and the format given the filename and extension. For example, "ja.strings" will assume that the file is a Japanese iOS strings file.'
opts.separator ''
opts.separator '- consume-all-string-files'
opts.separator ' Slurps all of the strings from a directory into the specified STRINGS_FILE. If you have some files returned to you by your translators you can use this command to incorporate all of their changes. This script will attempt to guess both the language and the format given the filename and extension. For example, "ja.strings" will assume that the file is a Japanese iOS strings file.'
opts.separator ''
opts.separator '- generate-loc-drop'
opts.separator ' Generates a zip archive of strings files in any format. The purpose of this command is to create a very simple archive that can be handed off to a translation team. The translation team can unzip the archive, translate all of the strings in the archived files, zip everything back up, and then hand that final archive back to be consumed by the consume-loc-drop command.'
opts.separator ''
opts.separator '- consume-loc-drop'
opts.separator ' Consumes an archive of translated files. This archive should be in the same format as the one created by the generate-loc-drop command.'
opts.separator ''
opts.separator '- validate-strings-file'
opts.separator ' Validates that the given strings file is parseable, contains no duplicates, and that every string has a tag. Exits with a non-zero exit code if those criteria are not met.'
opts.separator ''
opts.separator 'General Options:'
opts.separator ''
opts.on('-l', '--lang LANGUAGES', Array, 'The language code(s) to use for the specified action.') do |l|
options[:languages] = l
end
opts.on('-t', '--tags TAG1,TAG2,TAG3', Array, 'The tag(s) to use for the specified action. Only strings with that tag will be processed. Omit this option to match',
' all strings in the strings data file.') do |t|
options[:tags] = t
end
opts.on('-u', '--[no-]untagged', 'If you have specified tags using the --tags flag, then only those tags will be selected. If you also want to select',
' all strings that are untagged, then you can specify this option to do so.') do |u|
options[:untagged] = u
end
formats = Formatters.formatters.map(&:format_name).map(&:downcase)
opts.on('-f', '--format FORMAT', formats, "The file format to read or write: (#{formats.join(', ')}).",
" Additional formatters can be placed in the formats/ directory.") do |f|
options[:format] = f
end
opts.on('-a', '--[no-]consume-all', 'Normally, when consuming a string file, Twine will ignore any string keys that do not exist in your master file.') do |a|
options[:consume_all] = true
end
opts.on('-i', '--include SET', [:all, :translated, :untranslated],
"This flag will determine which strings are included when generating strings files. It's possible values:",
" all: All strings both translated and untranslated for the specified language are included. This is the default value.",
" translated: Only translated strings are included.",
" untranslated: Only untranslated strings are included.") do |i|
options[:include] = i
end
opts.on('-o', '--output-file OUTPUT_FILE', 'Write the new strings database to this file instead of replacing the original file. This flag is only useful when',
' running the consume-string-file or consume-loc-drop commands.') do |o|
options[:output_path] = o
end
opts.on('-n', '--file-name FILE_NAME', 'When running the generate-all-string-files command, this flag may be used to overwrite the default file name of the format.') do |n|
options[:file_name] = n
end
opts.on('-r', '--[no-]create-folders', "When running the generate-all-string-files command, this flag may be used to create output folders for all languages,",
" if they don't exist yet. As a result all languages will be exported, not only the ones where an output folder already",
" exists.") do |r|
options[:create_folders] = r
end
opts.on('-d', '--developer-language LANG', 'When writing the strings data file, set the specified language as the "developer language". In practice, this just',
' means that this language will appear first in the strings data file. When generating files this language will be',
' used as default language and its translations will be used if a key is not localized for the output language.') do |d|
options[:developer_language] = d
end
opts.on('-c', '--[no-]consume-comments', 'Normally, when consuming a string file, Twine will ignore all comments in the file. With this flag set, any comments',
' encountered will be read and parsed into the strings data file. This is especially useful when creating your first',
' strings data file from an existing project.') do |c|
options[:consume_comments] = c
end
opts.on('-e', '--encoding ENCODING', 'Twine defaults to encoding all output files in UTF-8. This flag will tell Twine to use an alternate encoding for these',
' files. For example, you could use this to write Apple .strings files in UTF-16. When reading files, Twine does its best',
" to determine the encoding automatically. However, if the files are UTF-16 without BOM, you need to specify if it's",
' UTF-16LE or UTF16-BE.') do |e|
options[:output_encoding] = e
end
opts.on('--[no-]validate', 'Validate the strings file before formatting it.') do |validate|
options[:validate] = validate
end
opts.on('-p', '--[no-]pedantic', 'When validating a strings file, perform additional checks that go beyond pure validity (like presence of tags).') do |p|
options[:pedantic] = p
end
opts.on('-h', '--help', 'Show this message.') do |h|
puts opts.help
exit
end
opts.on('--version', 'Print the version number and exit.') do
puts "Twine version #{Twine::VERSION}"
exit
end
opts.separator ''
opts.separator 'Examples:'
opts.separator ''
opts.separator '> twine generate-string-file strings.txt ko.xml --tags FT'
opts.separator '> twine generate-all-string-files strings.txt Resources/Locales/ --tags FT,FB'
opts.separator '> twine consume-string-file strings.txt ja.strings'
opts.separator '> twine consume-all-string-files strings.txt Resources/Locales/ --developer-language en --tags DefaultTag1,DefaultTag2'
opts.separator '> twine generate-loc-drop strings.txt LocDrop5.zip --tags FT,FB --format android --lang de,en,en-GB,ja,ko'
opts.separator '> twine consume-loc-drop strings.txt LocDrop5.zip'
opts.separator '> twine validate-strings-file strings.txt'
if args.any? { |a| a == '--version' }
Twine::stdout.puts "Twine version #{Twine::VERSION}"
return false
end
mapped_command = DEPRECATED_COMMAND_MAPPINGS[command]
if mapped_command
Twine::stdout.puts "WARNING: Twine commands names have changed. `#{command}` is now `#{mapped_command}`. The old command is deprecated and will soon stop working. For more information please check the documentation at https://github.com/mobiata/twine"
command = mapped_command
end
if command.nil?
print_help(args)
return false
elsif not COMMANDS.keys.include? command
raise Twine::Error.new "Invalid command: #{command}"
end
parse_command_options(command, args)
end
private
def self.print_help(args)
verbose = false
help_parser = OptionParser.new
help_parser.banner = 'Usage: twine [command] [options]'
help_parser.define('-h', '--help', 'Show this message.')
help_parser.define('--verbose', 'More detailed help.') { verbose = true }
help_parser.parse!(args)
Twine::stdout.puts help_parser.help
Twine::stdout.puts ''
Twine::stdout.puts 'Commands:'
COMMANDS.each do |name, properties|
if verbose
Twine::stdout.puts ''
Twine::stdout.puts ''
Twine::stdout.puts "# #{name}"
Twine::stdout.puts ''
Twine::stdout.puts properties[:description]
else
Twine::stdout.puts "- #{name}"
end
end
Twine::stdout.puts ''
Twine::stdout.puts 'type `twine [command] --help` for further information about a command.'
end
# source: https://www.safaribooksonline.com/library/view/ruby-cookbook/0596523696/ch01s15.html
def self.word_wrap(s, width)
s.gsub(/(.{1,#{width}})(\s+|\Z)/, "\\1\n").rstrip
end
def self.indent(string, first_line, following_lines)
lines = string.split("\n")
indentation = ' ' * following_lines
lines.map! { |line| indentation + line }
result = lines.join("\n").strip
' ' * first_line + result
end
# ensure the description forms a neat block on the right
def self.prepare_description!(options, summary_width)
lines = options[:description].split "\n"
# remove leadinge HEREDOC spaces
space_match = lines[0].match(/^\s+/)
if space_match
leading_spaces = space_match[0].length
lines.map! { |l| l[leading_spaces..-1] }
end
merged_lines = []
lines.each do |line|
# if the line is a continuation of the previous one
if not merged_lines.empty? and (line[0] != ' ' or line[0, 4] == ' ')
merged_lines[-1] += ' ' + line.strip
else
merged_lines << line.rstrip
end
end
if IO.console
console_width = IO.console.winsize[1]
else
console_width = 100
end
summary_width += 7 # account for description padding
max_description_width = console_width - summary_width
merged_lines.map! do |line|
if line[0] == ' '
line = word_wrap(line.strip, max_description_width - 2)
line = indent(line, 2, 4)
else
line = word_wrap(line, max_description_width)
end
line
end
options[:switch] << indent(merged_lines.join("\n"), 0, summary_width)
end
def self.parse_command_options(command_name, args)
command = COMMANDS[command_name]
result = {
command: command_name
}
parser = OptionParser.new
parser.banner = "Usage: twine #{command_name} #{command[:arguments].map { |c| "[#{c}]" }.join(' ')} [options]"
[:required_options, :optional_options].each do |option_type|
options = command[option_type]
if options and options.size > 0
parser.separator ''
parser.separator option_type.to_s.gsub('_', ' ').capitalize + ":"
options.each do |option_name|
option = OPTIONS[option_name]
result[option_name] = option[:default] if option[:default]
prepare_description!(option, parser.summary_width)
parser.define(*option[:switch]) do |value|
if option[:repeated]
result[option_name] = (result[option_name] || []) << value
else
result[option_name] = value
end
end
end
end
end
parser.define('-h', '--help', 'Show this message.') do
Twine::stdout.puts parser.help
return false
end
parser.separator ''
parser.separator 'Examples:'
parser.separator ''
parser.separator "> #{command[:example]}"
begin
parser.parse! args
rescue OptionParser::ParseError => e
Twine::stderr.puts e.message
exit false
raise Twine::Error.new e.message
end
if args.length == 0
puts parser.help
exit false
arguments = args.reject { |a| a[0] == '-' }
number_of_missing_arguments = command[:arguments].size - arguments.size
if number_of_missing_arguments > 0
missing_arguments = command[:arguments][-number_of_missing_arguments, number_of_missing_arguments]
raise Twine::Error.new "#{number_of_missing_arguments} missing argument#{number_of_missing_arguments > 1 ? "s" : ""}: #{missing_arguments.join(', ')}. Check `twine #{command_name} -h`"
end
number_of_needed_arguments = NEEDED_COMMAND_ARGUMENTS[args[0]]
unless number_of_needed_arguments
raise Twine::Error.new "Invalid command: #{args[0]}"
end
options[:command] = args[0]
if args.length < 2
raise Twine::Error.new 'You must specify your strings file.'
end
options[:strings_file] = args[1]
if args.length < number_of_needed_arguments
raise Twine::Error.new 'Not enough arguments.'
elsif args.length > number_of_needed_arguments
raise Twine::Error.new "Unknown argument: #{args[number_of_needed_arguments]}"
if args.length > command[:arguments].size
raise Twine::Error.new "Unknown argument: #{args[command[:arguments].size]}"
end
case options[:command]
when 'generate-string-file'
options[:output_path] = args[2]
if options[:languages] and options[:languages].length > 1
raise Twine::Error.new 'Please only specify a single language for the generate-string-file command.'
if command[:required_options]
command[:required_options].each do |option_name|
if result[option_name] == nil
raise Twine::Error.new "missing option: #{OPTIONS[option_name][:switch][0]}"
end
end
when 'generate-all-string-files'
options[:output_path] = args[2]
when 'consume-string-file'
options[:input_path] = args[2]
if options[:languages] and options[:languages].length > 1
raise Twine::Error.new 'Please only specify a single language for the consume-string-file command.'
end
when 'consume-all-string-files'
options[:input_path] = args[2]
when 'generate-loc-drop'
options[:output_path] = args[2]
if !options[:format]
raise Twine::Error.new 'You must specify a format.'
end
when 'consume-loc-drop'
options[:input_path] = args[2]
when 'validate-strings-file'
end
return options
command[:option_validation].call(result) if command[:option_validation]
command[:arguments].each do |argument_name|
result[argument_name] = args.shift
end
result
end
end
end

View file

@ -18,7 +18,3 @@ module Twine
end
end
end
Dir[File.join(File.dirname(__FILE__), 'formatters', '*.rb')].each do |file|
require file
end

View file

@ -3,11 +3,14 @@ require 'fileutils'
module Twine
module Formatters
class Abstract
attr_accessor :strings
SUPPORTS_PLURAL = false
LANGUAGE_CODE_WITH_OPTIONAL_REGION_CODE = "[a-z]{2}(?:-[A-Za-z]{2})?"
attr_accessor :twine_file
attr_accessor :options
def initialize
@strings = StringsFile.new
@twine_file = TwineFile.new
@options = {}
end
@ -20,7 +23,7 @@ module Twine
end
def can_handle_directory?(path)
raise NotImplementedError.new("You must implement can_handle_directory? in your formatter class.")
Dir.entries(path).any? { |item| /^.+#{Regexp.escape(extension)}$/.match(item) }
end
def default_file_name
@ -30,53 +33,58 @@ module Twine
def set_translation_for_key(key, lang, value)
value = value.gsub("\n", "\\n")
if @strings.strings_map.include?(key)
row = @strings.strings_map[key]
reference = @strings.strings_map[row.reference_key] if row.reference_key
if @twine_file.definitions_by_key.include?(key)
definition = @twine_file.definitions_by_key[key]
reference = @twine_file.definitions_by_key[definition.reference_key] if definition.reference_key
if !reference or value != reference.translations[lang]
row.translations[lang] = value
definition.translations[lang] = value
end
elsif @options[:consume_all]
Twine::stderr.puts "Adding new string '#{key}' to strings data file."
current_section = @strings.sections.find { |s| s.name == 'Uncategorized' }
Twine::stdout.puts "Adding new definition '#{key}' to twine file."
current_section = @twine_file.sections.find { |s| s.name == 'Uncategorized' }
unless current_section
current_section = StringsSection.new('Uncategorized')
@strings.sections.insert(0, current_section)
current_section = TwineSection.new('Uncategorized')
@twine_file.sections.insert(0, current_section)
end
current_row = StringsRow.new(key)
current_section.rows << current_row
current_definition = TwineDefinition.new(key)
current_section.definitions << current_definition
if @options[:tags] && @options[:tags].length > 0
current_row.tags = @options[:tags]
current_definition.tags = @options[:tags]
end
@strings.strings_map[key] = current_row
@strings.strings_map[key].translations[lang] = value
@twine_file.definitions_by_key[key] = current_definition
@twine_file.definitions_by_key[key].translations[lang] = value
else
Twine::stderr.puts "Warning: '#{key}' not found in strings data file."
Twine::stdout.puts "WARNING: '#{key}' not found in twine file."
end
if !@strings.language_codes.include?(lang)
@strings.add_language_code(lang)
if !@twine_file.language_codes.include?(lang)
@twine_file.add_language_code(lang)
end
end
def set_comment_for_key(key, comment)
return unless @options[:consume_comments]
if @strings.strings_map.include?(key)
row = @strings.strings_map[key]
if @twine_file.definitions_by_key.include?(key)
definition = @twine_file.definitions_by_key[key]
reference = @strings.strings_map[row.reference_key] if row.reference_key
reference = @twine_file.definitions_by_key[definition.reference_key] if definition.reference_key
if !reference or comment != reference.raw_comment
row.comment = comment
definition.comment = comment
end
end
end
def determine_language_given_path(path)
raise NotImplementedError.new("You must implement determine_language_given_path in your formatter class.")
only_language_and_region = /^#{LANGUAGE_CODE_WITH_OPTIONAL_REGION_CODE}$/i
basename = File.basename(path, File.extname(path))
return basename if basename =~ only_language_and_region
return basename if @twine_file.language_codes.include? basename
path.split(File::SEPARATOR).reverse.find { |segment| segment =~ only_language_and_region }
end
def output_path_for_language(lang)
@ -88,35 +96,35 @@ module Twine
end
def format_file(lang)
output_processor = Processors::OutputProcessor.new(@strings, @options)
processed_strings = output_processor.process(lang)
output_processor = Processors::OutputProcessor.new(@twine_file, @options)
processed_twine_file = output_processor.process(lang)
return nil if processed_strings.strings_map.empty?
return nil if processed_twine_file.definitions_by_key.empty?
header = format_header(lang)
result = ""
result += header + "\n" if header
result += format_sections(processed_strings, lang)
result += format_sections(processed_twine_file, lang)
end
def format_header(lang)
end
def format_sections(strings, lang)
sections = strings.sections.map { |section| format_section(section, lang) }
def format_sections(twine_file, lang)
sections = twine_file.sections.map { |section| format_section(section, lang) }
sections.compact.join("\n")
end
def format_section_header(section)
end
def should_include_row(row, lang)
row.translated_string_for_lang(lang)
def should_include_definition(definition, lang)
return !definition.translation_for_lang(lang).nil?
end
def format_section(section, lang)
rows = section.rows.select { |row| should_include_row(row, lang) }
return if rows.empty?
definitions = section.definitions.select { |definition| should_include_definition(definition, lang) }
return if definitions.empty?
result = ""
@ -125,28 +133,45 @@ module Twine
result += "\n#{section_header}" if section_header
end
rows.map! { |row| format_row(row, lang) }
rows.compact! # remove nil entries
rows.map! { |row| "\n#{row}" } # prepend newline
result += rows.join
definitions.map! { |definition| format_definition(definition, lang) }
definitions.compact! # remove nil definitions
definitions.map! { |definition| "\n#{definition}" } # prepend newline
result += definitions.join
end
def format_row(row, lang)
[format_comment(row, lang), format_key_value(row, lang)].compact.join
def format_definition(definition, lang)
formatted_definition = [format_comment(definition, lang)]
if self.class::SUPPORTS_PLURAL && definition.is_plural?
formatted_definition << format_plural(definition, lang)
else
formatted_definition << format_key_value(definition, lang)
end
formatted_definition.compact.join
end
def format_comment(row, lang)
def format_comment(definition, lang)
end
def format_key_value(row, lang)
value = row.translated_string_for_lang(lang)
key_value_pattern % { key: format_key(row.key.dup), value: format_value(value.dup) }
def format_key_value(definition, lang)
value = definition.translation_for_lang(lang)
key_value_pattern % { key: format_key(definition.key.dup), value: format_value(value.dup) }
end
def format_plural(definition, lang)
plural_hash = definition.plural_translation_for_lang(lang)
if plural_hash
format_plural_keys(definition.key.dup, plural_hash)
end
end
def key_value_pattern
raise NotImplementedError.new("You must implement key_value_pattern in your formatter class.")
end
def format_plural_keys(key, plural_hash)
raise NotImplementedError.new("You must implement format_plural_keys in your formatter class.")
end
def format_key(key)
key
end

View file

@ -7,14 +7,15 @@ module Twine
class Android < Abstract
include Twine::Placeholders
LANG_MAPPINGS = Hash[
'zh-rCN' => 'zh-Hans',
'zh-rHK' => 'zh-Hant',
'en-rGB' => 'en-UK',
SUPPORTS_PLURAL = true
LANG_CODES = Hash[
'zh' => 'zh-Hans',
'zh-CN' => 'zh-Hans',
'zh-HK' => 'zh-Hant',
# See https://developer.android.com/reference/java/util/Locale#legacy-language-codes
'iw' => 'he',
'in' => 'id',
'nb' => 'no'
# TODO: spanish
'ji' => 'yi'
]
def format_name
@ -30,32 +31,35 @@ module Twine
end
def default_file_name
return 'strings.xml'
'strings.xml'
end
def determine_language_given_path(path)
path_arr = path.split(File::SEPARATOR)
path_arr.each do |segment|
if segment == 'values'
return @strings.language_codes[0]
return @twine_file.language_codes[0]
else
# The language is defined by a two-letter ISO 639-1 language code, optionally followed by a two letter ISO 3166-1-alpha-2 region code (preceded by lowercase "r").
# see http://developer.android.com/guide/topics/resources/providing-resources.html#AlternativeResources
match = /^values-([a-z]{2}(-r[a-z]{2})?)$/i.match(segment)
if match
lang = match[1]
lang = LANG_MAPPINGS.fetch(lang, lang)
lang.sub!('-r', '-')
return lang
lang = match[1].sub('-r', '-')
return LANG_CODES.fetch(lang, lang)
end
end
end
return
return super
end
def output_path_for_language(lang)
"values-" + (LANG_MAPPINGS.key(lang) || lang)
if lang == @twine_file.language_codes[0]
"values"
else
"values-#{lang}".gsub(/-(\p{Lu})/, '-r\1')
end
end
def set_translation_for_key(key, lang, value)
@ -69,43 +73,33 @@ module Twine
end
def read(io, lang)
resources_regex = /<resources(?:[^>]*)>(.*)<\/resources>/m
key_regex = /<string name="(\w+)">/
comment_regex = /<!-- (.*) -->/
value_regex = /<string name="\w+">(.*)<\/string>/
key = nil
value = nil
document = REXML::Document.new io, :compress_whitespace => %w{ string }
document.context[:attribute_quote] = :quote
comment = nil
document.root.children.each do |child|
if child.is_a? REXML::Comment
content = child.string.strip
content.gsub!(/[\s]+/, ' ')
comment = content if content.length > 0 and not content.start_with?("SECTION:")
elsif child.is_a? REXML::Element
next unless child.name == 'string'
content_match = resources_regex.match(io.read)
if content_match
for line in content_match[1].split(/\r?\n/)
key_match = key_regex.match(line)
if key_match
key = key_match[1]
value_match = value_regex.match(line)
value = value_match ? value_match[1] : ""
set_translation_for_key(key, lang, value)
if comment and comment.length > 0 and !comment.start_with?("SECTION:")
set_comment_for_key(key, comment)
end
comment = nil
end
comment_match = comment_regex.match(line)
if comment_match
comment = comment_match[1]
end
end
key = child.attributes['name']
content = child.children.map(&:to_s).join
set_translation_for_key(key, lang, content)
set_comment_for_key(key, comment) if comment
comment = nil
end
end
end
def format_header(lang)
"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Android Strings File -->\n<!-- Generated by Twine #{Twine::VERSION} -->\n<!-- Language: #{lang} -->"
"<?xml version=\"1.0\" encoding=\"utf-8\"?>"
end
def format_sections(strings, lang)
def format_sections(twine_file, lang)
result = '<resources>'
result += super + "\n"
@ -114,30 +108,91 @@ module Twine
end
def format_section_header(section)
"\t<!-- SECTION: #{section.name} -->"
"#{space(4)}<!-- SECTION: #{section.name} -->"
end
def format_comment(row, lang)
"\t<!-- #{row.comment.gsub('--', '—')} -->\n" if row.comment
def format_comment(definition, lang)
"#{space(4)}<!-- #{definition.comment.gsub('--', '—')} -->\n" if definition.comment
end
def key_value_pattern
"\t<string name=\"%{key}\">%{value}</string>"
"#{space(4)}<string name=\"%{key}\">%{value}</string>"
end
def format_value(value)
# Android enforces the following rules on the values
# 1) apostrophes and quotes must be escaped with a backslash
value = escape_quotes(value)
value.gsub!("'", "\\\\'")
# 2) HTML escape the string
value = CGI.escapeHTML(value)
# 3) convert placeholders (e.g. %@ -> %s)
value = convert_placeholders_from_twine_to_android(value)
# 4) escape non resource identifier @ signs (http://developer.android.com/guide/topics/resources/accessing-resources.html#ResourcesFromXml)
def format_plural_keys(key, plural_hash)
result = "#{space(4)}<plurals name=\"#{key}\">\n"
result += plural_hash.map{|quantity,value| "#{space(8)}<item quantity=\"#{quantity}\">#{escape_value(value)}</item>"}.join("\n")
result += "\n#{space(4)}</plurals>"
end
def space(level)
' ' * level
end
def gsub_unless(text, pattern, replacement)
text.gsub(pattern) do |match|
match_start_position = Regexp.last_match.offset(0)[0]
yield(text[0, match_start_position]) ? match : replacement
end
end
# http://developer.android.com/guide/topics/resources/string-resource.html#FormattingAndStyling
def escape_value(value)
inside_cdata = /<\!\[CDATA\[((?!\]\]>).)*$/ # opening CDATA tag ('<![CDATA[') not followed by a closing tag (']]>')
inside_opening_tag = /<(a|font|span|p)\s?((?!>).)*$/ # tag start ('<a ', '<font ', '<span ' or '<p ') not followed by a '>'
# escape double and single quotes and & signs
value = gsub_unless(value, '"', '\\"') { |substring| substring =~ inside_cdata || substring =~ inside_opening_tag }
value = gsub_unless(value, "'", "\\'") { |substring| substring =~ inside_cdata }
value = gsub_unless(value, /&/, '&amp;') { |substring| substring =~ inside_cdata || substring =~ inside_opening_tag }
# if `value` contains a placeholder, escape all angle brackets
# if not, escape opening angle brackes unless it's a supported styling tag
# https://github.com/scelis/twine/issues/212
# https://stackoverflow.com/questions/3235131/#18199543
if number_of_twine_placeholders(value) > 0 or @options[:escape_all_tags]
# matches all `<` but <![CDATA
angle_bracket = /<(?!(\/?(\!\[CDATA)))/
else
# matches all '<' but <b>, <em>, <i>, <cite>, <dfn>, <big>, <small>, <font>, <tt>, <s>,
# <strike>, <del>, <u>, <super>, <sub>, <ul>, <li>, <br>, <div>, <span>, <p>, <a>
# and <![CDATA
angle_bracket = /<(?!(\/?(b|em|i|cite|dfn|big|small|font|tt|s|strike|del|u|super|sub|ul|li|br|div|span|p|a|\!\[CDATA)))/
end
value = gsub_unless(value, angle_bracket, '&lt;') { |substring| substring =~ inside_cdata }
value = gsub_unless(value, '\n', "\n\\n") { |substring| substring =~ inside_cdata }
# escape non resource identifier @ signs (http://developer.android.com/guide/topics/resources/accessing-resources.html#ResourcesFromXml)
resource_identifier_regex = /@(?!([a-z\.]+:)?[a-z+]+\/[a-zA-Z_]+)/ # @[<package_name>:]<resource_type>/<resource_name>
value.gsub!(resource_identifier_regex, '\@')
# 5) replace beginning and end spaces with \0020. Otherwise Android strips them.
value.gsub(resource_identifier_regex, '\@')
end
# see http://developer.android.com/guide/topics/resources/string-resource.html#FormattingAndStyling
# however unescaped HTML markup like in "Welcome to <b>Android</b>!" is stripped when retrieved with getString() (http://stackoverflow.com/questions/9891996/)
def format_value(value)
value = value.dup
# convert placeholders (e.g. %@ -> %s)
value = convert_placeholders_from_twine_to_android(value)
# capture xliff tags and replace them with a placeholder
xliff_tags = []
value.gsub! /<xliff:g.+?<\/xliff:g>/ do
xliff_tags << $&
'TWINE_XLIFF_TAG_PLACEHOLDER'
end
# escape everything outside xliff tags
value = escape_value(value)
# put xliff tags back into place
xliff_tags.each do |xliff_tag|
# escape content of xliff tags
xliff_tag.gsub! /(<xliff:g.*?>)(.*)(<\/xliff:g>)/ do "#{$1}#{escape_value($2)}#{$3}" end
value.sub! 'TWINE_XLIFF_TAG_PLACEHOLDER', xliff_tag
end
# replace beginning and end spaces with \u0020. Otherwise Android strips them.
value.gsub(/\A *| *\z/) { |spaces| '\u0020' * spaces.length }
end

View file

@ -1,6 +1,8 @@
module Twine
module Formatters
class Apple < Abstract
include Twine::Placeholders
def format_name
'apple'
end
@ -14,7 +16,7 @@ module Twine
end
def default_file_name
return 'Localizable.strings'
'Localizable.strings'
end
def determine_language_given_path(path)
@ -22,13 +24,15 @@ module Twine
path_arr.each do |segment|
match = /^(.+)\.lproj$/.match(segment)
if match
if match[1] != "Base"
if match[1] == "Base"
return @options[:developer_language]
else
return match[1]
end
end
end
return
return super
end
def output_path_for_language(lang)
@ -61,20 +65,21 @@ module Twine
end
end
def format_header(lang)
"/**\n * Apple Strings File\n * Generated by Twine #{Twine::VERSION}\n * Language: #{lang}\n */"
def format_file(lang)
result = super
result += "\n"
end
def format_section_header(section)
"/********** #{section.name} **********/\n"
"\n/********** #{section.name} **********/\n"
end
def key_value_pattern
"\"%{key}\" = \"%{value}\";\n"
"\"%{key}\" = \"%{value}\";"
end
def format_comment(row, lang)
"/* #{row.comment.gsub('*/', '* /')} */\n" if row.comment
def format_comment(definition, lang)
"\n/* #{definition.comment.gsub('*/', '* /')} */\n" if definition.comment
end
def format_key(key)
@ -82,8 +87,14 @@ module Twine
end
def format_value(value)
# Replace Android's %s with iOS %@
value = convert_placeholders_from_android_to_twine(value)
escape_quotes(value)
end
def should_include_definition(definition, lang)
return !definition.is_plural? && super
end
end
end
end

View file

@ -0,0 +1,72 @@
module Twine
module Formatters
class ApplePlural < Apple
include Twine::Placeholders
SUPPORTS_PLURAL = true
def format_name
'apple-plural'
end
def extension
'.stringsdict'
end
def default_file_name
'Localizable.stringsdict'
end
def format_footer(lang)
footer = "</dict>\n</plist>"
end
def format_file(lang)
result = super
result += format_footer(lang)
end
def format_header(lang)
header = "<\?xml version=\"1.0\" encoding=\"UTF-8\"\?>\n"
header += "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n"
header += "<plist version=\"1.0\">\n<dict>"
end
def format_section_header(section)
"<!-- ********** #{section.name} **********/ -->\n"
end
def format_plural_keys(key, plural_hash)
result = "\t<key>#{key}</key>\n"
result += "\t<dict>\n"
result += "\t\t<key>NSStringLocalizedFormatKey</key>\n"
result += "\t\t<string>\%\#@value@</string>\n"
result += "\t\t<key>value</key>\n"
result += "\t\t<dict>\n"
result += "\t\t\t<key>NSStringFormatSpecTypeKey</key>\n"
result += "\t\t\t<string>NSStringPluralRuleType</string>\n"
result += "\t\t\t<key>NSStringFormatValueTypeKey</key>\n"
result += "\t\t\t<string>d</string>\n"
# Replace Android's %s with iOS %@
result += plural_hash.map{|quantity,value| "\t\t\t<key>#{quantity}</key>\n\t\t\t<string>#{convert_placeholders_from_android_to_twine(value)}</string>"}.join("\n")
result += "\n"
result += "\t\t</dict>\n"
result += "\t</dict>\n"
end
def format_comment(definition, lang)
"<!-- #{definition.comment.gsub('--', '—')} -->\n" if definition.comment
end
def read(io, lang)
raise NotImplementedError.new("Reading \".stringdict\" files not implemented yet")
end
def should_include_definition(definition, lang)
return definition.is_plural? && definition.plural_translation_for_lang(lang)
end
end
end
end
Twine::Formatters.formatters << Twine::Formatters::ApplePlural.new

View file

@ -1,5 +1,6 @@
module Twine
module Formatters
# For a description of the .po file format, see https://www.gnu.org/software/gettext/manual/html_node/PO-Files.html
class Django < Abstract
def format_name
'django'
@ -9,32 +10,15 @@ module Twine
'.po'
end
def can_handle_directory?(path)
Dir.entries(path).any? { |item| /^.+\.po$/.match(item) }
end
def default_file_name
return 'strings.po'
end
def determine_language_given_path(path)
path_arr = path.split(File::SEPARATOR)
path_arr.each do |segment|
match = /(..)\.po$/.match(segment)
if match
return match[1]
end
end
return
'strings.po'
end
def read(io, lang)
comment_regex = /#\. *"?(.*)"?$/
key_regex = /msgid *"(.*)"$/
value_regex = /msgstr *"(.*)"$/m
comment_regex = /^\s*#\. *"?(.*)"?$/
key_regex = /^msgid *"(.*)"$/
value_regex = /^msgstr *"(.*)"$/m
last_comment = nil
while line = io.gets
comment_match = comment_regex.match(line)
if comment_match
@ -63,26 +47,27 @@ module Twine
end
def format_file(lang)
@default_lang = @strings.language_codes[0]
@default_lang = @twine_file.language_codes[0]
result = super
@default_lang = nil
result
end
def format_header(lang)
"##\n # Django Strings File\n # Generated by Twine #{Twine::VERSION}\n # Language: #{lang}\n"
# see https://www.gnu.org/software/trans-coord/manual/gnun/html_node/PO-Header.html for details
"# Django Strings File\n# Generated by Twine\n# Language: #{lang}\nmsgid \"\"\nmsgstr \"\"\n\"Content-Type: text/plain; charset=UTF-8\\n\""
end
def format_section_header(section)
"#--------- #{section.name} ---------#\n"
"# --------- #{section.name} --------- #\n"
end
def format_row(row, lang)
[format_comment(row, lang), format_base_translation(row), format_key_value(row, lang)].compact.join
def format_definition(definition, lang)
[format_comment(definition, lang), format_base_translation(definition), format_key_value(definition, lang)].compact.join
end
def format_base_translation(row)
base_translation = row.translations[@default_lang]
def format_base_translation(definition)
base_translation = definition.translations[@default_lang]
"# base translation: \"#{base_translation}\"\n" if base_translation
end
@ -91,8 +76,8 @@ module Twine
"msgstr \"%{value}\"\n"
end
def format_comment(row, lang)
"#. #{escape_quotes(row.comment)}\n" if row.comment
def format_comment(definition, lang)
"#. #{escape_quotes(definition.comment)}\n" if definition.comment
end
def format_key(key)

View file

@ -1,6 +1,8 @@
module Twine
module Formatters
class Flash < Abstract
include Twine::Placeholders
def format_name
'flash'
end
@ -9,16 +11,13 @@ module Twine
'.properties'
end
def can_handle_directory?(path)
return false
end
def default_file_name
return 'resources.properties'
'resources.properties'
end
def determine_language_given_path(path)
return
def set_translation_for_key(key, lang, value)
value = convert_placeholders_from_flash_to_twine(value)
super(key, lang, value)
end
def read(io, lang)
@ -28,36 +27,30 @@ module Twine
if match
key = match[1]
value = match[2].strip
value.gsub!(/\{[0-9]\}/, '%@')
set_translation_for_key(key, lang, value)
if last_comment
set_comment_for_key(key, last_comment)
end
set_comment_for_key(key, last_comment) if last_comment
end
match = /# *(.*)/.match(line)
if match
last_comment = match[1]
else
last_comment = nil
end
last_comment = match ? match[1] : nil
end
end
def format_sections(strings, lang)
def format_sections(twine_file, lang)
super + "\n"
end
def format_header(lang)
"## Flash Strings File\n## Generated by Twine #{Twine::VERSION}\n## Language: #{lang}"
"## Flash Strings File\n## Generated by Twine\n## Language: #{lang}"
end
def format_section_header(section)
"## #{section.name} ##\n"
end
def format_comment(row, lang)
"# #{row.comment}\n" if row.comment
def format_comment(definition, lang)
"# #{definition.comment}\n" if definition.comment
end
def key_value_pattern
@ -65,8 +58,7 @@ module Twine
end
def format_value(value)
placeHolderNumber = -1
value.gsub(/%[d@]/) { placeHolderNumber += 1; '{%d}' % placeHolderNumber }
convert_placeholders_from_twine_to_flash(value)
end
end
end

View file

@ -11,24 +11,8 @@ module Twine
'.po'
end
def can_handle_directory?(path)
Dir.entries(path).any? { |item| /^.+\.po$/.match(item) }
end
def default_file_name
return 'strings.po'
end
def determine_language_given_path(path)
path_arr = path.split(File::SEPARATOR)
path_arr.each do |segment|
match = /(..)\.po$/.match(segment)
if match
return match[1]
end
end
return
'strings.po'
end
def read(io, lang)
@ -64,43 +48,43 @@ module Twine
end
def format_file(lang)
@default_lang = strings.language_codes[0]
@default_lang = twine_file.language_codes[0]
result = super
@default_lang = nil
result
end
def format_header(lang)
"msgid \"\"\nmsgstr \"\"\n\"Language: #{lang}\\n\"\n\"X-Generator: Twine #{Twine::VERSION}\\n\"\n"
"msgid \"\"\nmsgstr \"\"\n\"Language: #{lang}\"\n\"X-Generator: Twine #{Twine::VERSION}\"\n"
end
def format_section_header(section)
"# SECTION: #{section.name}"
end
def should_include_row(row, lang)
super and row.translated_string_for_lang(@default_lang)
def should_include_definition(definition, lang)
super and !definition.translation_for_lang(@default_lang).nil?
end
def format_comment(row, lang)
"#. \"#{escape_quotes(row.comment)}\"\n" if row.comment
def format_comment(definition, lang)
"#. \"#{escape_quotes(definition.comment)}\"\n" if definition.comment
end
def format_key_value(row, lang)
value = row.translated_string_for_lang(lang)
[format_key(row.key.dup), format_base_translation(row), format_value(value.dup)].compact.join
def format_key_value(definition, lang)
value = definition.translation_for_lang(lang)
[format_key(definition.key.dup), format_base_translation(definition), format_value(value.dup)].compact.join
end
def format_key(key)
"msgctxt \"#{key}\"\n"
"msgctxt \"#{escape_quotes(key)}\"\n"
end
def format_base_translation(row)
"msgid \"#{row.translations[@default_lang]}\"\n"
def format_base_translation(definition)
"msgid \"#{escape_quotes(definition.translations[@default_lang])}\"\n"
end
def format_value(value)
"msgstr \"#{value}\"\n"
"msgstr \"#{escape_quotes(value)}\"\n"
end
end
end

View file

@ -9,31 +9,22 @@ module Twine
'.json'
end
def can_handle_directory?(path)
Dir.entries(path).any? { |item| /^.+\.json$/.match(item) }
end
def default_file_name
return 'localize.json'
'localize.json'
end
def determine_language_given_path(path)
path_arr = path.split(File::SEPARATOR)
path_arr.each do |segment|
match = /^((.+)-)?([^-]+)\.json$/.match(segment)
if match
return match[3]
end
end
match = /^.+([a-z]{2}-[A-Z]{2})\.json$/.match File.basename(path)
return match[1] if match
return
return super
end
def read(io, lang)
begin
require "json"
rescue LoadError
raise Twine::Error.new "You must run 'gem install json' in order to read or write jquery-localize files."
raise Twine::Error.new "You must run `gem install json` in order to read or write jquery-localize files."
end
json = JSON.load(io)
@ -48,8 +39,9 @@ module Twine
"{\n#{super}\n}\n"
end
def format_sections(strings, lang)
sections = strings.sections.map { |section| format_section(section, lang) }
def format_sections(twine_file, lang)
sections = twine_file.sections.map { |section| format_section(section, lang) }
sections.delete_if(&:empty?)
sections.join(",\n\n")
end
@ -57,11 +49,11 @@ module Twine
end
def format_section(section, lang)
rows = section.rows.dup
definitions = section.definitions.dup
rows.map! { |row| format_row(row, lang) }
rows.compact! # remove nil entries
rows.join(",\n")
definitions.map! { |definition| format_definition(definition, lang) }
definitions.compact! # remove nil definitions
definitions.join(",\n")
end
def key_value_pattern

View file

@ -33,7 +33,7 @@ module Twine
end
def default_file_name
return 'strings.xml'
'strings.xml'
end
def determine_language_given_path(path)
@ -91,10 +91,10 @@ module Twine
end
def format_header(lang)
"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Tizen Strings File -->\n<!-- Generated by Twine #{Twine::VERSION} -->\n<!-- Language: #{lang} -->"
"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Tizen Strings File -->\n<!-- Generated by Twine -->\n<!-- Language: #{lang} -->"
end
def format_sections(strings, lang)
def format_sections(twine_file, lang)
result = '<string_table Bversion="2.0.0.201311071819" Dversion="20120315">'
result += super + "\n"
@ -106,8 +106,8 @@ module Twine
"\t<!-- SECTION: #{section.name} -->"
end
def format_comment(row, lang)
"\t<!-- #{row.comment.gsub('--', '—')} -->\n" if row.comment
def format_comment(definition, lang)
"\t<!-- #{definition.comment.gsub('--', '—')} -->\n" if definition.comment
end
def key_value_pattern

View file

@ -2,48 +2,62 @@ module Twine
module Processors
class OutputProcessor
def initialize(strings, options)
@strings = strings
def initialize(twine_file, options)
@twine_file = twine_file
@options = options
end
def default_language
@options[:developer_language] || @strings.language_codes[0]
@options[:developer_language] || @twine_file.language_codes[0]
end
def fallback_languages(language)
fallback_mapping = {
'zh-CN' => 'zh-Hans', # if we don't have a zh-CN translation, try zh-Hans before en
'zh-TW' => 'zh-Hant' # if we don't have a zh-TW translation, try zh-Hant before en
}
[fallback_mapping[language], default_language].flatten.compact
# Regional dialect fallbacks to generic language (for example: 'es-MX' to 'es' instead of default 'en').
if language.match(/([a-zA-Z]{2})-[a-zA-Z]+/)
generic_language = language.gsub(/([a-zA-Z])-[a-zA-Z]+/, '\1')
end
[fallback_mapping[language], generic_language, default_language].flatten.compact
end
def process(language)
result = StringsFile.new
result = TwineFile.new
result.language_codes.concat @strings.language_codes
@strings.sections.each do |section|
new_section = StringsSection.new section.name
result.language_codes.concat @twine_file.language_codes
@twine_file.sections.each do |section|
new_section = TwineSection.new section.name
section.rows.each do |row|
next unless row.matches_tags?(@options[:tags], @options[:untagged])
section.definitions.each do |definition|
next unless definition.matches_tags?(@options[:tags], @options[:untagged])
value = row.translated_string_for_lang(language)
value = definition.translation_for_lang(language)
next if value && @options[:include] == :untranslated
if value.nil? && @options[:include] != :translated
value = row.translated_string_for_lang(fallback_languages(language))
value = definition.translation_for_lang(fallback_languages(language))
end
next unless value
new_row = row.dup
new_row.translations[language] = value
new_definition = definition.dup
new_definition.translations[language] = value
new_section.rows << new_row
result.strings_map[new_row.key] = new_row
if definition.is_plural?
# If definition is plural, but no translation found -> create
# Then check 'other' key
if !(new_definition.plural_translations[language] ||= {}).key? 'other'
new_definition.plural_translations[language]['other'] = value
end
end
new_section.definitions << new_definition
result.definitions_by_key[new_definition.key] = new_definition
end
result.sections << new_section

View file

@ -2,34 +2,42 @@ module Twine
module Placeholders
extend self
PLACEHOLDER_FLAGS_WIDTH_PRECISION_LENGTH = '([-+ 0#])?(\d+|\*)?(\.(\d+|\*))?(hh?|ll?|L|z|j|t)?'
# Note: the ` ` (single space) flag is NOT supported
PLACEHOLDER_FLAGS_WIDTH_PRECISION_LENGTH = '([-+0#])?(\d+|\*)?(\.(\d+|\*))?(hh?|ll?|L|z|j|t|q)?'
PLACEHOLDER_PARAMETER_FLAGS_WIDTH_PRECISION_LENGTH = '(\d+\$)?' + PLACEHOLDER_FLAGS_WIDTH_PRECISION_LENGTH
PLACEHOLDER_TYPES = '[diufFeEgGxXoscpaA]'
PLACEHOLDER_REGEX = /%#{PLACEHOLDER_PARAMETER_FLAGS_WIDTH_PRECISION_LENGTH}#{PLACEHOLDER_TYPES}/
def number_of_twine_placeholders(input)
input.scan(PLACEHOLDER_REGEX).size
end
def convert_twine_string_placeholder(input)
# %@ -> %s
input.gsub(/(%#{PLACEHOLDER_PARAMETER_FLAGS_WIDTH_PRECISION_LENGTH})@/, '\1s')
end
# http://developer.android.com/guide/topics/resources/string-resource.html#FormattingAndStyling
# http://stackoverflow.com/questions/4414389/android-xml-percent-symbol
# https://github.com/mobiata/twine/pull/106
def convert_placeholders_from_twine_to_android(input)
placeholder_types = '[diufFeEgGxXoscpaA]'
# %@ -> %s
value = input.gsub(/(%#{PLACEHOLDER_PARAMETER_FLAGS_WIDTH_PRECISION_LENGTH})@/, '\1s')
value = convert_twine_string_placeholder(input)
placeholder_syntax = PLACEHOLDER_PARAMETER_FLAGS_WIDTH_PRECISION_LENGTH + placeholder_types
placeholder_regex = /%#{placeholder_syntax}/
number_of_placeholders = value.scan(placeholder_regex).size
number_of_placeholders = number_of_twine_placeholders(value)
return value if number_of_placeholders == 0
# got placeholders -> need to double single percent signs
# % -> %% (but %% -> %%, %d -> %d)
placeholder_syntax = PLACEHOLDER_PARAMETER_FLAGS_WIDTH_PRECISION_LENGTH + PLACEHOLDER_TYPES
single_percent_regex = /([^%])(%)(?!(%|#{placeholder_syntax}))/
value.gsub! single_percent_regex, '\1%%'
return value if number_of_placeholders < 2
# number placeholders
non_numbered_placeholder_regex = /%(#{PLACEHOLDER_FLAGS_WIDTH_PRECISION_LENGTH}#{placeholder_types})/
non_numbered_placeholder_regex = /%(#{PLACEHOLDER_FLAGS_WIDTH_PRECISION_LENGTH}#{PLACEHOLDER_TYPES})/
number_of_non_numbered_placeholders = value.scan(non_numbered_placeholder_regex).size
@ -50,5 +58,25 @@ module Twine
# %s -> %@
input.gsub(placeholder_regex, '\1@')
end
# http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/mx/resources/IResourceManager.html#getString()
# http://soenkerohde.com/2008/07/flex-localization/comment-page-1/
def convert_placeholders_from_twine_to_flash(input)
value = convert_twine_string_placeholder(input)
value.gsub(PLACEHOLDER_REGEX).each_with_index do |match, index|
"{#{index}}"
end
end
def convert_placeholders_from_flash_to_twine(input)
input.gsub /\{\d+\}/, '%@'
end
# Python supports placeholders in the form of `%(amount)03d`
# see https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting
def contains_python_specific_placeholder(input)
/%\([a-zA-Z0-9_-]+\)#{PLACEHOLDER_PARAMETER_FLAGS_WIDTH_PRECISION_LENGTH}#{PLACEHOLDER_TYPES}/.match(input) != nil
end
end
end

View file

@ -1,3 +1,4 @@
require 'date'
require 'safe_yaml/load'
SafeYAML::OPTIONS[:suppress_warnings] = true
@ -56,7 +57,7 @@ module Twine
end
def join_path *paths
File.expand_path File.join *paths
File.expand_path File.join(*paths)
end
end
end

View file

@ -5,45 +5,58 @@ Twine::Plugin.new # Initialize plugins first in Runner.
module Twine
class Runner
class NullOutput
def puts(message)
end
def string
""
end
end
def self.run(args)
options = CLI.parse(args)
return unless options
strings = StringsFile.new
strings.read options[:strings_file]
runner = new(options, strings)
twine_file = TwineFile.new
twine_file.read options[:twine_file]
runner = new(options, twine_file)
case options[:command]
when 'generate-string-file'
runner.generate_string_file
when 'generate-all-string-files'
runner.generate_all_string_files
when 'consume-string-file'
runner.consume_string_file
when 'consume-all-string-files'
runner.consume_all_string_files
when 'generate-loc-drop'
runner.generate_loc_drop
when 'consume-loc-drop'
runner.consume_loc_drop
when 'validate-strings-file'
runner.validate_strings_file
when 'generate-localization-file'
runner.generate_localization_file
when 'generate-all-localization-files'
runner.generate_all_localization_files
when 'consume-localization-file'
runner.consume_localization_file
when 'consume-all-localization-files'
runner.consume_all_localization_files
when 'generate-localization-archive'
runner.generate_localization_archive
when 'consume-localization-archive'
runner.consume_localization_archive
when 'validate-twine-file'
runner.validate_twine_file
end
end
def initialize(options = {}, strings = StringsFile.new)
def initialize(options = {}, twine_file = TwineFile.new)
@options = options
@strings = strings
end
def write_strings_data(path)
if @options[:developer_language]
@strings.set_developer_language_code(@options[:developer_language])
@twine_file = twine_file
if @options[:quite]
Twine::stdout = NullOutput.new
end
@strings.write(path)
end
def generate_string_file
validate_strings_file if @options[:validate]
def write_twine_data(path)
if @options[:developer_language]
@twine_file.set_developer_language_code(@options[:developer_language])
end
@twine_file.write(path)
end
def generate_localization_file
validate_twine_file if @options[:validate]
lang = nil
lang = @options[:languages][0] if @options[:languages]
@ -51,13 +64,13 @@ module Twine
formatter, lang = prepare_read_write(@options[:output_path], lang)
output = formatter.format_file(lang)
raise Twine::Error.new "Nothing to generate! The resulting file would not contain any strings." unless output
raise Twine::Error.new "Nothing to generate! The resulting file would not contain any translations." unless output
IO.write(@options[:output_path], output, encoding: encoding)
IO.write(@options[:output_path], output, encoding: output_encoding)
end
def generate_all_string_files
validate_strings_file if @options[:validate]
def generate_all_localization_files
validate_twine_file if @options[:validate]
if !File.directory?(@options[:output_path])
if @options[:create_folders]
@ -67,16 +80,19 @@ module Twine
end
end
formatter_for_directory = find_formatter { |f| f.can_handle_directory?(@options[:output_path]) }
formatter = formatter_for_format(@options[:format]) || formatter_for_directory
if @options[:format]
formatter = formatter_for_format(@options[:format])
else
formatter = find_formatter { |f| f.can_handle_directory?(@options[:output_path]) }
end
unless formatter
raise Twine::Error.new "Could not determine format given the contents of #{@options[:output_path]}"
raise Twine::Error.new "Could not determine format given the contents of #{@options[:output_path]}. Try using `--format`."
end
file_name = @options[:file_name] || formatter.default_file_name
if @options[:create_folders]
@strings.language_codes.each do |lang|
@twine_file.language_codes.each do |lang|
output_path = File.join(@options[:output_path], formatter.output_path_for_language(lang))
FileUtils.mkdir_p(output_path)
@ -85,11 +101,11 @@ module Twine
output = formatter.format_file(lang)
unless output
Twine::stderr.puts "Skipping file at path #{file_path} since it would not contain any strings."
Twine::stdout.puts "Skipping file at path #{file_path} since it would not contain any translations."
next
end
IO.write(file_path, output, encoding: encoding)
IO.write(file_path, output, encoding: output_encoding)
end
else
language_found = false
@ -107,11 +123,11 @@ module Twine
file_path = File.join(output_path, file_name)
output = formatter.format_file(lang)
unless output
Twine::stderr.puts "Skipping file at path #{file_path} since it would not contain any strings."
Twine::stdout.puts "Skipping file at path #{file_path} since it would not contain any translations."
next
end
IO.write(file_path, output, encoding: encoding)
IO.write(file_path, output, encoding: output_encoding)
end
unless language_found
@ -121,38 +137,8 @@ module Twine
end
def consume_string_file
lang = nil
if @options[:languages]
lang = @options[:languages][0]
end
read_string_file(@options[:input_path], lang)
output_path = @options[:output_path] || @options[:strings_file]
write_strings_data(output_path)
end
def consume_all_string_files
if !File.directory?(@options[:input_path])
raise Twine::Error.new("Directory does not exist: #{@options[:output_path]}")
end
Dir.glob(File.join(@options[:input_path], "**/*")) do |item|
if File.file?(item)
begin
read_string_file(item)
rescue Twine::Error => e
Twine::stderr.puts "#{e.message}"
end
end
end
output_path = @options[:output_path] || @options[:strings_file]
write_strings_data(output_path)
end
def generate_loc_drop
validate_strings_file if @options[:validate]
def generate_localization_archive
validate_twine_file if @options[:validate]
require_rubyzip
@ -165,7 +151,7 @@ module Twine
zipfile.mkdir('Locales')
formatter = formatter_for_format(@options[:format])
@strings.language_codes.each do |lang|
@twine_file.language_codes.each do |lang|
if @options[:languages] == nil || @options[:languages].length == 0 || @options[:languages].include?(lang)
file_name = lang + formatter.extension
temp_path = File.join(temp_dir, file_name)
@ -173,11 +159,11 @@ module Twine
output = formatter.format_file(lang)
unless output
Twine::stderr.puts "Skipping file #{file_name} since it would not contain any strings."
Twine::stdout.puts "Skipping file #{file_name} since it would not contain any translations."
next
end
IO.write(temp_path, output, encoding: encoding)
IO.write(temp_path, output, encoding: output_encoding)
zipfile.add(zip_path, temp_path)
end
end
@ -185,13 +171,44 @@ module Twine
end
end
def consume_loc_drop
def consume_localization_file
lang = nil
if @options[:languages]
lang = @options[:languages][0]
end
read_localization_file(@options[:input_path], lang)
output_path = @options[:output_path] || @options[:twine_file]
write_twine_data(output_path)
end
def consume_all_localization_files
if !File.directory?(@options[:input_path])
raise Twine::Error.new("Directory does not exist: #{@options[:input_path]}")
end
Dir.glob(File.join(@options[:input_path], "**/*")) do |item|
if File.file?(item)
begin
read_localization_file(item)
rescue Twine::Error => e
Twine::stderr.puts "#{e.message}"
end
end
end
output_path = @options[:output_path] || @options[:twine_file]
write_twine_data(output_path)
end
def consume_localization_archive
require_rubyzip
if !File.file?(@options[:input_path])
raise Twine::Error.new("File does not exist: #{@options[:input_path]}")
end
error_encountered = false
Dir.mktmpdir do |temp_dir|
Zip::File.open(@options[:input_path]) do |zipfile|
zipfile.each do |entry|
@ -201,36 +218,44 @@ module Twine
FileUtils.mkdir_p(File.dirname(real_path))
zipfile.extract(entry.name, real_path)
begin
read_string_file(real_path)
read_localization_file(real_path)
rescue Twine::Error => e
Twine::stderr.puts "#{e.message}"
error_encountered = true
end
end
end
end
output_path = @options[:output_path] || @options[:strings_file]
write_strings_data(output_path)
output_path = @options[:output_path] || @options[:twine_file]
write_twine_data(output_path)
if error_encountered
raise Twine::Error.new("At least one file could not be consumed")
end
end
def validate_strings_file
total_strings = 0
def validate_twine_file
total_definitions = 0
all_keys = Set.new
duplicate_keys = Set.new
keys_without_tags = Set.new
invalid_keys = Set.new
keys_with_python_only_placeholders = Set.new
valid_key_regex = /^[A-Za-z0-9_]+$/
@strings.sections.each do |section|
section.rows.each do |row|
total_strings += 1
@twine_file.sections.each do |section|
section.definitions.each do |definition|
total_definitions += 1
duplicate_keys.add(row.key) if all_keys.include? row.key
all_keys.add(row.key)
duplicate_keys.add(definition.key) if all_keys.include? definition.key
all_keys.add(definition.key)
keys_without_tags.add(row.key) if row.tags == nil or row.tags.length == 0
keys_without_tags.add(definition.key) if definition.tags == nil or definition.tags.length == 0
invalid_keys << row.key unless row.key =~ valid_key_regex
invalid_keys << definition.key unless definition.key =~ valid_key_regex
keys_with_python_only_placeholders << definition.key if definition.translations.values.any? { |v| Placeholders.contains_python_specific_placeholder(v) }
end
end
@ -238,14 +263,14 @@ module Twine
join_keys = lambda { |set| set.map { |k| " " + k }.join("\n") }
unless duplicate_keys.empty?
errors << "Found duplicate string key(s):\n#{join_keys.call(duplicate_keys)}"
errors << "Found duplicate key(s):\n#{join_keys.call(duplicate_keys)}"
end
if @options[:pedantic]
if keys_without_tags.length == total_strings
errors << "None of your strings have tags."
if keys_without_tags.length == total_definitions
errors << "None of your definitions have tags."
elsif keys_without_tags.length > 0
errors << "Found strings without tags:\n#{join_keys.call(keys_without_tags)}"
errors << "Found definitions without tags:\n#{join_keys.call(keys_without_tags)}"
end
end
@ -253,71 +278,78 @@ module Twine
errors << "Found key(s) with invalid characters:\n#{join_keys.call(invalid_keys)}"
end
unless keys_with_python_only_placeholders.empty?
errors << "Found key(s) with placeholders that are only supported by Python:\n#{join_keys.call(keys_with_python_only_placeholders)}"
end
raise Twine::Error.new errors.join("\n\n") unless errors.empty?
Twine::stdout.puts "#{@options[:strings_file]} is valid."
Twine::stdout.puts "#{@options[:twine_file]} is valid."
end
private
def encoding
@options[:output_encoding] || 'UTF-8'
def output_encoding
@options[:encoding] || 'UTF-8'
end
def require_rubyzip
begin
require 'zip'
rescue LoadError
raise Twine::Error.new "You must run 'gem install rubyzip' in order to create or consume localization drops."
raise Twine::Error.new "You must run 'gem install rubyzip' in order to create or consume localization archives."
end
end
def determine_language_given_path(path)
code = File.basename(path, File.extname(path))
return code if @strings.language_codes.include? code
end
def formatter_for_format(format)
find_formatter { |f| f.format_name == format }
end
def find_formatter(&block)
formatter = Formatters.formatters.find &block
return nil unless formatter
formatter.strings = @strings
formatters = Formatters.formatters.select(&block)
if formatters.empty?
return nil
elsif formatters.size > 1
raise Twine::Error.new("Unable to determine format. Candidates are: #{formatters.map(&:format_name).join(', ')}. Please specify the format you want using `--format`")
end
formatter = formatters.first
formatter.twine_file = @twine_file
formatter.options = @options
formatter
end
def read_string_file(path, lang = nil)
def read_localization_file(path, lang = nil)
unless File.file?(path)
raise Twine::Error.new("File does not exist: #{path}")
end
formatter, lang = prepare_read_write(path, lang)
encoding = @options[:encoding] || Twine::Encoding.encoding_for_path(path)
external_encoding = @options[:encoding] || Twine::Encoding.encoding_for_path(path)
IO.open(IO.sysopen(path, 'rb'), 'rb', external_encoding: encoding, internal_encoding: 'UTF-8') do |io|
IO.open(IO.sysopen(path, 'rb'), 'rb', external_encoding: external_encoding, internal_encoding: 'UTF-8') do |io|
io.read(2) if Twine::Encoding.has_bom?(path)
formatter.read(io, lang)
end
end
def prepare_read_write(path, lang)
formatter_for_path = find_formatter { |f| f.extension == File.extname(path) }
formatter = formatter_for_format(@options[:format]) || formatter_for_path
if @options[:format]
formatter = formatter_for_format(@options[:format])
else
formatter = find_formatter { |f| f.extension == File.extname(path) }
end
unless formatter
raise Twine::Error.new "Unable to determine format of #{path}"
raise Twine::Error.new "Unable to determine format of #{path}. Try using `--format`."
end
lang = lang || determine_language_given_path(path) || formatter.determine_language_given_path(path)
lang = lang || formatter.determine_language_given_path(path)
unless lang
raise Twine::Error.new "Unable to determine language for #{path}"
raise Twine::Error.new "Unable to determine language for #{path}. Try using `--lang`."
end
@strings.language_codes << lang unless @strings.language_codes.include? lang
@twine_file.language_codes << lang unless @twine_file.language_codes.include? lang
return formatter, lang
end

View file

@ -1,19 +1,13 @@
module Twine
class StringsSection
attr_reader :name
attr_reader :rows
class TwineDefinition
PLURAL_KEYS = %w(zero one two few many other)
def initialize(name)
@name = name
@rows = []
end
end
class StringsRow
attr_reader :key
attr_accessor :comment
attr_accessor :tags
attr_reader :translations
attr_reader :plural_translations
attr_reader :is_plural
attr_accessor :reference
attr_accessor :reference_key
@ -22,6 +16,7 @@ module Twine
@comment = nil
@tags = nil
@translations = {}
@plural_translations = {}
end
def comment
@ -32,34 +27,59 @@ module Twine
@comment
end
# [['tag1', 'tag2'], ['~tag3']] == (tag1 OR tag2) AND (!tag3)
def matches_tags?(tags, include_untagged)
if tags == nil || tags.empty?
# The user did not specify any tags. Everything passes.
if tags == nil || tags.empty? # The user did not specify any tags. Everything passes.
return true
elsif @tags == nil
# This row has no tags.
elsif @tags == nil # This definition has no tags -> check reference (if any)
return reference ? reference.matches_tags?(tags, include_untagged) : include_untagged
elsif @tags.empty?
return include_untagged
else
return !(tags & @tags).empty?
return tags.all? do |set|
regular_tags, negated_tags = set.partition { |tag| tag[0] != '~' }
negated_tags.map! { |tag| tag[1..-1] }
matches_regular_tags = (!regular_tags.empty? && !(regular_tags & @tags).empty?)
matches_negated_tags = (!negated_tags.empty? && (negated_tags & @tags).empty?)
matches_regular_tags or matches_negated_tags
end
end
return false
end
def translated_string_for_lang(lang)
translation = [lang].flatten.map { |l| @translations[l] }.first
def translation_for_lang(lang)
translation = [lang].flatten.map { |l| @translations[l] }.compact.first
translation = reference.translated_string_for_lang(lang) if translation.nil? && reference
translation = reference.translation_for_lang(lang) if translation.nil? && reference
return translation
end
def plural_translation_for_lang(lang)
if @plural_translations.has_key? lang
@plural_translations[lang].dup.sort_by { |key,_| TwineDefinition::PLURAL_KEYS.index(key) }.to_h
end
end
def is_plural?
!@plural_translations.empty?
end
end
class StringsFile
class TwineSection
attr_reader :name
attr_reader :definitions
def initialize(name)
@name = name
@definitions = []
end
end
class TwineFile
attr_reader :sections
attr_reader :strings_map
attr_reader :definitions_by_key
attr_reader :language_codes
private
@ -73,7 +93,7 @@ module Twine
def initialize
@sections = []
@strings_map = {}
@definitions_by_key = {}
@language_codes = []
end
@ -102,7 +122,7 @@ module Twine
File.open(path, 'r:UTF-8') do |f|
line_num = 0
current_section = nil
current_row = nil
current_definition = nil
while line = f.gets
parsed = false
line.strip!
@ -115,42 +135,54 @@ module Twine
if line.length > 4 && line[0, 2] == '[['
match = /^\[\[(.+)\]\]$/.match(line)
if match
current_section = StringsSection.new(match[1])
current_section = TwineSection.new(match[1])
@sections << current_section
parsed = true
end
elsif line.length > 2 && line[0, 1] == '['
key = match_key(line)
if key
current_row = StringsRow.new(key)
@strings_map[current_row.key] = current_row
current_definition = TwineDefinition.new(key)
@definitions_by_key[current_definition.key] = current_definition
if !current_section
current_section = StringsSection.new('')
current_section = TwineSection.new('')
@sections << current_section
end
current_section.rows << current_row
current_section.definitions << current_definition
parsed = true
end
else
match = /^([^=]+)=(.*)$/.match(line)
match = /^([^:=]+)(?::([^=]+))?=(.*)$/.match(line)
if match
key = match[1].strip
value = match[2].strip
plural_key = match[2].to_s.strip
value = match[3].strip
value = value[1..-2] if value[0] == '`' && value[-1] == '`'
case key
when 'comment'
current_row.comment = value
current_definition.comment = value
when 'tags'
current_row.tags = value.split(',')
current_definition.tags = value.split(',')
when 'ref'
current_row.reference_key = value if value
current_definition.reference_key = value if value
else
if !@language_codes.include? key
add_language_code(key)
end
current_row.translations[key] = value
# Providing backward compatibility
# for formatters without plural support
if plural_key.empty? || plural_key == 'other'
current_definition.translations[key] = value
end
if !plural_key.empty?
if !TwineDefinition::PLURAL_KEYS.include? plural_key
warn("Unknown plural key #{plural_key}")
next
end
(current_definition.plural_translations[key] ||= {})[plural_key] = value
end
end
parsed = true
end
@ -163,9 +195,9 @@ module Twine
end
# resolve_references
@strings_map.each do |key, row|
next unless row.reference_key
row.reference = @strings_map[row.reference_key]
@definitions_by_key.each do |key, definition|
next unless definition.reference_key
definition.reference = @definitions_by_key[definition.reference_key]
end
end
@ -180,26 +212,26 @@ module Twine
f.puts "[[#{section.name}]]"
section.rows.each do |row|
f.puts "\t[#{row.key}]"
section.definitions.each do |definition|
f.puts "\t[#{definition.key}]"
value = write_value(row, dev_lang, f)
if !value && !row.reference_key
puts "Warning: #{row.key} does not exist in developer language '#{dev_lang}'"
value = write_value(definition, dev_lang, f)
if !value && !definition.reference_key
Twine::stdout.puts "WARNING: #{definition.key} does not exist in developer language '#{dev_lang}'"
end
if row.reference_key
f.puts "\t\tref = #{row.reference_key}"
if definition.reference_key
f.puts "\t\tref = #{definition.reference_key}"
end
if row.tags && row.tags.length > 0
tag_str = row.tags.join(',')
if definition.tags && definition.tags.length > 0
tag_str = definition.tags.join(',')
f.puts "\t\ttags = #{tag_str}"
end
if row.raw_comment and row.raw_comment.length > 0
f.puts "\t\tcomment = #{row.raw_comment}"
if definition.raw_comment and definition.raw_comment.length > 0
f.puts "\t\tcomment = #{definition.raw_comment}"
end
@language_codes[1..-1].each do |lang|
write_value(row, lang, f)
write_value(definition, lang, f)
end
end
end
@ -208,8 +240,8 @@ module Twine
private
def write_value(row, language, file)
value = row.translations[language]
def write_value(definition, language, file)
value = definition.translations[language]
return nil unless value
if value[0] == ' ' || value[-1] == ' ' || (value[0] == '`' && value[-1] == '`')

View file

@ -1,3 +1,3 @@
module Twine
VERSION = '0.9.0'
VERSION = '1.1.2-om'
end

14
test/command_test.rb Normal file
View file

@ -0,0 +1,14 @@
require 'twine_test'
class CommandTest < TwineTest
def prepare_mock_formatter(formatter_class, clear_other_formatters = true)
twine_file = Twine::TwineFile.new
twine_file.language_codes.concat KNOWN_LANGUAGES
formatter = formatter_class.new
formatter.twine_file = twine_file
Twine::Formatters.formatters.clear if clear_other_formatters
Twine::Formatters.formatters << formatter
formatter
end
end

View file

@ -1,14 +0,0 @@
require 'twine_test_case'
class CommandTestCase < TwineTestCase
def prepare_mock_formatter(formatter_class)
strings = Twine::StringsFile.new
strings.language_codes.concat KNOWN_LANGUAGES
formatter = formatter_class.new
formatter.strings = strings
Twine::Formatters.formatters.clear
Twine::Formatters.formatters << formatter
formatter
end
end

Binary file not shown.

Binary file not shown.

View file

@ -1,15 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Android Strings File -->
<!-- Generated by Twine <%= Twine::VERSION %> -->
<!-- Language: en -->
<resources>
<!-- SECTION: Section 1 -->
<!-- comment key1 -->
<string name="key1">value1-english</string>
<string name="key2">value2-english</string>
<!-- SECTION: Section 1 -->
<!-- comment key1 -->
<string name="key1">value1-english</string>
<string name="key2">value2-english</string>
<!-- SECTION: Section 2 -->
<string name="key3">value3-english</string>
<!-- comment key4 -->
<string name="key4">value4-english</string>
<!-- SECTION: Section 2 -->
<string name="key3">value3-english</string>
<!-- comment key4 -->
<string name="key4">value4-english</string>
</resources>

View file

@ -1,9 +1,3 @@
/**
* Apple Strings File
* Generated by Twine <%= Twine::VERSION %>
* Language: en
*/
/********** Section 1 **********/
/* comment key1 */

View file

@ -1,10 +1,11 @@
##
# Django Strings File
# Generated by Twine <%= Twine::VERSION %>
# Language: en
# Django Strings File
# Generated by Twine
# Language: en
msgid ""
msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
#--------- Section 1 ---------#
# --------- Section 1 --------- #
#. comment key1
# base translation: "value1-english"
@ -16,7 +17,7 @@ msgid "key2"
msgstr "value2-english"
#--------- Section 2 ---------#
# --------- Section 2 --------- #
# base translation: "value3-english"
msgid "key3"

View file

@ -1,5 +1,5 @@
## Flash Strings File
## Generated by Twine <%= Twine::VERSION %>
## Generated by Twine
## Language: en
## Section 1 ##

View file

@ -1,7 +1,7 @@
msgid ""
msgstr ""
"Language: en\n"
"X-Generator: Twine <%= Twine::VERSION %>\n"
"Language: en"
"X-Generator: Twine <%= Twine::VERSION %>"
# SECTION: Section 1

View file

@ -0,0 +1,10 @@
msgid ""
msgstr ""
"Language: en"
"X-Generator: Twine <%= Twine::VERSION %>"
# SECTION: Section
msgctxt "key"
msgid "foo \"bar\" baz"
msgstr "foo \"bar\" baz"

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Tizen Strings File -->
<!-- Generated by Twine <%= Twine::VERSION %> -->
<!-- Generated by Twine -->
<!-- Language: en -->
<string_table Bversion="2.0.0.201311071819" Dversion="20120315">
<!-- SECTION: Section 1 -->

View file

@ -1,164 +1,164 @@
require 'twine_test_case'
require 'twine_test'
class TestAbstractFormatter < TwineTestCase
class SetTranslation < TwineTestCase
class TestAbstractFormatter < TwineTest
class SetTranslation < TwineTest
def setup
super
@strings = build_twine_file 'en', 'fr' do
@twine_file = build_twine_file 'en', 'fr' do
add_section 'Section' do
add_row key1: 'value1-english'
add_row key2: { en: 'value2-english', fr: 'value2-french' }
add_definition key1: 'value1-english'
add_definition key2: { en: 'value2-english', fr: 'value2-french' }
end
end
@formatter = Twine::Formatters::Abstract.new
@formatter.strings = @strings
@formatter.twine_file = @twine_file
end
def test_set_translation_updates_existing_value
@formatter.set_translation_for_key 'key1', 'en', 'value1-english-updated'
assert_equal 'value1-english-updated', @strings.strings_map['key1'].translations['en']
assert_equal 'value1-english-updated', @twine_file.definitions_by_key['key1'].translations['en']
end
def test_set_translation_does_not_alter_other_language
@formatter.set_translation_for_key 'key2', 'en', 'value2-english-updated'
assert_equal 'value2-french', @strings.strings_map['key2'].translations['fr']
assert_equal 'value2-french', @twine_file.definitions_by_key['key2'].translations['fr']
end
def test_set_translation_escapes_newlines
@formatter.set_translation_for_key 'key1', 'en', "new\nline"
assert_equal 'new\nline', @strings.strings_map['key1'].translations['en']
assert_equal 'new\nline', @twine_file.definitions_by_key['key1'].translations['en']
end
def test_set_translation_adds_translation_to_existing_key
@formatter.set_translation_for_key 'key1', 'fr', 'value1-french'
assert_equal 'value1-french', @strings.strings_map['key1'].translations['fr']
assert_equal 'value1-french', @twine_file.definitions_by_key['key1'].translations['fr']
end
def test_set_translation_does_not_add_new_key
@formatter.set_translation_for_key 'new-key', 'en', 'new-key-english'
assert_nil @strings.strings_map['new-key']
assert_nil @twine_file.definitions_by_key['new-key']
end
def test_set_translation_consume_all_adds_new_key
formatter = Twine::Formatters::Abstract.new
formatter.strings = @strings
formatter.twine_file = @twine_file
formatter.options = { consume_all: true }
formatter.set_translation_for_key 'new-key', 'en', 'new-key-english'
assert_equal 'new-key-english', @strings.strings_map['new-key'].translations['en']
assert_equal 'new-key-english', @twine_file.definitions_by_key['new-key'].translations['en']
end
def test_set_translation_consume_all_adds_tags
random_tag = SecureRandom.uuid
formatter = Twine::Formatters::Abstract.new
formatter.strings = @strings
formatter.twine_file = @twine_file
formatter.options = { consume_all: true, tags: [random_tag] }
formatter.set_translation_for_key 'new-key', 'en', 'new-key-english'
assert_equal [random_tag], @strings.strings_map['new-key'].tags
assert_equal [random_tag], @twine_file.definitions_by_key['new-key'].tags
end
def test_set_translation_adds_new_keys_to_category_uncategoriezed
formatter = Twine::Formatters::Abstract.new
formatter.strings = @strings
formatter.twine_file = @twine_file
formatter.options = { consume_all: true }
formatter.set_translation_for_key 'new-key', 'en', 'new-key-english'
assert_equal 'Uncategorized', @strings.sections[0].name
assert_equal 'new-key', @strings.sections[0].rows[0].key
assert_equal 'Uncategorized', @twine_file.sections[0].name
assert_equal 'new-key', @twine_file.sections[0].definitions[0].key
end
end
class ValueReference < TwineTestCase
class ValueReference < TwineTest
def setup
super
@strings = build_twine_file 'en', 'fr' do
@twine_file = build_twine_file 'en', 'fr' do
add_section 'Section' do
add_row refkey: 'ref-value'
add_row key: :refkey
add_definition refkey: 'ref-value'
add_definition key: :refkey
end
end
@formatter = Twine::Formatters::Abstract.new
@formatter.strings = @strings
@formatter.twine_file = @twine_file
end
def test_set_translation_does_not_add_unchanged_translation
@formatter.set_translation_for_key 'key', 'en', 'ref-value'
assert_nil @strings.strings_map['key'].translations['en']
assert_nil @twine_file.definitions_by_key['key'].translations['en']
end
def test_set_translation_adds_changed_translation
@formatter.set_translation_for_key 'key', 'en', 'changed value'
assert_equal 'changed value', @strings.strings_map['key'].translations['en']
assert_equal 'changed value', @twine_file.definitions_by_key['key'].translations['en']
end
end
class SetComment < TwineTestCase
class SetComment < TwineTest
def setup
super
@strings = build_twine_file 'en' do
@twine_file = build_twine_file 'en' do
add_section 'Section' do
add_row key: 'value'
add_definition key: 'value'
end
end
end
def test_set_comment_for_key_does_not_update_comment
formatter = Twine::Formatters::Abstract.new
formatter.strings = @strings
formatter.twine_file = @twine_file
formatter.set_comment_for_key('key', 'comment')
assert_nil formatter.strings.strings_map['key'].comment
assert_nil formatter.twine_file.definitions_by_key['key'].comment
end
def test_set_comment_for_key_updates_comment_with_update_comments
formatter = Twine::Formatters::Abstract.new
formatter.strings = @strings
formatter.twine_file = @twine_file
formatter.options = { consume_comments: true }
formatter.set_comment_for_key('key', 'comment')
assert_equal 'comment', formatter.strings.strings_map['key'].comment
assert_equal 'comment', formatter.twine_file.definitions_by_key['key'].comment
end
end
class CommentReference < TwineTestCase
class CommentReference < TwineTest
def setup
super
@strings = build_twine_file 'en' do
@twine_file = build_twine_file 'en' do
add_section 'Section' do
add_row refkey: 'ref-value', comment: 'reference comment'
add_row key: 'value', ref: :refkey
add_definition refkey: 'ref-value', comment: 'reference comment'
add_definition key: 'value', ref: :refkey
end
end
@formatter = Twine::Formatters::Abstract.new
@formatter.strings = @strings
@formatter.twine_file = @twine_file
@formatter.options = { consume_comments: true }
end
def test_set_comment_does_not_add_unchanged_comment
@formatter.set_comment_for_key 'key', 'reference comment'
assert_nil @strings.strings_map['key'].raw_comment
assert_nil @twine_file.definitions_by_key['key'].raw_comment
end
def test_set_comment_adds_changed_comment
@formatter.set_comment_for_key 'key', 'changed comment'
assert_equal 'changed comment', @strings.strings_map['key'].raw_comment
assert_equal 'changed comment', @twine_file.definitions_by_key['key'].raw_comment
end
end

View file

@ -1,10 +1,10 @@
require 'twine_test_case'
require 'twine_test'
class CLITestCase < TwineTestCase
class CLITest < TwineTest
def setup
super
super()
@strings_file_path = File.join @output_dir, SecureRandom.uuid
@twine_file_path = File.join @output_dir, SecureRandom.uuid
@input_path = File.join @output_dir, SecureRandom.uuid
@input_dir = @output_dir
end
@ -13,284 +13,455 @@ class CLITestCase < TwineTestCase
@options = Twine::CLI::parse command.split
end
class TestValidateStringsFile < CLITestCase
def test_command
parse "validate-strings-file #{@strings_file_path}"
assert_equal 'validate-strings-file', @options[:command]
assert_equal @strings_file_path, @options[:strings_file]
end
def test_pedantic
parse "validate-strings-file #{@strings_file_path} --pedantic"
assert @options[:pedantic]
end
def test_missing_parameter
assert_raises Twine::Error do
parse 'validate-strings-file'
end
end
def test_extra_parameter
assert_raises Twine::Error do
parse 'validate-strings-file strings extra'
end
end
def parse_with(parameters)
raise "you need to implement `parse_with` in your test class"
end
class TestGenerateStringFile < CLITestCase
def test_command
parse "generate-string-file #{@strings_file_path} #{@output_path}"
assert_equal 'generate-string-file', @options[:command]
assert_equal @strings_file_path, @options[:strings_file]
assert_equal @output_path, @options[:output_path]
end
def test_missing_parameter
assert_raises Twine::Error do
parse 'generate-string-file strings'
end
end
def test_validate
parse "generate-string-file #{@strings_file_path} #{@output_path} --validate"
assert @options[:validate]
end
def test_extra_parameter
assert_raises Twine::Error do
parse 'generate-string-file strings output extra'
end
end
def test_only_allows_one_language
assert_raises Twine::Error do
parse "generate-string-file strings output --lang en,fr"
end
end
def assert_help
parse_with '--help'
assert_equal @options, false
assert_match /Usage: twine.*Examples:/m, Twine::stdout.string
end
class TestGenerateAllStringFiles < CLITestCase
def test_command
parse "generate-all-string-files #{@strings_file_path} #{@output_dir}"
assert_equal 'generate-all-string-files', @options[:command]
assert_equal @strings_file_path, @options[:strings_file]
assert_equal @output_dir, @options[:output_path]
end
def test_missing_parameter
assert_raises Twine::Error do
parse "generate-all-string-files strings"
end
end
def test_validate
parse "generate-all-string-files #{@strings_file_path} #{@output_dir} --validate"
assert @options[:validate]
end
def test_extra_parameter
assert_raises Twine::Error do
parse "generate-all-string-files strings output extra"
end
end
def assert_option_consume_all
parse_with '--consume-all'
assert @options[:consume_all]
parse_with '--no-consume-all'
refute @options[:consume_all]
end
class TestConsumeStringFile < CLITestCase
def test_command
parse "consume-string-file #{@strings_file_path} #{@input_path}"
assert_equal 'consume-string-file', @options[:command]
assert_equal @strings_file_path, @options[:strings_file]
assert_equal @input_path, @options[:input_path]
end
def test_missing_parameter
assert_raises Twine::Error do
parse "consume-string-file strings"
end
end
def test_extra_parameter
assert_raises Twine::Error do
parse "consume-string-file strings output extra"
end
end
def test_only_allows_one_language
assert_raises Twine::Error do
parse "consume-string-file strings output --lang en,fr"
end
end
def assert_option_consume_comments
parse_with '--consume-comments'
assert @options[:consume_comments]
parse_with '--no-consume-comments'
refute @options[:consume_comments]
end
class TestConsumeAllStringFiles < CLITestCase
def test_command
parse "consume-all-string-files #{@strings_file_path} #{@input_dir}"
assert_equal 'consume-all-string-files', @options[:command]
assert_equal @strings_file_path, @options[:strings_file]
assert_equal @input_dir, @options[:input_path]
end
def test_missing_parameter
assert_raises Twine::Error do
parse "consume-all-string-files strings"
end
end
def test_extra_parameter
assert_raises Twine::Error do
parse "consume-all-string-files strings output extra"
end
end
def assert_option_developer_language
random_language = KNOWN_LANGUAGES.sample
parse_with "--developer-language #{random_language}"
assert_equal random_language, @options[:developer_language]
end
class TestGenerateLocDrop < CLITestCase
def test_command
parse "generate-loc-drop #{@strings_file_path} #{@output_path} --format apple"
assert_equal 'generate-loc-drop', @options[:command]
assert_equal @strings_file_path, @options[:strings_file]
assert_equal @output_path, @options[:output_path]
end
def test_missing_parameter
assert_raises Twine::Error do
parse "generate-loc-drop strings --format apple"
end
end
def test_validate
parse "generate-loc-drop #{@strings_file_path} #{@output_path} --format apple --validate"
assert @options[:validate]
end
def test_extra_parameter
assert_raises Twine::Error do
parse "generate-loc-drop strings output extra --format apple"
end
end
def test_format_needed
assert_raises Twine::Error do
parse "generate-loc-drop strings output"
end
end
def assert_option_encoding
parse_with '--encoding UTF16'
assert_equal 'UTF16', @options[:encoding]
end
class TestConsumeLocDrop < CLITestCase
def test_command
parse "consume-loc-drop #{@strings_file_path} #{@input_path}"
assert_equal 'consume-loc-drop', @options[:command]
assert_equal @strings_file_path, @options[:strings_file]
assert_equal @input_path, @options[:input_path]
end
def test_missing_parameter
assert_raises Twine::Error do
parse "consume-loc-drop strings"
end
end
def test_extra_parameter
assert_raises Twine::Error do
parse "consume-loc-drop strings input extra"
end
end
def assert_option_escape_all_tags
parse_with "--escape-all-tags"
assert @options[:escape_all_tags]
parse_with "--no-escape-all-tags"
refute @options[:escape_all_tags]
end
class TestParameters < CLITestCase
def parse_with(parameter)
parse 'validate-strings-file input.txt ' + parameter
end
def assert_option_format
random_format = Twine::Formatters.formatters.sample.format_name.downcase
parse_with "--format #{random_format}"
assert_equal random_format, @options[:format]
end
def test_default_options
parse_with ''
expected = {command: 'validate-strings-file', strings_file: 'input.txt', include: :all}
assert_equal expected, @options
end
def assert_option_include
random_set = [:all, :translated, :untranslated].sample
parse_with "--include #{random_set}"
assert_equal random_set, @options[:include]
end
def test_create_folders
parse_with '--create-folders'
assert @options[:create_folders]
end
def assert_option_single_language
random_language = KNOWN_LANGUAGES.sample
parse_with "--lang #{random_language}"
assert_equal [random_language], @options[:languages]
end
def test_consume_all
parse_with '--consume-all'
assert @options[:consume_all]
end
def assert_option_multiple_languages
random_languages = KNOWN_LANGUAGES.shuffle[0, 3]
parse_with "--lang #{random_languages.join(',')}"
assert_equal random_languages.sort, @options[:languages].sort
end
def test_consume_comments
parse_with '--consume-comments'
assert @options[:consume_comments]
end
def assert_option_languages
assert_option_single_language
assert_option_multiple_languages
end
def test_untagged
parse_with '--untagged'
assert @options[:untagged]
end
def assert_option_output_path
parse_with "--output-file #{@output_path}"
assert_equal @output_path, @options[:output_path]
end
def test_developer_language
random_language = KNOWN_LANGUAGES.sample
parse_with "--developer-lang #{random_language}"
assert_equal random_language, @options[:developer_language]
end
def assert_option_quiet
parse_with '--quiet'
assert @options[:quiet]
parse_with '--no-quiet'
refute @options[:quiet]
end
def test_single_language
random_language = KNOWN_LANGUAGES.sample
parse_with "--lang #{random_language}"
assert_equal [random_language], @options[:languages]
end
def assert_option_tags
# single tag
random_tag = "tag#{rand(100)}"
parse_with "--tags #{random_tag}"
assert_equal [[random_tag]], @options[:tags]
def test_multiple_languages
random_languages = KNOWN_LANGUAGES.shuffle[0, 3]
parse_with "--lang #{random_languages.join(',')}"
assert_equal random_languages.sort, @options[:languages].sort
end
# multiple OR tags
random_tags = ["tag#{rand(100)}", "tag#{rand(100)}", "tag#{rand(100)}"]
parse_with "--tags #{random_tags.join(',')}"
sorted_tags = @options[:tags].map { |tags| tags.sort }
assert_equal [random_tags.sort], sorted_tags
def test_single_tag
random_tag = "tag#{rand(100)}"
parse_with "--tags #{random_tag}"
assert_equal [random_tag], @options[:tags]
end
# multiple AND tags
random_tag_1 = "tag#{rand(100)}"
random_tag_2 = "tag#{rand(100)}"
parse_with "--tags #{random_tag_1} --tags #{random_tag_2}"
assert_equal [[random_tag_1], [random_tag_2]], @options[:tags]
# NOT tag
random_tag = "~tag#{rand(100)}"
parse_with "--tags #{random_tag}"
assert_equal [[random_tag]], @options[:tags]
end
def test_multiple_tags
random_tags = ([nil] * 3).map { "tag#{rand(100)}" }
parse_with "--tags #{random_tags.join(',')}"
assert_equal random_tags.sort, @options[:tags].sort
end
def assert_option_untagged
parse_with '--untagged'
assert @options[:untagged]
parse_with '--no-untagged'
refute @options[:untagged]
end
def test_format
random_format = Twine::Formatters.formatters.sample.format_name.downcase
parse_with "--format #{random_format}"
assert_equal random_format, @options[:format]
end
def assert_option_validate
parse_with "--validate"
assert @options[:validate]
parse_with "--no-validate"
refute @options[:validate]
end
end
def test_include
random_set = [:all, :translated, :untranslated].sample
parse_with "--include #{random_set}"
assert_equal random_set, @options[:include]
end
class TestCLI < CLITest
def test_version
parse "--version"
def test_output_path
parse_with "--output-file #{@output_path}"
assert_equal @output_path, @options[:output_path]
end
assert_equal @options, false
assert_equal "Twine version #{Twine::VERSION}\n", Twine::stdout.string
end
def test_file_name
random_filename = "#{rand(10000)}"
parse_with "--file-name #{random_filename}"
assert_equal random_filename, @options[:file_name]
end
def test_help
parse ""
assert_match 'Usage: twine', Twine::stdout.string
end
def test_encoding
parse_with '--encoding UTF16'
assert_equal 'UTF16', @options[:output_encoding]
def test_invalid_command
assert_raises Twine::Error do
parse "not a command"
end
end
end
class TestGenerateLocalizationFileCLI < CLITest
def parse_with(parameters)
parse "generate-localization-file #{@twine_file_path} #{@output_path} " + parameters
end
def test_command
parse_with ""
assert_equal 'generate-localization-file', @options[:command]
assert_equal @twine_file_path, @options[:twine_file]
assert_equal @output_path, @options[:output_path]
end
def test_missing_argument
assert_raises Twine::Error do
parse "generate-localization-file #{@twine_file_path}"
end
end
def test_extra_argument
assert_raises Twine::Error do
parse_with "extra"
end
end
def test_options
assert_help
assert_option_developer_language
assert_option_encoding
assert_option_escape_all_tags
assert_option_format
assert_option_include
assert_option_single_language
assert_raises(Twine::Error) { assert_option_multiple_languages }
assert_option_quiet
assert_option_tags
assert_option_untagged
assert_option_validate
end
end
class TestGenerateAllLocalizationFilesCLI < CLITest
def parse_with(parameters)
parse "generate-all-localization-files #{@twine_file_path} #{@output_dir} " + parameters
end
def test_command
parse_with ""
assert_equal 'generate-all-localization-files', @options[:command]
assert_equal @twine_file_path, @options[:twine_file]
assert_equal @output_dir, @options[:output_path]
end
def test_missing_argument
assert_raises Twine::Error do
parse "generate-all-localization-files twine_file"
end
end
def test_extra_arguemnt
assert_raises Twine::Error do
parse_with "extra"
end
end
def test_options
assert_help
assert_option_developer_language
assert_option_encoding
assert_option_escape_all_tags
assert_option_format
assert_option_include
assert_option_quiet
assert_option_tags
assert_option_untagged
assert_option_validate
end
def test_option_create_folders
parse_with '--create-folders'
assert @options[:create_folders]
parse_with '--no-create-folders'
refute @options[:create_folders]
end
def test_option_file_name
random_filename = "#{rand(10000)}"
parse_with "--file-name #{random_filename}"
assert_equal random_filename, @options[:file_name]
end
end
class TestGenerateLocalizationArchiveCLI < CLITest
def parse_with(parameters)
parse "generate-localization-archive #{@twine_file_path} #{@output_path} --format apple " + parameters
end
def test_command
parse_with ""
assert_equal 'generate-localization-archive', @options[:command]
assert_equal @twine_file_path, @options[:twine_file]
assert_equal @output_path, @options[:output_path]
end
def test_missing_argument
assert_raises Twine::Error do
parse "generate-localization-archive twine_file --format apple"
end
end
def test_extra_argument
assert_raises Twine::Error do
parse_with "extra"
end
end
def test_options
assert_help
assert_option_developer_language
assert_option_encoding
assert_option_escape_all_tags
assert_option_include
assert_option_quiet
assert_option_tags
assert_option_untagged
assert_option_validate
end
def test_option_format_required
assert_raises Twine::Error do
parse "generate-localization-archive twine_file output"
end
end
def test_supports_deprecated_command
parse "generate-loc-drop #{@twine_file_path} #{@output_path} --format apple"
assert_equal 'generate-localization-archive', @options[:command]
end
def test_deprecated_command_prints_warning
parse "generate-loc-drop #{@twine_file_path} #{@output_path} --format apple"
assert_match "WARNING: Twine commands names have changed.", Twine::stdout.string
end
end
class TestConsumeLocalizationFileCLI < CLITest
def parse_with(parameters)
parse "consume-localization-file #{@twine_file_path} #{@input_path} " + parameters
end
def test_command
parse_with ""
assert_equal 'consume-localization-file', @options[:command]
assert_equal @twine_file_path, @options[:twine_file]
assert_equal @input_path, @options[:input_path]
end
def test_missing_argument
assert_raises Twine::Error do
parse "consume-localization-file twine_file"
end
end
def test_extra_argument
assert_raises Twine::Error do
parse_with "extra"
end
end
def test_options
assert_help
assert_option_consume_all
assert_option_consume_comments
assert_option_developer_language
assert_option_encoding
assert_option_format
assert_option_single_language
assert_raises(Twine::Error) { assert_option_multiple_languages }
assert_option_output_path
assert_option_quiet
assert_option_tags
end
end
class TestConsumeAllLocalizationFilesCLI < CLITest
def parse_with(parameters)
parse "consume-all-localization-files #{@twine_file_path} #{@input_dir} " + parameters
end
def test_command
parse_with ""
assert_equal 'consume-all-localization-files', @options[:command]
assert_equal @twine_file_path, @options[:twine_file]
assert_equal @input_dir, @options[:input_path]
end
def test_missing_argument
assert_raises Twine::Error do
parse "consume-all-localization-files twine_file"
end
end
def test_extra_argument
assert_raises Twine::Error do
parse_with "extra"
end
end
def test_options
assert_help
assert_option_consume_all
assert_option_consume_comments
assert_option_developer_language
assert_option_encoding
assert_option_format
assert_option_output_path
assert_option_quiet
assert_option_tags
end
end
class TestConsumeLocalizationArchiveCLI < CLITest
def parse_with(parameters)
parse "consume-localization-archive #{@twine_file_path} #{@input_path} " + parameters
end
def test_command
parse_with ""
assert_equal 'consume-localization-archive', @options[:command]
assert_equal @twine_file_path, @options[:twine_file]
assert_equal @input_path, @options[:input_path]
end
def test_missing_argument
assert_raises Twine::Error do
parse "consume-localization-archive twine_file"
end
end
def test_extra_argument
assert_raises Twine::Error do
parse_with "extra"
end
end
def test_options
assert_help
assert_option_consume_all
assert_option_consume_comments
assert_option_developer_language
assert_option_encoding
assert_option_format
assert_option_output_path
assert_option_quiet
assert_option_tags
end
def test_supports_deprecated_command
parse "consume-loc-drop #{@twine_file_path} #{@input_path}"
assert_equal 'consume-localization-archive', @options[:command]
end
def test_deprecated_command_prints_warning
parse "consume-loc-drop #{@twine_file_path} #{@input_path}"
assert_match "WARNING: Twine commands names have changed.", Twine::stdout.string
end
end
class TestValidateTwineFileCLI < CLITest
def parse_with(parameters)
parse "validate-twine-file #{@twine_file_path} " + parameters
end
def test_command
parse_with ""
assert_equal 'validate-twine-file', @options[:command]
assert_equal @twine_file_path, @options[:twine_file]
end
def test_missing_argument
assert_raises Twine::Error do
parse 'validate-twine-file'
end
end
def test_extra_argument
assert_raises Twine::Error do
parse_with 'extra'
end
end
def test_options
assert_help
assert_option_developer_language
assert_option_quiet
end
def test_option_pedantic
parse "validate-twine-file #{@twine_file_path} --pedantic"
assert @options[:pedantic]
parse "validate-twine-file #{@twine_file_path} --no-pedantic"
refute @options[:pedantic]
end
end

View file

@ -1,27 +0,0 @@
require 'command_test_case'
class TestConsumeLocDrop < CommandTestCase
def setup
super
options = {}
options[:input_path] = fixture_path 'consume_loc_drop.zip'
options[:output_path] = @output_path
options[:format] = 'apple'
@twine_file = build_twine_file 'en', 'es' do
add_section 'Section' do
add_row key1: 'value1'
end
end
@runner = Twine::Runner.new(options, @twine_file)
end
def test_consumes_zip_file
@runner.consume_loc_drop
assert @twine_file.strings_map['key1'].translations['en'], 'value1-english'
assert @twine_file.strings_map['key1'].translations['es'], 'value1-spanish'
end
end

View file

@ -0,0 +1,33 @@
require 'command_test'
class TestConsumeLocalizationArchive < CommandTest
def setup
super
@twine_file = build_twine_file 'en', 'es' do
add_section 'Section' do
add_definition key1: 'value1'
end
end
end
def new_runner(options = {})
options[:input_path] = fixture_path 'consume_localization_archive.zip'
options[:output_path] = @output_path
Twine::Runner.new(options, @twine_file)
end
def test_consumes_zip_file
new_runner(format: 'android').consume_localization_archive
assert @twine_file.definitions_by_key['key1'].translations['en'], 'value1-english'
assert @twine_file.definitions_by_key['key1'].translations['es'], 'value1-spanish'
end
def test_raises_error_if_format_ambiguous
assert_raises Twine::Error do
new_runner.consume_localization_archive
end
end
end

View file

@ -1,6 +1,6 @@
require 'command_test_case'
require 'command_test'
class TestConsumeStringFile < CommandTestCase
class TestConsumeLocalizationFile < CommandTest
def new_runner(language, file)
options = {}
options[:output_path] = File.join(@output_dir, file) if file
@ -8,10 +8,10 @@ class TestConsumeStringFile < CommandTestCase
FileUtils.touch options[:input_path]
options[:languages] = language if language
@strings = Twine::StringsFile.new
@strings.language_codes.concat KNOWN_LANGUAGES
@twine_file = Twine::TwineFile.new
@twine_file.language_codes.concat KNOWN_LANGUAGES
Twine::Runner.new(options, @strings)
Twine::Runner.new(options, @twine_file)
end
def prepare_mock_read_formatter(formatter_class)
@ -22,25 +22,25 @@ class TestConsumeStringFile < CommandTestCase
def test_deducts_android_format_from_output_path
prepare_mock_read_formatter Twine::Formatters::Android
new_runner('fr', 'fr.xml').consume_string_file
new_runner('fr', 'fr.xml').consume_localization_file
end
def test_deducts_apple_format_from_output_path
prepare_mock_read_formatter Twine::Formatters::Apple
new_runner('fr', 'fr.strings').consume_string_file
new_runner('fr', 'fr.strings').consume_localization_file
end
def test_deducts_jquery_format_from_output_path
prepare_mock_read_formatter Twine::Formatters::JQuery
new_runner('fr', 'fr.json').consume_string_file
new_runner('fr', 'fr.json').consume_localization_file
end
def test_deducts_gettext_format_from_output_path
prepare_mock_read_formatter Twine::Formatters::Gettext
new_runner('fr', 'fr.po').consume_string_file
new_runner('fr', 'fr.po').consume_localization_file
end
def test_deducts_language_from_input_path
@ -48,10 +48,10 @@ class TestConsumeStringFile < CommandTestCase
formatter = prepare_mock_formatter Twine::Formatters::Android
formatter.expects(:read).with(anything, random_language)
new_runner(nil, "#{random_language}.xml").consume_string_file
new_runner(nil, "#{random_language}.xml").consume_localization_file
end
class TestEncodings < CommandTestCase
class TestEncodings < CommandTest
class DummyFormatter < Twine::Formatters::Abstract
attr_reader :content
@ -75,10 +75,10 @@ class TestConsumeStringFile < CommandTestCase
options[:encoding] = encoding if encoding
options[:languages] = 'en'
@strings = Twine::StringsFile.new
@strings.language_codes.concat KNOWN_LANGUAGES
@twine_file = Twine::TwineFile.new
@twine_file.language_codes.concat KNOWN_LANGUAGES
Twine::Runner.new(options, @strings)
Twine::Runner.new(options, @twine_file)
end
def setup
@ -88,31 +88,31 @@ class TestConsumeStringFile < CommandTestCase
def test_reads_utf8
formatter = prepare_mock_formatter DummyFormatter
new_runner(fixture_path('enc_utf8.dummy')).consume_string_file
new_runner(fixture_path('enc_utf8.dummy')).consume_localization_file
assert_equal @expected_content, formatter.content
end
def test_reads_utf16le_bom
formatter = prepare_mock_formatter DummyFormatter
new_runner(fixture_path('enc_utf16le_bom.dummy')).consume_string_file
new_runner(fixture_path('enc_utf16le_bom.dummy')).consume_localization_file
assert_equal @expected_content, formatter.content
end
def test_reads_utf16be_bom
formatter = prepare_mock_formatter DummyFormatter
new_runner(fixture_path('enc_utf16be_bom.dummy')).consume_string_file
new_runner(fixture_path('enc_utf16be_bom.dummy')).consume_localization_file
assert_equal @expected_content, formatter.content
end
def test_reads_utf16le
formatter = prepare_mock_formatter DummyFormatter
new_runner(fixture_path('enc_utf16le.dummy'), 'UTF-16LE').consume_string_file
new_runner(fixture_path('enc_utf16le.dummy'), 'UTF-16LE').consume_localization_file
assert_equal @expected_content, formatter.content
end
def test_reads_utf16be
formatter = prepare_mock_formatter DummyFormatter
new_runner(fixture_path('enc_utf16be.dummy'), 'UTF-16BE').consume_string_file
new_runner(fixture_path('enc_utf16be.dummy'), 'UTF-16BE').consume_localization_file
assert_equal @expected_content, formatter.content
end
end

View file

@ -1,44 +1,148 @@
require 'twine_test_case'
require 'twine_test'
class FormatterTest < TwineTestCase
class FormatterTest < TwineTest
def setup(formatter_class)
super()
@twine_file = build_twine_file 'en' do
add_section 'Section 1' do
add_row key1: 'value1-english', comment: 'comment key1'
add_row key2: 'value2-english'
add_definition key1: 'value1-english', comment: 'comment key1'
add_definition key2: 'value2-english'
end
add_section 'Section 2' do
add_row key3: 'value3-english'
add_row key4: 'value4-english', comment: 'comment key4'
add_definition key3: 'value3-english'
add_definition key4: 'value4-english', comment: 'comment key4'
end
end
@strings = Twine::StringsFile.new
@empty_twine_file = Twine::TwineFile.new
@formatter = formatter_class.new
@formatter.strings = @strings
@formatter.twine_file = @empty_twine_file
@formatter.options = { consume_all: true, consume_comments: true }
end
def assert_translations_read_correctly
1.upto(4) do |i|
assert_equal "value#{i}-english", @strings.strings_map["key#{i}"].translations['en']
assert_equal "value#{i}-english", @empty_twine_file.definitions_by_key["key#{i}"].translations['en']
end
end
def assert_file_contents_read_correctly
assert_translations_read_correctly
assert_equal "comment key1", @strings.strings_map["key1"].comment
assert_equal "comment key4", @strings.strings_map["key4"].comment
assert_equal "comment key1", @empty_twine_file.definitions_by_key["key1"].comment
assert_equal "comment key4", @empty_twine_file.definitions_by_key["key4"].comment
end
end
class TestAndroidFormatter < FormatterTest
def setup
super Twine::Formatters::Android
@escape_test_values = {
'this & that' => 'this &amp; that',
'this < that' => 'this &lt; that',
"it's complicated" => "it\\'s complicated",
'a "good" way' => 'a \"good\" way',
'<b>bold</b>' => '<b>bold</b>',
'<em>bold</em>' => '<em>bold</em>',
'<i>italic</i>' => '<i>italic</i>',
'<cite>italic</cite>' => '<cite>italic</cite>',
'<dfn>italic</dfn>' => '<dfn>italic</dfn>',
'<big>larger</big>' => '<big>larger</big>',
'<small>smaller</small>' => '<small>smaller</small>',
'<font color="#45C1D0">F</font>' => '<font color="#45C1D0">F</font>',
'<tt>monospaced</tt>' => '<tt>monospaced</tt>',
'<s>strike</s>' => '<s>strike</s>',
'<strike>strike</strike>' => '<strike>strike</strike>',
'<del>strike</del>' => '<del>strike</del>',
'<u>underline</u>' => '<u>underline</u>',
'<super>superscript</super>'=> '<super>superscript</super>',
'<sub>subscript</sub>' => '<sub>subscript</sub>',
'<ul>bullet point</ul>' => '<ul>bullet point</ul>',
'<li>bullet point</li>' => '<li>bullet point</li>',
'<br>line break' => '<br>line break',
'<div>division</div>' => '<div>division</div>',
'<span style="color:#45C1D0">inline</span>' => '<span style="color:#45C1D0">inline</span>',
'<p>para</p>' => '<p>para</p>',
'<p dir="ltr">para</p>' => '<p dir="ltr">para</p>',
'<b>%@</b>' => '&lt;b>%s&lt;/b>',
'<em>%@</em>' => '&lt;em>%s&lt;/em>',
'<i>%@</i>' => '&lt;i>%s&lt;/i>',
'<cite>%@</cite>' => '&lt;cite>%s&lt;/cite>',
'<dfn>%@</dfn>' => '&lt;dfn>%s&lt;/dfn>',
'<big>%@</big>' => '&lt;big>%s&lt;/big>',
'<small>%@</small>' => '&lt;small>%s&lt;/small>',
'<font color="#45C1D0>%@</font>' => '&lt;font color="#45C1D0>%s&lt;/font>',
'<tt>%@</tt>' => '&lt;tt>%s&lt;/tt>',
'<s>%@</s>' => '&lt;s>%s&lt;/s>',
'<strike>%@</strike>' => '&lt;strike>%s&lt;/strike>',
'<del>%@</del>' => '&lt;del>%s&lt;/del>',
'<u>%@</u>' => '&lt;u>%s&lt;/u>',
'<super>%@</super>' => '&lt;super>%s&lt;/super>',
'<sub>%@</sub>' => '&lt;sub>%s&lt;/sub>',
'<ul>%@</ul>' => '&lt;ul>%s&lt;/ul>',
'<li>%@</li>' => '&lt;li>%s&lt;/li>',
'<br>%@' => '&lt;br>%s',
'<div>%@</div>' => '&lt;div>%s&lt;/div>',
'<span style="color:#45C1D0">%@</span>' => '&lt;span style="color:#45C1D0">%s&lt;/span>',
'<p>%@</p>' => '&lt;p>%s&lt;/p>',
'<p dir="ltr">%@</p>' => '&lt;p dir="ltr">%s&lt;/p>',
'<a href="target">link</a>' => '<a href="target">link</a>',
'<a href="target">"link"</a>' => '<a href="target">\"link\"</a>',
'<a href="target"></a>"out"' => '<a href="target"></a>\"out\"',
'<a href="http://url.com?param=1&param2=3&param3=%20">link</a>' => '<a href="http://url.com?param=1&param2=3&param3=%20">link</a>',
'<q>escaped</q><![CDATA[]]>' => '&lt;q>escaped&lt;/q><![CDATA[]]>',
'<![CDATA[]]><q>escaped</q>' => '<![CDATA[]]>&lt;q>escaped&lt;/q>',
'<![CDATA[<q>unescaped</q>]]>' => '<![CDATA[<q>unescaped</q>]]>',
'<![CDATA[<q>unescaped with %@</q>]]>' => '<![CDATA[<q>unescaped with %s</q>]]>',
'<![CDATA[]]><![CDATA[<q>unescaped</q>]]>' => '<![CDATA[]]><![CDATA[<q>unescaped</q>]]>',
'<![CDATA[&]]>' => '<![CDATA[&]]>',
'<![CDATA[\']]>' => '<![CDATA[\']]>',
'<![CDATA["]]>' => '<![CDATA["]]>',
'<xliff:g></xliff:g>' => '<xliff:g></xliff:g>',
'<xliff:g>untouched</xliff:g>' => '<xliff:g>untouched</xliff:g>',
'<xliff:g id="42">untouched</xliff:g>' => '<xliff:g id="42">untouched</xliff:g>',
'<xliff:g id="1">first</xliff:g> inbetween <xliff:g id="2">second</xliff:g>' => '<xliff:g id="1">first</xliff:g> inbetween <xliff:g id="2">second</xliff:g>'
}
@escape_all_test_values = {
'<b>bold</b>' => '&lt;b>bold&lt;/b>',
'<i>italic</i>' => '&lt;i>italic&lt;/i>',
'<u>underline</u>' => '&lt;u>underline&lt;/u>'
}
end
def test_read_format
@ -47,29 +151,105 @@ class TestAndroidFormatter < FormatterTest
assert_file_contents_read_correctly
end
def test_read_multiline_translation
content = <<-EOCONTENT
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="foo">This is
a string</string>
</resources>
EOCONTENT
io = StringIO.new(content)
@formatter.read io, 'en'
assert_equal 'This is\n a string', @empty_twine_file.definitions_by_key["foo"].translations['en']
end
def test_read_multiline_comment
content = <<-EOCONTENT
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- multiline
comment -->
<string name="foo">This is
a string</string>
</resources>
EOCONTENT
io = StringIO.new(content)
@formatter.read io, 'en'
assert_equal 'multiline comment', @empty_twine_file.definitions_by_key["foo"].comment
end
def test_read_html_tags
content = <<-EOCONTENT
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="foo">Hello, <b>BOLD</b></string>
</resources>
EOCONTENT
io = StringIO.new(content)
@formatter.read io, 'en'
assert_equal 'Hello, <b>BOLD</b>', @empty_twine_file.definitions_by_key["foo"].translations['en']
end
def test_double_quotes_are_not_modified
content = <<-EOCONTENT
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="foo">Hello, <a href="http://www.foo.com">BOLD</a></string>
</resources>
EOCONTENT
io = StringIO.new(content)
@formatter.read io, 'en'
assert_equal 'Hello, <a href="http://www.foo.com">BOLD</a>', @empty_twine_file.definitions_by_key["foo"].translations['en']
end
def test_set_translation_converts_leading_spaces
@formatter.set_translation_for_key 'key1', 'en', "\u0020value"
assert_equal ' value', @strings.strings_map['key1'].translations['en']
assert_equal ' value', @empty_twine_file.definitions_by_key['key1'].translations['en']
end
def test_set_translation_coverts_trailing_spaces
@formatter.set_translation_for_key 'key1', 'en', "value\u0020\u0020"
assert_equal 'value ', @strings.strings_map['key1'].translations['en']
assert_equal 'value ', @empty_twine_file.definitions_by_key['key1'].translations['en']
end
def test_set_translation_converts_string_placeholders
@formatter.set_translation_for_key 'key1', 'en', "value %s"
assert_equal 'value %@', @strings.strings_map['key1'].translations['en']
assert_equal 'value %@', @empty_twine_file.definitions_by_key['key1'].translations['en']
end
def test_set_translation_unescapes_at_signs
@formatter.set_translation_for_key 'key1', 'en', '\@value'
assert_equal '@value', @strings.strings_map['key1'].translations['en']
assert_equal '@value', @empty_twine_file.definitions_by_key['key1'].translations['en']
end
def test_set_translation_unescaping
@escape_test_values.each do |expected, input|
@formatter.set_translation_for_key 'key1', 'en', input
assert_equal expected, @empty_twine_file.definitions_by_key['key1'].translations['en']
end
@escape_all_test_values.each do |expected, input|
@formatter.set_translation_for_key 'key1', 'en', input
assert_equal expected, @empty_twine_file.definitions_by_key['key1'].translations['en']
end
end
def test_format_file
formatter = Twine::Formatters::Android.new
formatter.strings = @twine_file
formatter.twine_file = @twine_file
assert_equal content('formatter_android.xml'), formatter.format_file('en')
end
@ -85,10 +265,19 @@ class TestAndroidFormatter < FormatterTest
assert_equal "value\\u0020", @formatter.format_value('value ')
end
def test_format_value_escapes_single_quotes
skip 'not working with ruby 2.0'
# http://stackoverflow.com/questions/18735608/cgiescapehtml-is-escaping-single-quote
assert_equal "not \\'so\\' easy", @formatter.format_value("not 'so' easy")
def test_format_value_string_placeholder
assert_equal "The file %s could not be found.", @formatter.format_value("The file %@ could not be found.")
end
def test_format_value_escaping
@escape_test_values.each do |input, expected|
assert_equal expected, @formatter.format_value(input)
end
@formatter.options.merge!({ escape_all_tags: true })
@escape_all_test_values.each do |input, expected|
assert_equal expected, @formatter.format_value(input)
end
end
def test_format_value_escapes_non_resource_identifier_at_signs
@ -100,8 +289,24 @@ class TestAndroidFormatter < FormatterTest
assert_equal identifier, @formatter.format_value(identifier)
end
def test_deducts_language_from_filename
language = KNOWN_LANGUAGES.sample
assert_equal language, @formatter.determine_language_given_path("#{language}.xml")
end
def test_recognize_every_twine_language_from_filename
twine_file = build_twine_file "not-a-lang-code" do
add_section "Section" do
add_definition key: "value"
end
end
@formatter.twine_file = twine_file
assert_equal "not-a-lang-code", @formatter.determine_language_given_path("not-a-lang-code.xml")
end
def test_deducts_language_from_resource_folder
language = %w(en de fr).sample
language = KNOWN_LANGUAGES.sample
assert_equal language, @formatter.determine_language_given_path("res/values-#{language}")
end
@ -109,10 +314,6 @@ class TestAndroidFormatter < FormatterTest
assert_equal 'de-AT', @formatter.determine_language_given_path("res/values-de-rAT")
end
def test_maps_laguage_deducted_from_resource_folder
assert_equal 'zh-Hans', @formatter.determine_language_given_path("res/values-zh-rCN")
end
def test_does_not_deduct_language_from_device_capability_resource_folder
assert_nil @formatter.determine_language_given_path('res/values-w820dp')
end
@ -121,8 +322,15 @@ class TestAndroidFormatter < FormatterTest
assert_equal 'values-en', @formatter.output_path_for_language('en')
end
def test_output_path_language_mappings
assert_equal 'values-zh-rCN', @formatter.output_path_for_language('zh-Hans')
def test_output_path_with_region
assert_equal 'values-en-rGB', @formatter.output_path_for_language('en-GB')
end
def test_output_path_respects_default_lang
@formatter.twine_file.language_codes.concat KNOWN_LANGUAGES
non_default_language = KNOWN_LANGUAGES[1..-1].sample
assert_equal 'values', @formatter.output_path_for_language(KNOWN_LANGUAGES[0])
assert_equal "values-#{non_default_language}", @formatter.output_path_for_language(non_default_language)
end
end
@ -137,49 +345,75 @@ class TestAppleFormatter < FormatterTest
assert_file_contents_read_correctly
end
def test_deducts_language_from_filename
language = KNOWN_LANGUAGES.sample
assert_equal language, @formatter.determine_language_given_path("#{language}.strings")
end
def test_recognize_every_twine_language_from_filename
twine_file = build_twine_file "not-a-lang-code" do
add_section "Section" do
add_definition key: "value"
end
end
@formatter.twine_file = twine_file
assert_equal "not-a-lang-code", @formatter.determine_language_given_path("not-a-lang-code.strings")
end
def test_deducts_language_from_resource_folder
language = %w(en de fr).sample
assert_equal language, @formatter.determine_language_given_path("#{language}.lproj/Localizable.strings")
end
def test_deducts_base_language_from_resource_folder
@formatter.options = { consume_all: true, consume_comments: true, developer_language: 'en' }
assert_equal 'en', @formatter.determine_language_given_path('Base.lproj/Localizations.strings')
end
def test_reads_quoted_keys
@formatter.read StringIO.new('"key" = "value"'), 'en'
assert_equal 'value', @strings.strings_map['key'].translations['en']
assert_equal 'value', @empty_twine_file.definitions_by_key['key'].translations['en']
end
def test_reads_unquoted_keys
@formatter.read StringIO.new('key = "value"'), 'en'
assert_equal 'value', @strings.strings_map['key'].translations['en']
assert_equal 'value', @empty_twine_file.definitions_by_key['key'].translations['en']
end
def test_ignores_leading_whitespace_before_quoted_keys
@formatter.read StringIO.new("\t \"key\" = \"value\""), 'en'
assert_equal 'value', @strings.strings_map['key'].translations['en']
assert_equal 'value', @empty_twine_file.definitions_by_key['key'].translations['en']
end
def test_ignores_leading_whitespace_before_unquoted_keys
@formatter.read StringIO.new("\t key = \"value\""), 'en'
assert_equal 'value', @strings.strings_map['key'].translations['en']
assert_equal 'value', @empty_twine_file.definitions_by_key['key'].translations['en']
end
def test_allows_quotes_in_quoted_keys
@formatter.read StringIO.new('"ke\"y" = "value"'), 'en'
assert_equal 'value', @strings.strings_map['ke"y'].translations['en']
assert_equal 'value', @empty_twine_file.definitions_by_key['ke"y'].translations['en']
end
def test_does_not_allow_quotes_in_quoted_keys
@formatter.read StringIO.new('ke"y = "value"'), 'en'
assert_nil @strings.strings_map['key']
assert_nil @empty_twine_file.definitions_by_key['key']
end
def test_allows_equal_signs_in_quoted_keys
@formatter.read StringIO.new('"k=ey" = "value"'), 'en'
assert_equal 'value', @strings.strings_map['k=ey'].translations['en']
assert_equal 'value', @empty_twine_file.definitions_by_key['k=ey'].translations['en']
end
def test_does_not_allow_equal_signs_in_unquoted_keys
@formatter.read StringIO.new('k=ey = "value"'), 'en'
assert_nil @strings.strings_map['key']
assert_nil @empty_twine_file.definitions_by_key['key']
end
def test_format_file
formatter = Twine::Formatters::Apple.new
formatter.strings = @twine_file
formatter.twine_file = @twine_file
assert_equal content('formatter_apple.strings'), formatter.format_file('en')
end
@ -210,17 +444,45 @@ class TestJQueryFormatter < FormatterTest
def test_format_file
formatter = Twine::Formatters::JQuery.new
formatter.strings = @twine_file
formatter.twine_file = @twine_file
assert_equal content('formatter_jquery.json'), formatter.format_file('en')
end
def test_empty_sections_are_removed
@twine_file = build_twine_file 'en' do
add_section 'Section 1' do
end
add_section 'Section 2' do
add_definition key: 'value'
end
end
formatter = Twine::Formatters::JQuery.new
formatter.twine_file = @twine_file
refute_includes formatter.format_file('en'), ','
end
def test_format_value_with_newline
assert_equal "value\nwith\nline\nbreaks", @formatter.format_value("value\nwith\nline\nbreaks")
end
def test_deducts_language_from_filename
language = KNOWN_LANGUAGES.sample
assert_equal language, @formatter.determine_language_given_path("#{language}.json")
end
def test_deducts_language_from_extended_filename
language = KNOWN_LANGUAGES.sample
assert_equal language, @formatter.determine_language_given_path("something-#{language}.json")
end
def test_deducts_language_from_path
language = %w(en-GB de fr).sample
assert_equal language, @formatter.determine_language_given_path("/output/#{language}/#{@formatter.default_file_name}")
end
end
class TestGettextFormatter < FormatterTest
def setup
super Twine::Formatters::Gettext
end
@ -234,15 +496,34 @@ class TestGettextFormatter < FormatterTest
def test_read_with_multiple_line_value
@formatter.read content_io('gettext_multiline.po'), 'en'
assert_equal 'multiline\nstring', @strings.strings_map['key1'].translations['en']
assert_equal 'multiline\nstring', @empty_twine_file.definitions_by_key['key1'].translations['en']
end
def test_format_file
formatter = Twine::Formatters::Gettext.new
formatter.strings = @twine_file
formatter.twine_file = @twine_file
assert_equal content('formatter_gettext.po'), formatter.format_file('en')
end
def test_deducts_language_and_region
language = "en-GB"
assert_equal language, @formatter.determine_language_given_path("#{language}.po")
end
def test_deducts_language_from_path
language = %w(en-GB de fr).sample
assert_equal language, @formatter.determine_language_given_path("/output/#{language}/#{@formatter.default_file_name}")
end
def test_quoted_strings
formatter = Twine::Formatters::Gettext.new
formatter.twine_file = build_twine_file "not-a-lang-code" do
add_section "Section" do
add_definition key: "foo \"bar\" baz"
end
end
assert_equal content('formatter_gettext_quotes.po'), formatter.format_file('en')
end
end
class TestTizenFormatter < FormatterTest
@ -260,10 +541,9 @@ class TestTizenFormatter < FormatterTest
def test_format_file
formatter = Twine::Formatters::Tizen.new
formatter.strings = @twine_file
formatter.twine_file = @twine_file
assert_equal content('formatter_tizen.xml'), formatter.format_file('en')
end
end
class TestDjangoFormatter < FormatterTest
@ -279,9 +559,32 @@ class TestDjangoFormatter < FormatterTest
def test_format_file
formatter = Twine::Formatters::Django.new
formatter.strings = @twine_file
formatter.twine_file = @twine_file
assert_equal content('formatter_django.po'), formatter.format_file('en')
end
def test_deducts_language_and_region
language = "en-GB"
assert_equal language, @formatter.determine_language_given_path("#{language}.po")
end
def test_deducts_language_from_path
language = %w(en-GB de fr).sample
assert_equal language, @formatter.determine_language_given_path("/output/#{language}/#{@formatter.default_file_name}")
end
def test_ignores_commented_out_strings
content = <<-EOCONTENT
#~ msgid "foo"
#~ msgstr "This should be ignored"
EOCONTENT
io = StringIO.new(content)
@formatter.read io, 'en'
assert_nil @empty_twine_file.definitions_by_key["foo"]
end
end
class TestFlashFormatter < FormatterTest
@ -295,9 +598,27 @@ class TestFlashFormatter < FormatterTest
assert_file_contents_read_correctly
end
def test_set_translation_converts_placeholders
@formatter.set_translation_for_key 'key1', 'en', "value {#{rand(10)}}"
assert_equal 'value %@', @empty_twine_file.definitions_by_key['key1'].translations['en']
end
def test_format_file
formatter = Twine::Formatters::Flash.new
formatter.strings = @twine_file
formatter.twine_file = @twine_file
assert_equal content('formatter_flash.properties'), formatter.format_file('en')
end
def test_format_value_converts_placeholders
assert_equal "value {0}", @formatter.format_value('value %d')
end
def test_deducts_language_from_resource_folder
language = %w(en de fr).sample
assert_equal language, @formatter.determine_language_given_path("locale/#{language}/#{@formatter.default_file_name}")
end
def test_deducts_language_and_region_from_resource_folder
assert_equal 'de-AT', @formatter.determine_language_given_path("locale/de-AT/#{@formatter.default_file_name}")
end
end

View file

@ -0,0 +1,140 @@
require 'command_test'
class TestGenerateAllLocalizationFiles < CommandTest
def new_runner(create_folders, twine_file = nil, options = {})
default_options = {}
default_options[:output_path] = @output_dir
default_options[:format] = 'apple'
default_options[:create_folders] = create_folders
options = default_options.merge options
unless twine_file
twine_file = build_twine_file 'en', 'es' do
add_section 'Section' do
add_definition key: 'value'
end
end
end
Twine::Runner.new(options, twine_file)
end
class TestFormatterSelection < TestGenerateAllLocalizationFiles
def setup
super
Dir.mkdir File.join @output_dir, 'values-en'
end
def new_runner(options = {})
super(true, nil, options)
end
def test_returns_error_for_ambiguous_output_path
assert_raises Twine::Error do
new_runner(format: nil).generate_all_localization_files
end
end
def test_uses_specified_formatter_to_resolve_ambiguity
# implicit assert that this call doesn't raise an exception
new_runner(format: 'android').generate_all_localization_files
end
end
class TestDoNotCreateFolders < TestGenerateAllLocalizationFiles
def new_runner(twine_file = nil, options = {})
super(false, twine_file, options)
end
def test_fails_if_output_folder_does_not_exist
assert_raises Twine::Error do
new_runner.generate_all_localization_files
end
end
def test_does_not_create_language_folders
Dir.mkdir File.join @output_dir, 'en.lproj'
new_runner.generate_all_localization_files
refute File.exist?(File.join(@output_dir, 'es.lproj')), "language folder should not be created"
end
def test_prints_empty_file_warnings
Dir.mkdir File.join @output_dir, 'en.lproj'
empty_twine_file = build_twine_file('en') {}
new_runner(empty_twine_file).generate_all_localization_files
assert_match "Skipping file at path", Twine::stdout.string
end
def test_does_not_print_empty_file_warnings_if_quite
Dir.mkdir File.join @output_dir, 'en.lproj'
empty_twine_file = build_twine_file('en') {}
new_runner(empty_twine_file, quite: true).generate_all_localization_files
refute_match "Skipping file at path", Twine::stdout.string
end
end
class TestCreateFolders < TestGenerateAllLocalizationFiles
def new_runner(twine_file = nil, options = {})
super(true, twine_file, options)
end
def test_creates_output_folder
FileUtils.remove_entry_secure @output_dir
new_runner.generate_all_localization_files
assert File.exist? @output_dir
end
def test_creates_language_folders
new_runner.generate_all_localization_files
assert File.exist?(File.join(@output_dir, 'en.lproj')), "language folder 'en.lproj' should be created"
assert File.exist?(File.join(@output_dir, 'es.lproj')), "language folder 'es.lproj' should be created"
end
def test_prints_empty_file_warnings
empty_twine_file = build_twine_file('en') {}
new_runner(empty_twine_file).generate_all_localization_files
assert_match "Skipping file at path", Twine::stdout.string
end
def test_does_not_print_empty_file_warnings_if_quite
empty_twine_file = build_twine_file('en') {}
new_runner(empty_twine_file, quite: true).generate_all_localization_files
refute_match "Skipping file at path", Twine::stdout.string
end
end
class TestValidate < CommandTest
def new_runner(validate)
Dir.mkdir File.join @output_dir, 'values-en'
options = {}
options[:output_path] = @output_dir
options[:format] = 'android'
options[:validate] = validate
twine_file = build_twine_file 'en' do
add_section 'Section' do
add_definition key: 'value'
add_definition key: 'value'
end
end
Twine::Runner.new(options, twine_file)
end
def test_does_not_validate_twine_file
prepare_mock_formatter Twine::Formatters::Android
new_runner(false).generate_all_localization_files
end
def test_validates_twine_file_if_validate
assert_raises Twine::Error do
new_runner(true).generate_all_localization_files
end
end
end
end

View file

@ -1,102 +0,0 @@
require 'command_test_case'
class TestGenerateAllStringFiles < CommandTestCase
def new_runner(create_folders, twine_file = nil)
options = {}
options[:output_path] = @output_dir
options[:format] = 'apple'
options[:create_folders] = create_folders
unless twine_file
twine_file = build_twine_file 'en', 'es' do
add_section 'Section' do
add_row key: 'value'
end
end
end
Twine::Runner.new(options, twine_file)
end
class TestDoNotCreateFolders < TestGenerateAllStringFiles
def new_runner(twine_file = nil)
super(false, twine_file)
end
def test_fails_if_output_folder_does_not_exist
assert_raises Twine::Error do
new_runner.generate_all_string_files
end
end
def test_does_not_create_language_folders
Dir.mkdir File.join @output_dir, 'en.lproj'
new_runner.generate_all_string_files
refute File.exists?(File.join(@output_dir, 'es.lproj')), "language folder should not be created"
end
def test_prints_empty_file_warnings
Dir.mkdir File.join @output_dir, 'en.lproj'
empty_twine_file = build_twine_file('en') {}
new_runner(empty_twine_file).generate_all_string_files
assert_match "Skipping file at path", Twine::stderr.string
end
end
class TestCreateFolders < TestGenerateAllStringFiles
def new_runner(twine_file = nil)
super(true, twine_file)
end
def test_creates_output_folder
FileUtils.remove_entry_secure @output_dir
new_runner.generate_all_string_files
assert File.exists? @output_dir
end
def test_creates_language_folders
new_runner.generate_all_string_files
assert File.exists?(File.join(@output_dir, 'en.lproj')), "language folder 'en.lproj' should be created"
assert File.exists?(File.join(@output_dir, 'es.lproj')), "language folder 'es.lproj' should be created"
end
def test_prints_empty_file_warnings
empty_twine_file = build_twine_file('en') {}
new_runner(empty_twine_file).generate_all_string_files
assert_match "Skipping file at path", Twine::stderr.string
end
end
class TestValidate < CommandTestCase
def new_runner(validate)
Dir.mkdir File.join @output_dir, 'values-en'
options = {}
options[:output_path] = @output_dir
options[:format] = 'android'
options[:validate] = validate
twine_file = build_twine_file 'en' do
add_section 'Section' do
add_row key: 'value'
add_row key: 'value'
end
end
Twine::Runner.new(options, twine_file)
end
def test_does_not_validate_strings_file
prepare_mock_formatter Twine::Formatters::Android
new_runner(false).generate_all_string_files
end
def test_validates_strings_file_if_validate
assert_raises Twine::Error do
new_runner(true).generate_all_string_files
end
end
end
end

View file

@ -1,15 +1,14 @@
require 'command_test_case'
require 'command_test'
class TestGenerateLocDrop < CommandTestCase
def new_runner(twine_file = nil)
options = {}
class TestGenerateLocalizationArchive < CommandTest
def new_runner(twine_file = nil, options = {})
options[:output_path] = @output_path
options[:format] = 'apple'
unless twine_file
twine_file = build_twine_file 'en', 'fr' do
add_section 'Section' do
add_row key: 'value'
add_definition key: 'value'
end
end
end
@ -18,13 +17,13 @@ class TestGenerateLocDrop < CommandTestCase
end
def test_generates_zip_file
new_runner.generate_loc_drop
new_runner.generate_localization_archive
assert File.exists?(@output_path), "zip file should exist"
assert File.exist?(@output_path), "zip file should exist"
end
def test_zip_file_structure
new_runner.generate_loc_drop
new_runner.generate_localization_archive
names = []
Zip::File.open(@output_path) do |zipfile|
@ -39,16 +38,22 @@ class TestGenerateLocDrop < CommandTestCase
formatter = prepare_mock_formatter Twine::Formatters::Apple
formatter.expects(:format_file).twice
new_runner.generate_loc_drop
new_runner.generate_localization_archive
end
def test_prints_empty_file_warnings
empty_twine_file = build_twine_file('en') {}
new_runner(empty_twine_file).generate_loc_drop
assert_match "Skipping file", Twine::stderr.string
new_runner(empty_twine_file).generate_localization_archive
assert_match "Skipping file", Twine::stdout.string
end
class TestValidate < CommandTestCase
def test_does_not_print_empty_file_warnings_if_quite
empty_twine_file = build_twine_file('en') {}
new_runner(empty_twine_file, quite: true).generate_localization_archive
refute_match "Skipping file", Twine::stdout.string
end
class TestValidate < CommandTest
def new_runner(validate)
options = {}
options[:output_path] = @output_path
@ -57,23 +62,23 @@ class TestGenerateLocDrop < CommandTestCase
twine_file = build_twine_file 'en' do
add_section 'Section' do
add_row key: 'value'
add_row key: 'value'
add_definition key: 'value'
add_definition key: 'value'
end
end
Twine::Runner.new(options, twine_file)
end
def test_does_not_validate_strings_file
def test_does_not_validate_twine_file
prepare_mock_formatter Twine::Formatters::Android
new_runner(false).generate_loc_drop
new_runner(false).generate_localization_archive
end
def test_validates_strings_file_if_validate
def test_validates_twine_file_if_validate
assert_raises Twine::Error do
new_runner(true).generate_loc_drop
new_runner(true).generate_localization_archive
end
end
end

View file

@ -0,0 +1,119 @@
require 'command_test'
class TestGenerateLocalizationFile < CommandTest
def new_runner(language, file, options = {})
options[:output_path] = File.join(@output_dir, file) if file
options[:languages] = language if language
twine_file = Twine::TwineFile.new
twine_file.language_codes.concat KNOWN_LANGUAGES
Twine::Runner.new(options, twine_file)
end
def prepare_mock_format_file_formatter(formatter_class)
formatter = prepare_mock_formatter(formatter_class)
formatter.expects(:format_file).returns(true)
end
def test_deducts_android_format_from_output_path
prepare_mock_format_file_formatter Twine::Formatters::Android
new_runner('fr', 'fr.xml').generate_localization_file
end
def test_deducts_apple_format_from_output_path
prepare_mock_format_file_formatter Twine::Formatters::Apple
new_runner('fr', 'fr.strings').generate_localization_file
end
def test_deducts_jquery_format_from_output_path
prepare_mock_format_file_formatter Twine::Formatters::JQuery
new_runner('fr', 'fr.json').generate_localization_file
end
def test_deducts_gettext_format_from_output_path
prepare_mock_format_file_formatter Twine::Formatters::Gettext
new_runner('fr', 'fr.po').generate_localization_file
end
def test_deducts_django_format_from_output_path
prepare_mock_format_file_formatter Twine::Formatters::Django
new_runner('fr', 'fr.po').generate_localization_file
end
def test_returns_error_for_ambiguous_output_path
# both Gettext and Django use .po
gettext_formatter = prepare_mock_formatter(Twine::Formatters::Gettext)
gettext_formatter.stubs(:format_file).returns(true)
django_formatter = prepare_mock_formatter(Twine::Formatters::Django, false)
django_formatter.stubs(:format_file).returns(true)
assert_raises Twine::Error do
new_runner('fr', 'fr.po').generate_localization_file
end
end
def test_uses_specified_formatter_to_resolve_ambiguity
# both Android and Tizen use .xml
android_formatter = prepare_mock_formatter(Twine::Formatters::Android)
android_formatter.stubs(:format_file).returns(true)
tizen_formatter = prepare_mock_formatter(Twine::Formatters::Tizen, false)
tizen_formatter.stubs(:format_file).returns(true)
# implicit assert that this call doesn't raise an exception
new_runner('fr', 'fr.xml', format: 'android').generate_localization_file
end
def test_deducts_language_from_output_path
random_language = KNOWN_LANGUAGES.sample
formatter = prepare_mock_formatter Twine::Formatters::Android
formatter.expects(:format_file).with(random_language).returns(true)
new_runner(nil, "#{random_language}.xml").generate_localization_file
end
def test_returns_error_if_nothing_written
formatter = prepare_mock_formatter Twine::Formatters::Android
formatter.expects(:format_file).returns(false)
assert_raises Twine::Error do
new_runner('fr', 'fr.xml').generate_localization_file
end
end
class TestValidate < CommandTest
def new_runner(validate)
options = {}
options[:output_path] = @output_path
options[:languages] = ['en']
options[:format] = 'android'
options[:validate] = validate
twine_file = build_twine_file 'en' do
add_section 'Section' do
add_definition key: 'value'
add_definition key: 'value'
end
end
Twine::Runner.new(options, twine_file)
end
def test_does_not_validate_twine_file
prepare_mock_formatter Twine::Formatters::Android
new_runner(false).generate_localization_file
end
def test_validates_twine_file_if_validate
assert_raises Twine::Error do
new_runner(true).generate_localization_file
end
end
end
end

View file

@ -1,91 +0,0 @@
require 'command_test_case'
class TestGenerateStringFile < CommandTestCase
def new_runner(language, file)
options = {}
options[:output_path] = File.join(@output_dir, file) if file
options[:languages] = language if language
strings = Twine::StringsFile.new
strings.language_codes.concat KNOWN_LANGUAGES
Twine::Runner.new(options, strings)
end
def prepare_mock_format_file_formatter(formatter_class)
formatter = prepare_mock_formatter(formatter_class)
formatter.expects(:format_file).returns(true)
end
def test_deducts_android_format_from_output_path
prepare_mock_format_file_formatter Twine::Formatters::Android
new_runner('fr', 'fr.xml').generate_string_file
end
def test_deducts_apple_format_from_output_path
prepare_mock_format_file_formatter Twine::Formatters::Apple
new_runner('fr', 'fr.strings').generate_string_file
end
def test_deducts_jquery_format_from_output_path
prepare_mock_format_file_formatter Twine::Formatters::JQuery
new_runner('fr', 'fr.json').generate_string_file
end
def test_deducts_gettext_format_from_output_path
prepare_mock_format_file_formatter Twine::Formatters::Gettext
new_runner('fr', 'fr.po').generate_string_file
end
def test_deducts_language_from_output_path
random_language = KNOWN_LANGUAGES.sample
formatter = prepare_mock_formatter Twine::Formatters::Android
formatter.expects(:format_file).with(random_language).returns(true)
new_runner(nil, "#{random_language}.xml").generate_string_file
end
def test_returns_error_if_nothing_written
formatter = prepare_mock_formatter Twine::Formatters::Android
formatter.expects(:format_file).returns(false)
assert_raises Twine::Error do
new_runner('fr', 'fr.xml').generate_string_file
end
end
class TestValidate < CommandTestCase
def new_runner(validate)
options = {}
options[:output_path] = @output_path
options[:languages] = ['en']
options[:format] = 'android'
options[:validate] = validate
twine_file = build_twine_file 'en' do
add_section 'Section' do
add_row key: 'value'
add_row key: 'value'
end
end
Twine::Runner.new(options, twine_file)
end
def test_does_not_validate_strings_file
prepare_mock_formatter Twine::Formatters::Android
new_runner(false).generate_string_file
end
def test_validates_strings_file_if_validate
assert_raises Twine::Error do
new_runner(true).generate_string_file
end
end
end
end

View file

@ -1,84 +1,84 @@
require 'twine_test_case'
require 'twine_test'
class TestOutputProcessor < TwineTestCase
class TestOutputProcessor < TwineTest
def setup
super
@strings = build_twine_file 'en', 'fr' do
@twine_file = build_twine_file 'en', 'fr' do
add_section 'Section' do
add_row key1: 'value1', tags: ['tag1']
add_row key2: 'value2', tags: ['tag1', 'tag2']
add_row key3: 'value3', tags: ['tag2']
add_row key4: { en: 'value4-en', fr: 'value4-fr' }
add_definition key1: 'value1', tags: ['tag1']
add_definition key2: 'value2', tags: ['tag1', 'tag2']
add_definition key3: 'value3', tags: ['tag2']
add_definition key4: { en: 'value4-en', fr: 'value4-fr' }
end
end
end
def test_includes_all_keys_by_default
processor = Twine::Processors::OutputProcessor.new(@strings, {})
processor = Twine::Processors::OutputProcessor.new(@twine_file, {})
result = processor.process('en')
assert_equal %w(key1 key2 key3 key4), result.strings_map.keys.sort
assert_equal %w(key1 key2 key3 key4), result.definitions_by_key.keys.sort
end
def test_filter_by_tag
processor = Twine::Processors::OutputProcessor.new(@strings, { tags: ['tag1'] })
processor = Twine::Processors::OutputProcessor.new(@twine_file, { tags: [['tag1']] })
result = processor.process('en')
assert_equal %w(key1 key2), result.strings_map.keys.sort
assert_equal %w(key1 key2), result.definitions_by_key.keys.sort
end
def test_filter_by_multiple_tags
processor = Twine::Processors::OutputProcessor.new(@strings, { tags: ['tag1', 'tag2'] })
processor = Twine::Processors::OutputProcessor.new(@twine_file, { tags: [['tag1', 'tag2']] })
result = processor.process('en')
assert_equal %w(key1 key2 key3), result.strings_map.keys.sort
assert_equal %w(key1 key2 key3), result.definitions_by_key.keys.sort
end
def test_filter_untagged
processor = Twine::Processors::OutputProcessor.new(@strings, { tags: ['tag1'], untagged: true })
processor = Twine::Processors::OutputProcessor.new(@twine_file, { tags: [['tag1']], untagged: true })
result = processor.process('en')
assert_equal %w(key1 key2 key4), result.strings_map.keys.sort
assert_equal %w(key1 key2 key4), result.definitions_by_key.keys.sort
end
def test_include_translated
processor = Twine::Processors::OutputProcessor.new(@strings, { include: :translated })
processor = Twine::Processors::OutputProcessor.new(@twine_file, { include: :translated })
result = processor.process('fr')
assert_equal %w(key4), result.strings_map.keys.sort
assert_equal %w(key4), result.definitions_by_key.keys.sort
end
def test_include_untranslated
processor = Twine::Processors::OutputProcessor.new(@strings, { include: :untranslated })
processor = Twine::Processors::OutputProcessor.new(@twine_file, { include: :untranslated })
result = processor.process('fr')
assert_equal %w(key1 key2 key3), result.strings_map.keys.sort
assert_equal %w(key1 key2 key3), result.definitions_by_key.keys.sort
end
class TranslationFallback < TwineTestCase
class TranslationFallback < TwineTest
def setup
super
@strings = build_twine_file 'en', 'fr', 'de' do
@twine_file = build_twine_file 'en', 'fr', 'de' do
add_section 'Section' do
add_row key1: { en: 'value1-en', fr: 'value1-fr' }
add_definition key1: { en: 'value1-en', fr: 'value1-fr' }
end
end
end
def test_fallback_to_default_language
processor = Twine::Processors::OutputProcessor.new(@strings, {})
processor = Twine::Processors::OutputProcessor.new(@twine_file, {})
result = processor.process('de')
assert_equal 'value1-en', result.strings_map['key1'].translations['de']
assert_equal 'value1-en', result.definitions_by_key['key1'].translations['de']
end
def test_fallback_to_developer_language
processor = Twine::Processors::OutputProcessor.new(@strings, {developer_language: 'fr'})
processor = Twine::Processors::OutputProcessor.new(@twine_file, {developer_language: 'fr'})
result = processor.process('de')
assert_equal 'value1-fr', result.strings_map['key1'].translations['de']
assert_equal 'value1-fr', result.definitions_by_key['key1'].translations['de']
end
end

View file

@ -1,6 +1,6 @@
require 'twine_test_case'
require 'twine_test'
class PlaceholderTestCase < TwineTestCase
class PlaceholderTest < TwineTest
def assert_starts_with(prefix, value)
msg = message(nil) { "Expected #{mu_pp(value)} to start with #{mu_pp(prefix)}" }
assert value.start_with?(prefix), msg
@ -11,23 +11,25 @@ class PlaceholderTestCase < TwineTestCase
lucky = lambda { rand > 0.5 }
placeholder = '%'
placeholder += (rand * 20).to_i.to_s + '$' if lucky.call
placeholder += '-+ 0#'.chars.to_a.sample if lucky.call
placeholder += '-+0#'.chars.to_a.sample if lucky.call
placeholder += (0.upto(20).map(&:to_s) << "*").sample if lucky.call
placeholder += '.' + (0.upto(20).map(&:to_s) << "*").sample if lucky.call
placeholder += %w(h hh l ll L z j t).sample if lucky.call
placeholder += type || 'diufFeEgGxXocpaA'.chars.to_a.sample # this does not contain s or @ because strings are a special case
end
end
class PlaceholderTest < TwineTestCase
class ToAndroid < PlaceholderTestCase
class ToAndroid < PlaceholderTest
def to_android(value)
Twine::Placeholders.convert_placeholders_from_twine_to_android(value)
end
def test_replaces_string_placeholder
def test_replaces_simple_string_placeholder
assert_equal "some '%s' value", to_android("some '%@' value")
end
def test_replaces_complicated_string_placeholder
placeholder = placeholder('@')
expected = placeholder
expected = placeholder.dup
expected[-1] = 's'
assert_equal "some #{expected} value", to_android("some #{placeholder} value")
end
@ -41,6 +43,11 @@ class PlaceholderTest < TwineTestCase
assert_equal "some % value", to_android("some % value")
end
def test_does_not_modify_single_percent_signs_when_followed_by_space_and_format_letter
# Said differently: formartter parser should not recognize %a in "70% and"
assert_equal 'If 70% and 30% dog 80% end', to_android('If 70% and 30% dog 80% end')
end
def test_escapes_single_percent_signs_if_placeholder_present
assert_starts_with "some %% v", to_android("some % value #{placeholder}")
end
@ -72,9 +79,13 @@ class PlaceholderTest < TwineTestCase
to_android("some %d second %2$f")
end
end
def test_complicated_float_placeholders
assert_equal "%1$.0f%2$s (apparent: %3$.0f)", to_android("%.0f%@ (apparent: %.0f)")
end
end
class FromAndroid < PlaceholderTestCase
class FromAndroid < PlaceholderTest
def from_android(value)
Twine::Placeholders.convert_placeholders_from_android_to_twine(value)
end
@ -83,4 +94,49 @@ class PlaceholderTest < TwineTestCase
assert_equal "some %@ value", from_android("some %s value")
end
end
class ToFlash < PlaceholderTest
def to_flash(value)
Twine::Placeholders.convert_placeholders_from_twine_to_flash(value)
end
def test_replaces_placeholder
assert_equal "some {0} text", to_flash("some #{placeholder} text")
end
def test_replaces_string_placeholder
assert_equal "some {0} text", to_flash("some #{placeholder('@')} text")
end
def test_numbers_placeholders
assert_equal "some {0} more {1} text {2}", to_flash("some #{placeholder('@')} more #{placeholder('@')} text #{placeholder('@')}")
end
end
class FromFlash < PlaceholderTest
def from_flash(value)
Twine::Placeholders.convert_placeholders_from_flash_to_twine(value)
end
def test_maps_all_placeholders_to_string
assert_equal "some %@ more %@ text %@", from_flash("some {0} more {1} text {2}")
end
end
class PythonPlaceholder < PlaceholderTest
def test_negative_for_regular_placeholders
assert_equal false, Twine::Placeholders.contains_python_specific_placeholder(placeholder)
end
def test_positive_for_named_placeholders
inputs = [
"%(language)s has",
"For %(number)03d quotes",
"bought on %(app_name)s"
]
inputs.each do |input|
assert_equal true, Twine::Placeholders.contains_python_specific_placeholder(input)
end
end
end
end

View file

@ -1,58 +0,0 @@
require 'twine_test_case'
class TestStringsFile < TwineTestCase
class Reading < TwineTestCase
def setup
super
@strings = Twine::StringsFile.new
@strings.read fixture_path('twine_accent_values.txt')
end
def test_reading_keeps_leading_accent
assert_equal '`value', @strings.strings_map['value_with_leading_accent'].translations['en']
end
def test_reading_keeps_trailing_accent
assert_equal 'value`', @strings.strings_map['value_with_trailing_accent'].translations['en']
end
def test_reading_keeps_leading_space
assert_equal ' value', @strings.strings_map['value_with_leading_space'].translations['en']
end
def test_reading_keeps_trailing_space
assert_equal 'value ', @strings.strings_map['value_with_trailing_space'].translations['en']
end
def test_reading_keeps_wrapping_spaces
assert_equal ' value ', @strings.strings_map['value_wrapped_by_spaces'].translations['en']
end
def test_reading_keeps_wrapping_accents
assert_equal '`value`', @strings.strings_map['value_wrapped_by_accents'].translations['en']
end
end
class Writing < TwineTestCase
def test_accent_wrapping
@strings = build_twine_file 'en' do
add_section 'Section' do
add_row value_with_leading_accent: '`value'
add_row value_with_trailing_accent: 'value`'
add_row value_with_leading_space: ' value'
add_row value_with_trailing_space: 'value '
add_row value_wrapped_by_spaces: ' value '
add_row value_wrapped_by_accents: '`value`'
end
end
@strings.write @output_path
assert_equal content('twine_accent_values.txt'), File.read(@output_path)
end
end
end

View file

@ -1,47 +0,0 @@
require 'twine_test_case'
class TestStringsRow < TwineTestCase
def setup
super
@reference = Twine::StringsRow.new 'reference-key'
@reference.comment = 'reference comment'
@reference.tags = ['ref1']
@reference.translations['en'] = 'ref-value'
@row = Twine::StringsRow.new 'key'
@row.reference_key = @reference.key
@row.reference = @reference
end
def test_reference_comment_used
assert_equal 'reference comment', @row.comment
end
def test_reference_comment_override
@row.comment = 'row comment'
assert_equal 'row comment', @row.comment
end
def test_reference_tags_used
assert @row.matches_tags?(['ref1'], false)
end
def test_reference_tags_override
@row.tags = ['tag1']
refute @row.matches_tags?(['ref1'], false)
assert @row.matches_tags?(['tag1'], false)
end
def test_reference_translation_used
assert_equal 'ref-value', @row.translated_string_for_lang('en')
end
def test_reference_translation_override
@row.translations['en'] = 'value'
assert_equal 'value', @row.translated_string_for_lang('en')
end
end

View file

@ -0,0 +1,111 @@
require 'twine_test'
class TestTwineDefinition < TwineTest
class TestTags < TwineTest
def setup
super
@definition = Twine::TwineDefinition.new 'key'
end
def test_include_untagged
assert @definition.matches_tags?([[rand(100000).to_s]], true)
end
def test_matches_no_given_tags
assert @definition.matches_tags?([], false)
end
def test_matches_tag
@definition.tags = ['tag1']
assert @definition.matches_tags?([['tag1']], false)
end
def test_matches_any_tag
@definition.tags = ['tag1']
assert @definition.matches_tags?([['tag0', 'tag1', 'tag2']], false)
end
def test_matches_all_tags
@definition.tags = ['tag1', 'tag2']
assert @definition.matches_tags?([['tag1'], ['tag2']], false)
end
def test_does_not_match_all_tags
@definition.tags = ['tag1']
refute @definition.matches_tags?([['tag1'], ['tag2']], false)
end
def test_does_not_match_excluded_tag
@definition.tags = ['tag1']
refute @definition.matches_tags?([['~tag1']], false)
end
def test_matches_excluded_tag
@definition.tags = ['tag2']
assert @definition.matches_tags?([['~tag1']], false)
end
def test_complex_rules
@definition.tags = ['tag1', 'tag2', 'tag3']
assert @definition.matches_tags?([['tag1']], false)
assert @definition.matches_tags?([['tag1', 'tag4']], false)
assert @definition.matches_tags?([['tag1'], ['tag2'], ['tag3']], false)
refute @definition.matches_tags?([['tag1'], ['tag4']], false)
assert @definition.matches_tags?([['tag4', '~tag5']], false)
end
end
class TestReferences < TwineTest
def setup
super
@reference = Twine::TwineDefinition.new 'reference-key'
@reference.comment = 'reference comment'
@reference.tags = ['ref1']
@reference.translations['en'] = 'ref-value'
@definition = Twine::TwineDefinition.new 'key'
@definition.reference_key = @reference.key
@definition.reference = @reference
end
def test_reference_comment_used
assert_equal 'reference comment', @definition.comment
end
def test_reference_comment_override
@definition.comment = 'definition comment'
assert_equal 'definition comment', @definition.comment
end
def test_reference_tags_used
assert @definition.matches_tags?([['ref1']], false)
end
def test_reference_tags_override
@definition.tags = ['tag1']
refute @definition.matches_tags?([['ref1']], false)
assert @definition.matches_tags?([['tag1']], false)
end
def test_reference_translation_used
assert_equal 'ref-value', @definition.translation_for_lang('en')
end
def test_reference_translation_override
@definition.translations['en'] = 'value'
assert_equal 'value', @definition.translation_for_lang('en')
end
end
end

58
test/test_twine_file.rb Normal file
View file

@ -0,0 +1,58 @@
require 'twine_test'
class TestTwineFile < TwineTest
class Reading < TwineTest
def setup
super
@twine_file = Twine::TwineFile.new
@twine_file.read fixture_path('twine_accent_values.txt')
end
def test_reading_keeps_leading_accent
assert_equal '`value', @twine_file.definitions_by_key['value_with_leading_accent'].translations['en']
end
def test_reading_keeps_trailing_accent
assert_equal 'value`', @twine_file.definitions_by_key['value_with_trailing_accent'].translations['en']
end
def test_reading_keeps_leading_space
assert_equal ' value', @twine_file.definitions_by_key['value_with_leading_space'].translations['en']
end
def test_reading_keeps_trailing_space
assert_equal 'value ', @twine_file.definitions_by_key['value_with_trailing_space'].translations['en']
end
def test_reading_keeps_wrapping_spaces
assert_equal ' value ', @twine_file.definitions_by_key['value_wrapped_by_spaces'].translations['en']
end
def test_reading_keeps_wrapping_accents
assert_equal '`value`', @twine_file.definitions_by_key['value_wrapped_by_accents'].translations['en']
end
end
class Writing < TwineTest
def test_accent_wrapping
@twine_file = build_twine_file 'en' do
add_section 'Section' do
add_definition value_with_leading_accent: '`value'
add_definition value_with_trailing_accent: 'value`'
add_definition value_with_leading_space: ' value'
add_definition value_with_trailing_space: 'value '
add_definition value_wrapped_by_spaces: ' value '
add_definition value_wrapped_by_accents: '`value`'
end
end
@twine_file.write @output_path
assert_equal content('twine_accent_values.txt'), File.read(@output_path)
end
end
end

View file

@ -1,61 +0,0 @@
# encoding: utf-8
require 'command_test_case'
class TestValidateStringsFile < CommandTestCase
def setup
super
@options = { strings_file: 'input.txt' }
@twine_file = build_twine_file 'en' do
add_section 'Section 1' do
add_row key1: 'value1', tags: ['tag1']
add_row key2: 'value2', tags: ['tag1']
end
add_section 'Section 2' do
add_row key3: 'value3', tags: ['tag1', 'tag2']
add_row key4: 'value4', tags: ['tag2']
end
end
end
def random_row
@twine_file.strings_map[@twine_file.strings_map.keys.sample]
end
def test_recognizes_valid_file
Twine::Runner.new(@options, @twine_file).validate_strings_file
assert_equal "input.txt is valid.\n", Twine::stdout.string
end
def test_reports_duplicate_keys
@twine_file.sections[0].rows << random_row
assert_raises Twine::Error do
Twine::Runner.new(@options, @twine_file).validate_strings_file
end
end
def test_reports_invalid_characters_in_keys
random_row.key[0] = "!?;:,^`´'\"\\|/(){}[]~-+*=#$%".chars.to_a.sample
assert_raises Twine::Error do
Twine::Runner.new(@options, @twine_file).validate_strings_file
end
end
def test_does_not_reports_missing_tags_by_default
random_row.tags.clear
Twine::Runner.new(@options, @twine_file).validate_strings_file
end
def test_reports_missing_tags
random_row.tags.clear
assert_raises Twine::Error do
Twine::Runner.new(@options.merge(pedantic: true), @twine_file).validate_strings_file
end
end
end

View file

@ -0,0 +1,69 @@
# encoding: utf-8
require 'command_test'
class TestValidateTwineFile < CommandTest
def setup
super
@options = { twine_file: 'input.txt' }
@twine_file = build_twine_file 'en' do
add_section 'Section 1' do
add_definition key1: 'value1', tags: ['tag1']
add_definition key2: 'value2', tags: ['tag1']
end
add_section 'Section 2' do
add_definition key3: 'value3', tags: ['tag1', 'tag2']
add_definition key4: 'value4', tags: ['tag2']
end
end
end
def random_definition
@twine_file.definitions_by_key[@twine_file.definitions_by_key.keys.sample]
end
def test_recognizes_valid_file
Twine::Runner.new(@options, @twine_file).validate_twine_file
assert_equal "input.txt is valid.\n", Twine::stdout.string
end
def test_reports_duplicate_keys
@twine_file.sections[0].definitions << random_definition
assert_raises Twine::Error do
Twine::Runner.new(@options, @twine_file).validate_twine_file
end
end
def test_reports_invalid_characters_in_keys
random_definition.key[0] = "!?;:,^`´'\"\\|/(){}[]~-+*=#$%".chars.to_a.sample
assert_raises Twine::Error do
Twine::Runner.new(@options, @twine_file).validate_twine_file
end
end
def test_does_not_reports_missing_tags_by_default
random_definition.tags.clear
Twine::Runner.new(@options, @twine_file).validate_twine_file
end
def test_reports_missing_tags
random_definition.tags.clear
assert_raises Twine::Error do
Twine::Runner.new(@options.merge(pedantic: true), @twine_file).validate_twine_file
end
end
def test_reports_python_specific_placeholders
random_definition.translations["en"] = "%(python_only)s"
assert_raises Twine::Error do
Twine::Runner.new(@options, @twine_file).validate_twine_file
end
end
end

View file

@ -1,6 +1,6 @@
module TwineFileDSL
def build_twine_file(*languages)
@currently_built_twine_file = Twine::StringsFile.new
@currently_built_twine_file = Twine::TwineFile.new
@currently_built_twine_file.language_codes.concat languages
yield
result = @currently_built_twine_file
@ -10,37 +10,37 @@ module TwineFileDSL
def add_section(name)
return unless @currently_built_twine_file
@currently_built_twine_file_section = Twine::StringsSection.new name
@currently_built_twine_file_section = Twine::TwineSection.new name
@currently_built_twine_file.sections << @currently_built_twine_file_section
yield
@currently_built_twine_file_section = nil
end
def add_row(parameters)
def add_definition(parameters)
return unless @currently_built_twine_file
return unless @currently_built_twine_file_section
# this relies on Ruby preserving the order of hash elements
key, value = parameters.first
row = Twine::StringsRow.new(key.to_s)
definition = Twine::TwineDefinition.new(key.to_s)
if value.is_a? Hash
value.each do |language, translation|
row.translations[language.to_s] = translation
definition.translations[language.to_s] = translation
end
elsif !value.is_a? Symbol
language = @currently_built_twine_file.language_codes.first
row.translations[language] = value
definition.translations[language] = value
end
row.comment = parameters[:comment] if parameters[:comment]
row.tags = parameters[:tags] if parameters[:tags]
definition.comment = parameters[:comment] if parameters[:comment]
definition.tags = parameters[:tags] if parameters[:tags]
if parameters[:ref] || value.is_a?(Symbol)
reference_key = (parameters[:ref] || value).to_s
row.reference_key = reference_key
row.reference = @currently_built_twine_file.strings_map[reference_key]
definition.reference_key = reference_key
definition.reference = @currently_built_twine_file.definitions_by_key[reference_key]
end
@currently_built_twine_file_section.rows << row
@currently_built_twine_file.strings_map[row.key] = row
@currently_built_twine_file_section.definitions << definition
@currently_built_twine_file.definitions_by_key[definition.key] = definition
end
end

View file

@ -1,12 +1,12 @@
require 'erb'
require 'minitest/autorun'
require "mocha/mini_test"
require "mocha/minitest"
require 'securerandom'
require 'stringio'
require 'twine'
require 'twine_file_dsl'
class TwineTestCase < Minitest::Test
class TwineTest < Minitest::Test
include TwineFileDSL
KNOWN_LANGUAGES = %w(en fr de es)
@ -23,14 +23,14 @@ class TwineTestCase < Minitest::Test
end
def teardown
FileUtils.remove_entry_secure @output_dir if File.exists? @output_dir
FileUtils.remove_entry_secure @output_dir if File.exist? @output_dir
Twine::Formatters.formatters.clear
Twine::Formatters.formatters.concat @formatters
super
end
def execute(command)
command += " -o #{@output_path}"
command += " -o #{@output_path}"
Twine::Runner.run(command.split(" "))
end

2
twine
View file

@ -1,3 +1,3 @@
#!/bin/sh
BASEDIR=$(dirname $0)
ruby -rubygems -I $BASEDIR/lib $BASEDIR/bin/twine $@
ruby -rrubygems -I $BASEDIR/lib $BASEDIR/bin/twine "$@"

View file

@ -5,8 +5,8 @@ Gem::Specification.new do |s|
s.name = "twine"
s.version = Twine::VERSION
s.date = Time.now.strftime('%Y-%m-%d')
s.summary = "Manage strings and their translations for your iOS and Android projects."
s.homepage = "https://github.com/mobiata/twine"
s.summary = "Manage strings and their translations for your iOS, Android and other projects."
s.homepage = "https://github.com/scelis/twine"
s.email = "twine@mobiata.com"
s.authors = [ "Sebastian Celis" ]
s.has_rdoc = false
@ -18,11 +18,13 @@ Gem::Specification.new do |s|
s.files += Dir.glob("test/**/*")
s.test_files = Dir.glob("test/test_*")
s.required_ruby_version = ">= 2.0"
s.add_runtime_dependency('rubyzip', "~> 1.1")
s.required_ruby_version = ">= 2.6"
s.add_runtime_dependency('rexml', "~> 3.2")
s.add_runtime_dependency('rubyzip', "~> 2.0")
s.add_runtime_dependency('safe_yaml', "~> 1.0")
s.add_development_dependency('rake', "~> 10.4")
s.add_development_dependency('rake', "~> 13.0")
s.add_development_dependency('minitest', "~> 5.5")
s.add_development_dependency('minitest-ci', "~> 3.0")
s.add_development_dependency('mocha', "~> 1.1")
s.executables = %w( twine )