Compare commits

...

101 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
41 changed files with 777 additions and 219 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

View file

@ -1,3 +1,34 @@
# 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)

View file

@ -1,8 +1,6 @@
# Twine
[![Continuous Integration by CircleCI](https://circleci.com/gh/teespring/twine.svg?style=shield)](https://circleci.com/gh/teespring/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
@ -24,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
@ -80,8 +78,8 @@ Twine currently supports the following output formats:
* [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] with `<b>`, `<i>`, `<u>` and `<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 programatically.
* 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)
@ -150,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
@ -174,7 +172,10 @@ 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'
@ -183,10 +184,46 @@ task generateLocalizations {
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
@ -224,4 +261,4 @@ Many thanks to all of the contributors to the Twine project, including:
[djangopo]: https://docs.djangoproject.com/en/dev/topics/i18n/translation/
[tizen]: https://developer.tizen.org/documentation/articles/localization
[flash]: http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/mx/resources/IResourceManager.html#getString()
[printf]: https://en.wikipedia.org/wiki/Printf_format_string
[printf]: https://en.wikipedia.org/wiki/Printf_format_string

View file

@ -1,28 +0,0 @@
machine:
environment:
BUNDLE_INSTALL_PATH: "./vendor/bundle" # circle caches this by default
TEST_RUBIES: "system 2.1 2.2 2.3"
xcode:
version: "8.2"
dependencies:
override:
- >
for v in $TEST_RUBIES; do
echo
echo "****************************************"
echo "Installing gems on Ruby version: $v"
echo "****************************************"
chruby-exec $v -- bundle install --path $BUNDLE_INSTALL_PATH
done
test:
override:
- >
for v in $TEST_RUBIES; do
echo
echo "*******************************"
echo "Testing on Ruby version: $v"
echo "********************************"
chruby-exec $v -- bundle exec rake test TESTOPTS="--ci-dir=$CIRCLE_TEST_REPORTS/reports"
done

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

@ -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'
}
@ -227,7 +249,7 @@ module Twine
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 and 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

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
@ -33,15 +44,22 @@ module Twine
# 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}".gsub(/-(\p{Lu})/, '-r\1')
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,15 +108,25 @@ 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)
@ -108,24 +138,29 @@ module Twine
# http://developer.android.com/guide/topics/resources/string-resource.html#FormattingAndStyling
def escape_value(value)
inside_cdata = /<\!\[CDATA\[((?!\]\]>).)*$/ # opening CDATA tag ('<![CDATA[') not followed by a closing tag (']]>')
inside_opening_anchor_tag = /<a\s?((?!>).)*$/ # anchor tag start ('<a ') not followed by a '>'
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_anchor_tag }
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_anchor_tag }
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
angle_bracket = /<(?!(\/?(\!\[CDATA)))/ # matches all `<` but <![CDATA
else
angle_bracket = /<(?!(\/?(b|u|i|a|\!\[CDATA)))/ # matches all `<` but <b>, <u>, <i>, <a> and <![CDATA
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
@ -30,7 +32,7 @@ module Twine
end
end
return
return super
end
def output_path_for_language(lang)
@ -63,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)
@ -84,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 = /([a-z]{2}(-[A-Za-z]{2})?)\.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,16 +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 = /([a-z]{2}(-[A-Za-z]{2})?)\.po$/.match(segment)
return match[1] if match
end
return
end
def read(io, lang)
comment_regex = /#.? *"(.*)"$/
key_regex = /msgctxt *"(.*)"$/
@ -65,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)
@ -86,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

@ -72,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

@ -57,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,6 +5,14 @@ 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)
@ -35,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)
@ -76,7 +87,7 @@ module Twine
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
@ -90,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
@ -112,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
@ -148,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
@ -197,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|
@ -209,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
@ -216,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
@ -224,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|
@ -236,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
@ -258,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."
@ -277,21 +301,16 @@ 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)
formatters = Formatters.formatters.select &block
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'")
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
@ -322,12 +341,12 @@ module Twine
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 = '1.0.3'
VERSION = '1.1.2-om'
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

@ -48,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}"
@ -82,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)}"
@ -156,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
@ -170,10 +184,12 @@ class TestGenerateLocalizationFileCLI < CLITest
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
@ -209,8 +225,10 @@ class TestGenerateAllLocalizationFilesCLI < CLITest
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
@ -259,7 +277,9 @@ class TestGenerateLocalizationArchiveCLI < CLITest
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
@ -278,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
@ -317,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
@ -354,6 +375,7 @@ class TestConsumeAllLocalizationFilesCLI < CLITest
assert_option_encoding
assert_option_format
assert_option_output_path
assert_option_quiet
assert_option_tags
end
end
@ -391,6 +413,7 @@ class TestConsumeLocalizationArchiveCLI < CLITest
assert_option_encoding
assert_option_format
assert_option_output_path
assert_option_quiet
assert_option_tags
end
@ -401,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
@ -432,6 +455,7 @@ class TestValidateTwineFileCLI < CLITest
def test_options
assert_help
assert_option_developer_language
assert_option_quiet
end
def test_option_pedantic

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

@ -47,26 +47,87 @@ class TestAndroidFormatter < FormatterTest
'a "good" way' => 'a \"good\" way',
'<b>bold</b>' => '<b>bold</b>',
'<em>bold</em>' => '<em>bold</em>',
'<i>italic</i>' => '<i>italic</i>',
'<cite>italic</cite>' => '<cite>italic</cite>',
'<dfn>italic</dfn>' => '<dfn>italic</dfn>',
'<big>larger</big>' => '<big>larger</big>',
'<small>smaller</small>' => '<small>smaller</small>',
'<font color="#45C1D0">F</font>' => '<font color="#45C1D0">F</font>',
'<tt>monospaced</tt>' => '<tt>monospaced</tt>',
'<s>strike</s>' => '<s>strike</s>',
'<strike>strike</strike>' => '<strike>strike</strike>',
'<del>strike</del>' => '<del>strike</del>',
'<u>underline</u>' => '<u>underline</u>',
'<super>superscript</super>'=> '<super>superscript</super>',
'<sub>subscript</sub>' => '<sub>subscript</sub>',
'<ul>bullet point</ul>' => '<ul>bullet point</ul>',
'<li>bullet point</li>' => '<li>bullet point</li>',
'<br>line break' => '<br>line break',
'<div>division</div>' => '<div>division</div>',
'<span style="color:#45C1D0">inline</span>' => '<span style="color:#45C1D0">inline</span>',
'<p>para</p>' => '<p>para</p>',
'<p dir="ltr">para</p>' => '<p dir="ltr">para</p>',
'<b>%@</b>' => '&lt;b>%s&lt;/b>',
'<em>%@</em>' => '&lt;em>%s&lt;/em>',
'<i>%@</i>' => '&lt;i>%s&lt;/i>',
'<cite>%@</cite>' => '&lt;cite>%s&lt;/cite>',
'<dfn>%@</dfn>' => '&lt;dfn>%s&lt;/dfn>',
'<big>%@</big>' => '&lt;big>%s&lt;/big>',
'<small>%@</small>' => '&lt;small>%s&lt;/small>',
'<font color="#45C1D0>%@</font>' => '&lt;font color="#45C1D0>%s&lt;/font>',
'<tt>%@</tt>' => '&lt;tt>%s&lt;/tt>',
'<s>%@</s>' => '&lt;s>%s&lt;/s>',
'<strike>%@</strike>' => '&lt;strike>%s&lt;/strike>',
'<del>%@</del>' => '&lt;del>%s&lt;/del>',
'<u>%@</u>' => '&lt;u>%s&lt;/u>',
'<span>inline</span>' => '&lt;span>inline&lt;/span>',
'<p>paragraph</p>' => '&lt;p>paragraph&lt;/p>',
'<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>',
'<p>escaped</p><![CDATA[]]>' => '&lt;p>escaped&lt;/p><![CDATA[]]>',
'<![CDATA[]]><p>escaped</p>' => '<![CDATA[]]>&lt;p>escaped&lt;/p>',
'<![CDATA[<p>unescaped</p>]]>' => '<![CDATA[<p>unescaped</p>]]>',
'<![CDATA[<p>unescaped with %@</p>]]>' => '<![CDATA[<p>unescaped with %s</p>]]>',
'<![CDATA[]]><![CDATA[<p>unescaped</p>]]>' => '<![CDATA[]]><![CDATA[<p>unescaped</p>]]>',
'<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[\']]>',
@ -77,6 +138,11 @@ class TestAndroidFormatter < FormatterTest
'<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
@ -101,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']
@ -126,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
@ -154,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
@ -165,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
@ -185,6 +325,13 @@ class TestAndroidFormatter < FormatterTest
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
@ -198,6 +345,22 @@ 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")
@ -302,6 +465,21 @@ 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
@ -331,6 +509,21 @@ class TestGettextFormatter < FormatterTest
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
@ -351,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
@ -375,6 +567,24 @@ class TestDjangoFormatter < FormatterTest
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
@ -405,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

@ -24,10 +24,6 @@ class TestGenerateAllLocalizationFiles < CommandTest
def setup
super
Dir.mkdir File.join @output_dir, 'values-en'
# both Android and Tizen can handle folders containing `values-en`
android_formatter = prepare_mock_formatter(Twine::Formatters::Android)
tizen_formatter = prepare_mock_formatter(Twine::Formatters::Tizen, false)
end
def new_runner(options = {})
@ -47,8 +43,8 @@ class TestGenerateAllLocalizationFiles < CommandTest
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
@ -60,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

@ -122,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,7 +23,7 @@ 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

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,10 +18,11 @@ 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")