Compare commits

..

160 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
44 changed files with 1077 additions and 264 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)

144
README.md
View file

@ -1,26 +1,13 @@
# Twine
Twine is a command line tool for managing your strings and their translations. These are all stored in a master 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.
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
You can also run Twine directly from source. However, it requires [rubyzip][rubyzip] in order to create and read standard zip files.
$ 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.
## Twine File Format
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.
@ -35,7 +22,7 @@ Twine supports [`printf` style placeholders][printf] with one peculiarity: `@` i
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 defintions 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.
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
@ -48,39 +35,39 @@ If you want a definition to inherit the values of another definition, you can us
### Example
```ini
[[General]]
[yes]
en = Yes
es = Sí
fr = Oui
ja = はい
[no]
en = No
fr = Non
ja = いいえ
[[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
@ -89,6 +76,12 @@ 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)
@ -155,7 +148,7 @@ This command validates that the Twine data file can be parsed, contains no dupli
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 twine.txt
$ twine consume-all-localization-files twine.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 --format apple/android/gettext/jquery/django/tizen/flash
## Twine and Your Build Process
@ -179,24 +172,62 @@ 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 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
}
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 localization files 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 files. In particular, it lets you use code folding to easily collapse and expand both definitions 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.
## Extending Twine
@ -222,8 +253,9 @@ 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/

View file

@ -172,7 +172,7 @@
\f0\fs36 \cf0 /**\
* Apple Strings File\
* Generated by Twine 0.8.1\
* Generated by Twine\
* Language: en\
*/\
\

View file

@ -30,12 +30,6 @@ If we map the _input_ for each method to the example file, it looks like this
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
```
/**
* Apple Strings File
* Generated by Twine 0.8.1
* Language: en
*/
/********** General **********/
"yes" = "Yes";
@ -61,7 +55,7 @@ Formatters inherit from [`Abstract`](/lib/twine/formatters/abstract.rb) and need
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 [bundeled formatters](/lib/twine/formatters) will provide some insight.
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
@ -83,8 +77,13 @@ Plugins are specified as values for the `gems` key. The following is an example
gems: wicked_twine
```
Multiple gems can also be specfied in the yaml file.
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

@ -31,6 +31,7 @@ module Twine
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'

View file

@ -22,7 +22,7 @@ module Twine
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.
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
@ -45,6 +45,14 @@ module Twine
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.'
@ -78,6 +86,10 @@ module Twine
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,
@ -107,9 +119,11 @@ module Twine
optional_options: [
:developer_language,
:encoding,
:escape_all_tags,
:format,
:include,
:languages,
:quiet,
:tags,
:untagged,
:validate
@ -128,9 +142,11 @@ module Twine
:create_folders,
:developer_language,
:encoding,
:escape_all_tags,
:file_name,
:format,
:include,
:quiet,
:tags,
:untagged,
:validate
@ -146,7 +162,9 @@ module Twine
optional_options: [
:developer_language,
:encoding,
:escape_all_tags,
:include,
:quiet,
:tags,
:untagged,
:validate
@ -164,6 +182,7 @@ module Twine
:format,
:languages,
:output_path,
:quiet,
:tags
],
option_validation: Proc.new { |options|
@ -183,6 +202,7 @@ module Twine
:encoding,
:format,
:output_path,
:quiet,
:tags
],
example: 'twine consume-all-localization-files twine.txt Resources/Locales/ --developer-language en --tags DefaultTag1,DefaultTag2'
@ -197,6 +217,7 @@ module Twine
:encoding,
:format,
:output_path,
:quiet,
:tags
],
example: 'twine consume-localization-archive twine.txt LocDrop5.zip'
@ -206,7 +227,8 @@ module Twine
arguments: [:twine_file],
optional_options: [
:developer_language,
:pedantic
:pedantic,
:quiet
],
example: 'twine validate-twine-file twine.txt'
}
@ -220,21 +242,25 @@ module Twine
command = args.select { |a| a[0] != '-' }[0]
args = args.reject { |a| a == command }
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::stderr.puts "WARNING: Twine commands names have changed. `#{command}` is now `#{mapped_command}`. The old command is deprecated will soon stop working. For more information please check the documentation at https://github.com/mobiata/twine"
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
unless COMMANDS.keys.include? command
Twine::stderr.puts "Invalid command: #{command}" unless command.nil?
if command.nil?
print_help(args)
abort
return false
elsif not COMMANDS.keys.include? command
raise Twine::Error.new "Invalid command: #{command}"
end
options = parse_command_options(command, args)
return options
parse_command_options(command, args)
end
private
@ -306,8 +332,13 @@ module Twine
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 = IO.console.winsize[1] - summary_width
max_description_width = console_width - summary_width
merged_lines.map! do |line|
if line[0] == ' '
line = word_wrap(line.strip, max_description_width - 2)
@ -347,8 +378,6 @@ module Twine
parser.define(*option[:switch]) do |value|
if option[:repeated]
result[option_name] = (result[option_name] || []) << value
elsif option[:boolean]
result[option_name] = true
else
result[option_name] = value
end
@ -358,8 +387,8 @@ module Twine
end
parser.define('-h', '--help', 'Show this message.') do
puts parser.help
exit
Twine::stdout.puts parser.help
return false
end
parser.separator ''

View file

@ -3,6 +3,9 @@ require 'fileutils'
module Twine
module Formatters
class Abstract
SUPPORTS_PLURAL = false
LANGUAGE_CODE_WITH_OPTIONAL_REGION_CODE = "[a-z]{2}(?:-[A-Za-z]{2})?"
attr_accessor :twine_file
attr_accessor :options
@ -38,7 +41,7 @@ module Twine
definition.translations[lang] = value
end
elsif @options[:consume_all]
Twine::stderr.puts "Adding new definition '#{key}' to twine file."
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 = TwineSection.new('Uncategorized')
@ -54,7 +57,7 @@ module Twine
@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 twine file."
Twine::stdout.puts "WARNING: '#{key}' not found in twine file."
end
if !@twine_file.language_codes.include?(lang)
@twine_file.add_language_code(lang)
@ -76,7 +79,12 @@ module Twine
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)
@ -132,7 +140,13 @@ module Twine
end
def format_definition(definition, lang)
[format_comment(definition, lang), format_key_value(definition, lang)].compact.join
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(definition, lang)
@ -143,10 +157,21 @@ module Twine
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,6 +7,17 @@ module Twine
class Android < Abstract
include Twine::Placeholders
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',
'ji' => 'yi'
]
def format_name
'android'
end
@ -32,16 +43,23 @@ module Twine
# 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)
return match[1].sub('-r', '-') if match
if match
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}"
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)
@ -56,18 +74,20 @@ module Twine
def read(io, lang)
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'
key = child.attributes['name']
set_translation_for_key(key, lang, child.text)
content = child.children.map(&:to_s).join
set_translation_for_key(key, lang, content)
set_comment_for_key(key, comment) if comment
comment = nil
@ -76,7 +96,7 @@ module Twine
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(twine_file, lang)
@ -88,23 +108,59 @@ module Twine
end
def format_section_header(section)
"\t<!-- SECTION: #{section.name} -->"
"#{space(4)}<!-- SECTION: #{section.name} -->"
end
def format_comment(definition, lang)
"\t<!-- #{definition.comment.gsub('--', '—')} -->\n" if definition.comment
"#{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_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)
# escape double and single quotes, & signs and tags
value = escape_quotes(value)
value.gsub!("'", "\\\\'")
value.gsub!(/&/, '&amp;')
value.gsub!('<', '&lt;')
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>

View file

@ -1,6 +1,8 @@
module Twine
module Formatters
class Apple < Abstract
include Twine::Placeholders
def format_name
'apple'
end
@ -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(definition, lang)
"/* #{definition.comment.gsub('*/', '* /')} */\n" if definition.comment
"\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'
@ -13,22 +14,11 @@ module Twine
'strings.po'
end
def determine_language_given_path(path)
path_arr = path.split(File::SEPARATOR)
path_arr.each do |segment|
match = /(..)\.po$/.match(segment)
return match[1] if match
end
return
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
@ -64,11 +54,12 @@ module Twine
end
def format_header(lang)
"##\n # Django Strings File\n # Generated by Twine #{Twine::VERSION}\n # Language: #{lang}\nmsgid \"\"\nmsgstr \"\"\n\"Content-Type: text/plain; charset=UTF-8\\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_definition(definition, lang)

View file

@ -15,11 +15,6 @@ module Twine
'resources.properties'
end
def determine_language_given_path(path)
# match two-letter language code, optionally followed by a two letter region code
path.split(File::SEPARATOR).reverse.find { |segment| segment =~ /^([a-z]{2}(-[a-z]{2})?)$/i }
end
def set_translation_for_key(key, lang, value)
value = convert_placeholders_from_flash_to_twine(value)
super(key, lang, value)
@ -47,7 +42,7 @@ module Twine
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)

View file

@ -15,18 +15,6 @@ module Twine
'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
end
def read(io, lang)
comment_regex = /#.? *"(.*)"$/
key_regex = /msgctxt *"(.*)"$/
@ -67,7 +55,7 @@ module Twine
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)
@ -88,15 +76,15 @@ module Twine
end
def format_key(key)
"msgctxt \"#{key}\"\n"
"msgctxt \"#{escape_quotes(key)}\"\n"
end
def format_base_translation(definition)
"msgid \"#{definition.translations[@default_lang]}\"\n"
"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

@ -14,22 +14,17 @@ module Twine
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)
@ -46,7 +41,7 @@ module Twine
def format_sections(twine_file, lang)
sections = twine_file.sections.map { |section| format_section(section, lang) }
sections.delete_if &:empty?
sections.delete_if(&:empty?)
sections.join(",\n\n")
end

View file

@ -91,7 +91,7 @@ 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(twine_file, lang)

View file

@ -13,10 +13,16 @@ module Twine
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)
@ -42,6 +48,14 @@ module Twine
new_definition = definition.dup
new_definition.translations[language] = value
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

View file

@ -3,9 +3,14 @@ module Twine
extend self
# Note: the ` ` (single space) flag is NOT supported
PLACEHOLDER_FLAGS_WIDTH_PRECISION_LENGTH = '([-+0#])?(\d+|\*)?(\.(\d+|\*))?(hh?|ll?|L|z|j|t)?'
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
@ -19,15 +24,13 @@ module Twine
# %@ -> %s
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%%'
@ -61,8 +64,7 @@ module Twine
def convert_placeholders_from_twine_to_flash(input)
value = convert_twine_string_placeholder(input)
placeholder_regex = /%#{PLACEHOLDER_PARAMETER_FLAGS_WIDTH_PRECISION_LENGTH}#{PLACEHOLDER_TYPES}/
value.gsub(placeholder_regex).each_with_index do |match, index|
value.gsub(PLACEHOLDER_REGEX).each_with_index do |match, index|
"{#{index}}"
end
end
@ -70,5 +72,11 @@ module Twine
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,8 +5,18 @@ 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
twine_file = TwineFile.new
twine_file.read options[:twine_file]
@ -33,6 +43,9 @@ module Twine
def initialize(options = {}, twine_file = TwineFile.new)
@options = options
@twine_file = twine_file
if @options[:quite]
Twine::stdout = NullOutput.new
end
end
def write_twine_data(path)
@ -67,11 +80,14 @@ 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
@ -85,7 +101,7 @@ module Twine
output = formatter.format_file(lang)
unless output
Twine::stderr.puts "Skipping file at path #{file_path} since it would not contain any translations."
Twine::stdout.puts "Skipping file at path #{file_path} since it would not contain any translations."
next
end
@ -107,7 +123,7 @@ 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 translations."
Twine::stdout.puts "Skipping file at path #{file_path} since it would not contain any translations."
next
end
@ -143,7 +159,7 @@ module Twine
output = formatter.format_file(lang)
unless output
Twine::stderr.puts "Skipping file #{file_name} since it would not contain any translations."
Twine::stdout.puts "Skipping file #{file_name} since it would not contain any translations."
next
end
@ -192,6 +208,7 @@ module Twine
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|
@ -204,6 +221,7 @@ module Twine
read_localization_file(real_path)
rescue Twine::Error => e
Twine::stderr.puts "#{e.message}"
error_encountered = true
end
end
end
@ -211,6 +229,10 @@ module Twine
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_twine_file
@ -219,6 +241,7 @@ module Twine
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_]+$/
@twine_file.sections.each do |section|
@ -231,6 +254,8 @@ module Twine
keys_without_tags.add(definition.key) if definition.tags == nil or definition.tags.length == 0
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
@ -253,6 +278,10 @@ 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[:twine_file]} is valid."
@ -272,18 +301,18 @@ module Twine
end
end
def determine_language_given_path(path)
code = File.basename(path, File.extname(path))
return code if @twine_file.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
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
@ -305,16 +334,19 @@ module Twine
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
@twine_file.language_codes << lang unless @twine_file.language_codes.include? lang

View file

@ -1,9 +1,13 @@
module Twine
class TwineDefinition
PLURAL_KEYS = %w(zero one two few many other)
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
@ -12,6 +16,7 @@ module Twine
@comment = nil
@tags = nil
@translations = {}
@plural_translations = {}
end
def comment
@ -44,12 +49,22 @@ module Twine
end
def translation_for_lang(lang)
translation = [lang].flatten.map { |l| @translations[l] }.first
translation = [lang].flatten.map { |l| @translations[l] }.compact.first
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 TwineSection
@ -137,11 +152,12 @@ module Twine
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
@ -155,7 +171,18 @@ module Twine
if !@language_codes.include? key
add_language_code(key)
end
current_definition.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
@ -190,7 +217,7 @@ module Twine
value = write_value(definition, dev_lang, f)
if !value && !definition.reference_key
puts "Warning: #{definition.key} does not exist in developer language '#{dev_lang}'"
Twine::stdout.puts "WARNING: #{definition.key} does not exist in developer language '#{dev_lang}'"
end
if definition.reference_key

View file

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

View file

@ -1,13 +1,13 @@
require 'twine_test'
class CommandTest < TwineTest
def prepare_mock_formatter(formatter_class)
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
Twine::Formatters.formatters.clear if clear_other_formatters
Twine::Formatters.formatters << formatter
formatter
end

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,12 +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"
@ -18,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

@ -17,14 +17,24 @@ class CLITest < TwineTest
raise "you need to implement `parse_with` in your test class"
end
def assert_help
parse_with '--help'
assert_equal @options, false
assert_match /Usage: twine.*Examples:/m, Twine::stdout.string
end
def assert_option_consume_all
parse_with '--consume-all'
assert @options[:consume_all]
parse_with '--no-consume-all'
refute @options[:consume_all]
end
def assert_option_consume_comments
parse_with '--consume-comments'
assert @options[:consume_comments]
parse_with '--no-consume-comments'
refute @options[:consume_comments]
end
def assert_option_developer_language
@ -38,6 +48,13 @@ class CLITest < TwineTest
assert_equal 'UTF16', @options[:encoding]
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
def assert_option_format
random_format = Twine::Formatters.formatters.sample.format_name.downcase
parse_with "--format #{random_format}"
@ -72,6 +89,13 @@ class CLITest < TwineTest
assert_equal @output_path, @options[:output_path]
end
def assert_option_quiet
parse_with '--quiet'
assert @options[:quiet]
parse_with '--no-quiet'
refute @options[:quiet]
end
def assert_option_tags
# single tag
random_tag = "tag#{rand(100)}"
@ -99,11 +123,35 @@ class CLITest < TwineTest
def assert_option_untagged
parse_with '--untagged'
assert @options[:untagged]
parse_with '--no-untagged'
refute @options[:untagged]
end
def assert_option_validate
parse_with "--validate"
assert @options[:validate]
parse_with "--no-validate"
refute @options[:validate]
end
end
class TestCLI < CLITest
def test_version
parse "--version"
assert_equal @options, false
assert_equal "Twine version #{Twine::VERSION}\n", Twine::stdout.string
end
def test_help
parse ""
assert_match 'Usage: twine', Twine::stdout.string
end
def test_invalid_command
assert_raises Twine::Error do
parse "not a command"
end
end
end
@ -122,7 +170,7 @@ class TestGenerateLocalizationFileCLI < CLITest
def test_missing_argument
assert_raises Twine::Error do
parse "generate-localization-file #{@twine_file}"
parse "generate-localization-file #{@twine_file_path}"
end
end
@ -133,12 +181,15 @@ class TestGenerateLocalizationFileCLI < CLITest
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
@ -171,10 +222,13 @@ class TestGenerateAllLocalizationFilesCLI < CLITest
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
@ -183,6 +237,8 @@ class TestGenerateAllLocalizationFilesCLI < CLITest
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
@ -218,9 +274,12 @@ class TestGenerateLocalizationArchiveCLI < CLITest
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
@ -239,7 +298,7 @@ class TestGenerateLocalizationArchiveCLI < CLITest
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::stderr.string
assert_match "WARNING: Twine commands names have changed.", Twine::stdout.string
end
end
@ -269,6 +328,7 @@ class TestConsumeLocalizationFileCLI < CLITest
end
def test_options
assert_help
assert_option_consume_all
assert_option_consume_comments
assert_option_developer_language
@ -277,6 +337,7 @@ class TestConsumeLocalizationFileCLI < CLITest
assert_option_single_language
assert_raises(Twine::Error) { assert_option_multiple_languages }
assert_option_output_path
assert_option_quiet
assert_option_tags
end
end
@ -307,12 +368,14 @@ class TestConsumeAllLocalizationFilesCLI < CLITest
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
@ -343,12 +406,14 @@ class TestConsumeLocalizationArchiveCLI < CLITest
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
@ -359,7 +424,7 @@ class TestConsumeLocalizationArchiveCLI < CLITest
def test_deprecated_command_prints_warning
parse "consume-loc-drop #{@twine_file_path} #{@input_path}"
assert_match "WARNING: Twine commands names have changed.", Twine::stderr.string
assert_match "WARNING: Twine commands names have changed.", Twine::stdout.string
end
end
@ -388,11 +453,15 @@ class TestValidateTwineFileCLI < CLITest
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

@ -4,24 +4,30 @@ class TestConsumeLocalizationArchive < CommandTest
def setup
super
options = {}
options[:input_path] = fixture_path 'consume_localization_archive.zip'
options[:output_path] = @output_path
options[:format] = 'apple'
@twine_file = build_twine_file 'en', 'es' do
add_section 'Section' do
add_definition key1: 'value1'
end
end
end
@runner = Twine::Runner.new(options, @twine_file)
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
@runner.consume_localization_archive
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

@ -39,20 +39,110 @@ 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>' => '&lt;b>bold&lt;/b>',
'<a href="target">link</a>' => '&lt;a href=\"target\">link&lt;/a>',
'<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
@ -77,6 +167,54 @@ class TestAndroidFormatter < FormatterTest
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', @empty_twine_file.definitions_by_key['key1'].translations['en']
@ -102,6 +240,11 @@ class TestAndroidFormatter < FormatterTest
@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
@ -130,6 +273,11 @@ class TestAndroidFormatter < FormatterTest
@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
@ -141,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
@ -157,6 +321,17 @@ class TestAndroidFormatter < FormatterTest
def test_output_path_is_prefixed
assert_equal 'values-en', @formatter.output_path_for_language('en')
end
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
class TestAppleFormatter < FormatterTest
@ -170,6 +345,32 @@ 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', @empty_twine_file.definitions_by_key['key'].translations['en']
@ -264,10 +465,24 @@ class TestJQueryFormatter < FormatterTest
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
@ -290,6 +505,25 @@ class TestGettextFormatter < FormatterTest
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
@ -310,7 +544,6 @@ class TestTizenFormatter < FormatterTest
formatter.twine_file = @twine_file
assert_equal content('formatter_tizen.xml'), formatter.format_file('en')
end
end
class TestDjangoFormatter < FormatterTest
@ -329,6 +562,29 @@ class TestDjangoFormatter < FormatterTest
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
@ -359,10 +615,10 @@ class TestFlashFormatter < FormatterTest
def test_deducts_language_from_resource_folder
language = %w(en de fr).sample
assert_equal language, @formatter.determine_language_given_path("locale/#{language}")
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")
assert_equal 'de-AT', @formatter.determine_language_given_path("locale/de-AT/#{@formatter.default_file_name}")
end
end

View file

@ -1,11 +1,13 @@
require 'command_test'
class TestGenerateAllLocalizationFiles < CommandTest
def new_runner(create_folders, twine_file = nil)
options = {}
options[:output_path] = @output_dir
options[:format] = 'apple'
options[:create_folders] = create_folders
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
@ -18,9 +20,31 @@ class TestGenerateAllLocalizationFiles < CommandTest
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)
super(false, twine_file)
def new_runner(twine_file = nil, options = {})
super(false, twine_file, options)
end
def test_fails_if_output_folder_does_not_exist
@ -32,39 +56,53 @@ class TestGenerateAllLocalizationFiles < CommandTest
def test_does_not_create_language_folders
Dir.mkdir File.join @output_dir, 'en.lproj'
new_runner.generate_all_localization_files
refute File.exists?(File.join(@output_dir, 'es.lproj')), "language folder should not be created"
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::stderr.string
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)
super(true, twine_file)
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.exists? @output_dir
assert File.exist? @output_dir
end
def test_creates_language_folders
new_runner.generate_all_localization_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"
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::stderr.string
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

View file

@ -1,8 +1,7 @@
require 'command_test'
class TestGenerateLocalizationArchive < CommandTest
def new_runner(twine_file = nil)
options = {}
def new_runner(twine_file = nil, options = {})
options[:output_path] = @output_path
options[:format] = 'apple'
@ -20,7 +19,7 @@ class TestGenerateLocalizationArchive < CommandTest
def test_generates_zip_file
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
@ -45,7 +44,13 @@ class TestGenerateLocalizationArchive < CommandTest
def test_prints_empty_file_warnings
empty_twine_file = build_twine_file('en') {}
new_runner(empty_twine_file).generate_localization_archive
assert_match "Skipping file", Twine::stderr.string
assert_match "Skipping file", 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_localization_archive
refute_match "Skipping file", Twine::stdout.string
end
class TestValidate < CommandTest

View file

@ -1,8 +1,7 @@
require 'command_test'
class TestGenerateLocalizationFile < CommandTest
def new_runner(language, file)
options = {}
def new_runner(language, file, options = {})
options[:output_path] = File.join(@output_dir, file) if file
options[:languages] = language if language
@ -41,6 +40,35 @@ class TestGenerateLocalizationFile < CommandTest
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

View file

@ -79,6 +79,10 @@ class PlaceholderTest < TwineTest
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 < PlaceholderTest
@ -118,4 +122,21 @@ class PlaceholderTest < TwineTest
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

@ -58,4 +58,12 @@ class TestValidateTwineFile < CommandTest
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 @@
require 'erb'
require 'minitest/autorun'
require "mocha/mini_test"
require "mocha/minitest"
require 'securerandom'
require 'stringio'
require 'twine'
@ -23,14 +23,14 @@ class TwineTest < 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

@ -6,7 +6,7 @@ Gem::Specification.new do |s|
s.version = Twine::VERSION
s.date = Time.now.strftime('%Y-%m-%d')
s.summary = "Manage strings and their translations for your iOS, Android and other projects."
s.homepage = "https://github.com/mobiata/twine"
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 )