Compare commits
160 commits
v0.10.0
...
organicmap
Author | SHA1 | Date | |
---|---|---|---|
9ed9c04c53 | |||
|
89dd0a6b35 | ||
|
ebe50c3adc | ||
|
e7215ccba2 | ||
|
1aeee66812 | ||
|
4cfda06f87 | ||
|
9c6143efe5 | ||
|
860c79f3a6 | ||
|
a9a97d19c5 | ||
|
f724a8fe7a | ||
|
b5d723caf5 | ||
|
457c5bbda5 | ||
|
c9bcdcd00b | ||
|
1a140cbdf8 | ||
|
50e7eb95cc | ||
|
6917052e54 | ||
|
55a140a44c | ||
|
a3418dea9c | ||
|
7a6d18559b | ||
|
39aafe4784 | ||
|
ca95b9ed02 | ||
|
b10fe933f5 | ||
|
26c0562936 | ||
|
2a872b8b71 | ||
|
6bf9a2ddde | ||
|
f331423475 | ||
|
d2ec00d57b | ||
|
ccf5f38d6f | ||
|
6cd569fa49 | ||
|
96e0e2c3cd | ||
|
13f5b7b088 | ||
|
649967b71e | ||
|
38f1761ac9 | ||
|
792b2492dc | ||
|
304b3ec63f | ||
|
790e3f7b3e | ||
|
df60ce8e68 | ||
|
7a77a55776 | ||
|
fd326d0029 | ||
|
67a856d10a | ||
|
ad9b8d504b | ||
|
b09c83229d | ||
|
7be9e4549d | ||
|
4d0f6326fb | ||
|
1662f131a9 | ||
|
e220f9cc9e | ||
|
54072398dc | ||
|
4dffd16ec8 | ||
|
6e70c05235 | ||
|
1eb18d8726 | ||
|
edee7bbd11 | ||
|
dc124b6b0e | ||
|
aa99d02fc0 | ||
|
b216a2db01 | ||
|
42895063e1 | ||
|
8566a0b70e | ||
|
9c70cb5638 | ||
|
3fa675dd84 | ||
|
085cdb2585 | ||
|
053aa14d03 | ||
|
59c2f23064 | ||
|
b3b8c395d7 | ||
|
a0fa380a84 | ||
|
5e7a9c9be3 | ||
|
bc4bd7daf0 | ||
|
2e19dccd74 | ||
|
408670e48d | ||
|
a8a9980d2f | ||
|
28825fcf78 | ||
|
692c41460c | ||
|
ac5573ca7a | ||
|
e1ca68f6b9 | ||
|
5a5a263fec | ||
|
97106cdd1f | ||
|
b0474a6f87 | ||
|
62a6f7889d | ||
|
b9135c20ef | ||
|
2cb9639047 | ||
|
5ce4ac934c | ||
|
3b99a796f1 | ||
|
a186d37481 | ||
|
079065da31 | ||
|
68b59b9e0f | ||
|
b1f629061a | ||
|
a0a12ad1c1 | ||
|
0eefb31ba1 | ||
|
cd0735b39d | ||
|
094ba47ac8 | ||
|
9345cdf26e | ||
|
04cb7f66cd | ||
|
394fd019f6 | ||
|
2878050d18 | ||
|
b92ce136cc | ||
|
a5edde0511 | ||
|
890d461eb9 | ||
|
4354775577 | ||
|
5b2ddf3135 | ||
|
d0dc544023 | ||
|
937c713b71 | ||
|
8eccb7fa57 | ||
|
b610e30065 | ||
|
b52de34ce9 | ||
|
01ee6d4eef | ||
|
15006424ed | ||
|
cdec8c2109 | ||
|
5cda6ace82 | ||
|
e72f661b75 | ||
|
c6b3c9c875 | ||
|
72098611ba | ||
|
4891736b1b | ||
|
7a7ca59c2d | ||
|
c2b517707a | ||
|
77a7b49a18 | ||
|
e838dcc8fd | ||
|
b5cd295e3a | ||
|
0688167c59 | ||
|
30ac0d566d | ||
|
6f947b076d | ||
|
bd37ebf582 | ||
|
993557a7e2 | ||
|
4fa59d6205 | ||
|
8e3170ccd7 | ||
|
041fe7d5cb | ||
|
960431ce52 | ||
|
86cf20478b | ||
|
f5af8cf670 | ||
|
21fdd84682 | ||
|
8781528429 | ||
|
3043a48131 | ||
|
922f1a72a1 | ||
|
d621e25750 | ||
|
f7092c7605 | ||
|
9dc3845cae | ||
|
f106e2e272 | ||
|
968d796389 | ||
|
545c106b44 | ||
|
f57c8db096 | ||
|
81f8f15f1d | ||
|
8bda06bf80 | ||
|
1789a59bc1 | ||
|
9d75f104df | ||
|
7f2d75a1ca | ||
|
e1984e06c0 | ||
|
c6a9424f5a | ||
|
cd28717f92 | ||
|
1270ef2767 | ||
|
f7e74392fa | ||
|
a165b98e1e | ||
|
6686a64743 | ||
|
a4f4e4fbb7 | ||
|
8a92e73971 | ||
|
b7405f9b3c | ||
|
7b599e5f92 | ||
|
19b53343c6 | ||
|
e863aa6081 | ||
|
efd637fb1b | ||
|
fc18caa77e | ||
|
fe7e1a7d92 | ||
|
76bcd5becd | ||
|
ae190eb8bd |
44 changed files with 1077 additions and 264 deletions
37
.github/workflows/test.yml
vendored
Normal file
37
.github/workflows/test.yml
vendored
Normal 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
2
.gitignore
vendored
|
@ -1,3 +1,5 @@
|
|||
*.gem
|
||||
.idea/
|
||||
*.lock
|
||||
.ruby-version
|
||||
.DS_Store
|
||||
|
|
59
CHANGELOG.md
Normal file
59
CHANGELOG.md
Normal 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
6
ISSUE_TEMPLATE.md
Normal 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
144
README.md
|
@ -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 `<`
|
||||
* 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/
|
||||
|
|
|
@ -172,7 +172,7 @@
|
|||
|
||||
\f0\fs36 \cf0 /**\
|
||||
* Apple Strings File\
|
||||
* Generated by Twine 0.8.1\
|
||||
* Generated by Twine\
|
||||
* Language: en\
|
||||
*/\
|
||||
\
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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 ''
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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!(/&/, '&')
|
||||
value.gsub!('<', '<')
|
||||
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, /&/, '&') { |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, '<') { |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>
|
||||
|
|
|
@ -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
|
||||
|
|
72
lib/twine/formatters/apple_plural.rb
Normal file
72
lib/twine/formatters/apple_plural.rb
Normal 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
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
module Twine
|
||||
VERSION = '0.10.0'
|
||||
VERSION = '1.1.2-om'
|
||||
end
|
||||
|
|
|
@ -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
|
||||
|
|
BIN
test/fixtures/consume_localization_archive.zip
vendored
BIN
test/fixtures/consume_localization_archive.zip
vendored
Binary file not shown.
19
test/fixtures/formatter_android.xml
vendored
19
test/fixtures/formatter_android.xml
vendored
|
@ -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>
|
||||
|
|
6
test/fixtures/formatter_apple.strings
vendored
6
test/fixtures/formatter_apple.strings
vendored
|
@ -1,9 +1,3 @@
|
|||
/**
|
||||
* Apple Strings File
|
||||
* Generated by Twine <%= Twine::VERSION %>
|
||||
* Language: en
|
||||
*/
|
||||
|
||||
/********** Section 1 **********/
|
||||
|
||||
/* comment key1 */
|
||||
|
|
11
test/fixtures/formatter_django.po
vendored
11
test/fixtures/formatter_django.po
vendored
|
@ -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"
|
||||
|
|
2
test/fixtures/formatter_flash.properties
vendored
2
test/fixtures/formatter_flash.properties
vendored
|
@ -1,5 +1,5 @@
|
|||
## Flash Strings File
|
||||
## Generated by Twine <%= Twine::VERSION %>
|
||||
## Generated by Twine
|
||||
## Language: en
|
||||
|
||||
## Section 1 ##
|
||||
|
|
4
test/fixtures/formatter_gettext.po
vendored
4
test/fixtures/formatter_gettext.po
vendored
|
@ -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
|
||||
|
|
10
test/fixtures/formatter_gettext_quotes.po
vendored
Normal file
10
test/fixtures/formatter_gettext_quotes.po
vendored
Normal 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"
|
2
test/fixtures/formatter_tizen.xml
vendored
2
test/fixtures/formatter_tizen.xml
vendored
|
@ -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 -->
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -39,20 +39,110 @@ end
|
|||
class TestAndroidFormatter < FormatterTest
|
||||
def setup
|
||||
super Twine::Formatters::Android
|
||||
|
||||
|
||||
@escape_test_values = {
|
||||
'this & that' => 'this & that',
|
||||
'this < that' => 'this < that',
|
||||
"it's complicated" => "it\\'s complicated",
|
||||
'a "good" way' => 'a \"good\" way',
|
||||
'<b>bold</b>' => '<b>bold</b>',
|
||||
'<a href="target">link</a>' => '<a href=\"target\">link</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>' => '<b>%s</b>',
|
||||
'<em>%@</em>' => '<em>%s</em>',
|
||||
|
||||
'<i>%@</i>' => '<i>%s</i>',
|
||||
'<cite>%@</cite>' => '<cite>%s</cite>',
|
||||
'<dfn>%@</dfn>' => '<dfn>%s</dfn>',
|
||||
|
||||
'<big>%@</big>' => '<big>%s</big>',
|
||||
'<small>%@</small>' => '<small>%s</small>',
|
||||
|
||||
'<font color="#45C1D0>%@</font>' => '<font color="#45C1D0>%s</font>',
|
||||
|
||||
'<tt>%@</tt>' => '<tt>%s</tt>',
|
||||
|
||||
'<s>%@</s>' => '<s>%s</s>',
|
||||
'<strike>%@</strike>' => '<strike>%s</strike>',
|
||||
'<del>%@</del>' => '<del>%s</del>',
|
||||
|
||||
'<u>%@</u>' => '<u>%s</u>',
|
||||
|
||||
'<super>%@</super>' => '<super>%s</super>',
|
||||
|
||||
'<sub>%@</sub>' => '<sub>%s</sub>',
|
||||
|
||||
'<ul>%@</ul>' => '<ul>%s</ul>',
|
||||
'<li>%@</li>' => '<li>%s</li>',
|
||||
|
||||
'<br>%@' => '<br>%s',
|
||||
|
||||
'<div>%@</div>' => '<div>%s</div>',
|
||||
|
||||
'<span style="color:#45C1D0">%@</span>' => '<span style="color:#45C1D0">%s</span>',
|
||||
|
||||
'<p>%@</p>' => '<p>%s</p>',
|
||||
'<p dir="ltr">%@</p>' => '<p dir="ltr">%s</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¶m2=3¶m3=%20">link</a>' => '<a href="http://url.com?param=1¶m2=3¶m3=%20">link</a>',
|
||||
|
||||
'<q>escaped</q><![CDATA[]]>' => '<q>escaped</q><![CDATA[]]>',
|
||||
'<![CDATA[]]><q>escaped</q>' => '<![CDATA[]]><q>escaped</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>' => '<b>bold</b>',
|
||||
'<i>italic</i>' => '<i>italic</i>',
|
||||
'<u>underline</u>' => '<u>underline</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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
2
twine
|
@ -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 "$@"
|
||||
|
|
|
@ -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 )
|
||||
|
|
Reference in a new issue