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

|
||||
|
||||
A formatter takes each of these components (and a few more) and formats them, working it's way from the outside inwards. First the method `format_file` is called, which doesn't do much more than calling `format_sections` which in turn calls `format_section` for each section and so on. To get an overview which method calls which, look at the next picture, where each method is represented by a block that is called by its surrounding block - read it from top to bottom and from the outside in.
|
||||
|
||||

|
||||
|
||||
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
|
||||
|
||||
```
|
||||
/********** General **********/
|
||||
|
||||
"yes" = "Yes";
|
||||
|
||||
"no" = "No";
|
||||
|
||||
|
||||
/********** Messages **********/
|
||||
|
||||
/* Everything worked */
|
||||
"success" = "All good";
|
||||
```
|
||||
|
||||
Or, highlighted by the method that produces each piece of the output
|
||||
|
||||

|
||||
|
||||
The process described above is implemented by the structure giving formatter base class [`Abstract`](/lib/twine/formatters/abstract.rb). To generate a desired output format, a formatter overwrites just enough methods to gain as much control as it needs - it's basically the [Template Method Pattern](https://en.wikipedia.org/wiki/Template_method_pattern) applied again and again.
|
||||
|
||||
# Write a Formatter
|
||||
|
||||
Formatters inherit from [`Abstract`](/lib/twine/formatters/abstract.rb) and need to specify some information about the format they are supporting like the format name, the default file name, if they can handle a directory of localization files and so on - just take a look at the class and the [other formatters](/lib/twine/formatters) to get an idea.
|
||||
|
||||
The `Abstract` formatter also specifies two utility methods to be used when read a localization file, `set_translation_for_key` and `set_comment_for_key`, however the actual parsing is too formatter specific and must be implemented in the `read` method of a formatter.
|
||||
|
||||
Which methods to overwrite to produce the desired output depends pretty much on the format. Again, looking at the [bundled formatters](/lib/twine/formatters) will provide some insight.
|
||||
|
||||
Finally, to make a formatter available, it needs to be added to the list of formatters
|
||||
|
||||
```
|
||||
Twine::Formatters.formatters << MyWickedFormatter.new
|
||||
```
|
||||
|
||||
# Plugins
|
||||
|
||||
Once a formatter has been developed, it can be distributed as a gem and loaded as a plugin. Twine will read a yaml config file specifying which plugins to load from three locations.
|
||||
|
||||
0. `./twine.yml` The current working directory
|
||||
0. `~/.twine` The home directory
|
||||
0. `/etc/twine.yml` The etc directory
|
||||
|
||||
Plugins are specified as values for the `gems` key. The following is an example config:
|
||||
|
||||
```
|
||||
gems: wicked_twine
|
||||
```
|
||||
|
||||
Multiple gems can also be specified in the yaml file.
|
||||
|
||||
```
|
||||
gems: [wicked_twine, some_other_plugin]
|
||||
```
|
||||
|
||||
## Sample Plugins
|
||||
|
||||
* [appium-twine](https://github.com/appium/appium_twine)
|
||||
* [twine-flutter](https://github.com/tiknil/twine-flutter)
|
15
lib/twine.rb
15
lib/twine.rb
|
@ -21,13 +21,22 @@ module Twine
|
|||
class Error < StandardError
|
||||
end
|
||||
|
||||
require 'twine/version'
|
||||
require 'twine/plugin'
|
||||
require 'twine/cli'
|
||||
require 'twine/stringsfile'
|
||||
require 'twine/twine_file'
|
||||
require 'twine/encoding'
|
||||
require 'twine/output_processor'
|
||||
require 'twine/placeholders'
|
||||
require 'twine/formatters'
|
||||
require 'twine/formatters/abstract'
|
||||
require 'twine/formatters/android'
|
||||
require 'twine/formatters/apple'
|
||||
require 'twine/formatters/apple_plural'
|
||||
require 'twine/formatters/django'
|
||||
require 'twine/formatters/flash'
|
||||
require 'twine/formatters/gettext'
|
||||
require 'twine/formatters/jquery'
|
||||
require 'twine/formatters/tizen'
|
||||
require 'twine/runner'
|
||||
require 'twine/version'
|
||||
require 'twine/cli'
|
||||
end
|
||||
|
|
569
lib/twine/cli.rb
569
lib/twine/cli.rb
|
@ -1,184 +1,433 @@
|
|||
require 'optparse'
|
||||
require 'io/console'
|
||||
|
||||
module Twine
|
||||
module CLI
|
||||
NEEDED_COMMAND_ARGUMENTS = {
|
||||
'generate-string-file' => 3,
|
||||
'generate-all-string-files' => 3,
|
||||
'consume-string-file' => 3,
|
||||
'consume-all-string-files' => 3,
|
||||
'generate-loc-drop' => 3,
|
||||
'consume-loc-drop' => 3,
|
||||
'validate-strings-file' => 2
|
||||
ALL_FORMATS = Formatters.formatters.map(&:format_name).map(&:downcase)
|
||||
OPTIONS = {
|
||||
consume_all: {
|
||||
switch: ['-a', '--[no-]consume-all'],
|
||||
description: 'Normally Twine will ignore any translation keys that do not exist in your Twine file.',
|
||||
boolean: true
|
||||
},
|
||||
consume_comments: {
|
||||
switch: ['-c', '--[no-]consume-comments'],
|
||||
description: <<-DESC,
|
||||
Normally Twine will ignore all comments in the file. With this flag set, any
|
||||
comments encountered will be read and parsed into the Twine data file. This is especially useful
|
||||
when creating your first Twine data file from an existing project.
|
||||
DESC
|
||||
boolean: true
|
||||
},
|
||||
create_folders: {
|
||||
switch: ['-r', '--[no-]create-folders'],
|
||||
description: <<-DESC,
|
||||
This flag may be used to create output folders for all languages, if they don't exist yet.
|
||||
As a result all languages will be exported, not only the ones where an output folder already exists.
|
||||
DESC
|
||||
boolean: true
|
||||
},
|
||||
developer_language: {
|
||||
switch: ['-d', '--developer-language LANG'],
|
||||
description: <<-DESC,
|
||||
When writing the Twine data file, set the specified language as the "developer language". In
|
||||
practice, this just means that this language will appear first in the Twine data file. When
|
||||
generating files this language will be used as default language and its translations will be
|
||||
used if a definition is not localized for the output language.
|
||||
DESC
|
||||
},
|
||||
encoding: {
|
||||
switch: ['-e', '--encoding ENCODING'],
|
||||
description: <<-DESC,
|
||||
Twine defaults to encoding all output files in UTF-8. This flag will tell Twine to use an alternate
|
||||
encoding for these files. For example, you could use this to write Apple .strings files in UTF-16.
|
||||
When reading files, Twine does its best to determine the encoding automatically. However, if the
|
||||
files are UTF-16 without BOM, you need to specify if it's UTF-16LE or UTF16-BE.
|
||||
DESC
|
||||
},
|
||||
escape_all_tags: {
|
||||
switch: ['--[no-]escape-all-tags'],
|
||||
description: <<-DESC,
|
||||
Always escape all HTML tags. By default the Android formatter will ONLY escape styling tags, if a
|
||||
string also contains placeholders. This flag enforces that styling tags are escaped regardless of
|
||||
placeholders.
|
||||
DESC
|
||||
},
|
||||
file_name: {
|
||||
switch: ['-n', '--file-name FILE_NAME'],
|
||||
description: 'This flag may be used to overwrite the default file name of the format.'
|
||||
},
|
||||
format: {
|
||||
switch: ['-f', '--format FORMAT', ALL_FORMATS],
|
||||
description: <<-DESC,
|
||||
The file format to read or write: (#{ALL_FORMATS.join(', ')}). Additional formatters can be placed in the formats/ directory.
|
||||
DESC
|
||||
},
|
||||
:include => {
|
||||
switch: ['-i', '--include SET', [:all, :translated, :untranslated]],
|
||||
description: <<-DESC,
|
||||
This flag will determine which definitions are included. It's possible values are:
|
||||
all: All definitions both translated and untranslated for the specified language are included.
|
||||
This is the default value.
|
||||
translated: Only definitions with translation for the specified language are included.
|
||||
untranslated: Only definitions without translation for the specified language are included.
|
||||
DESC
|
||||
default: :all
|
||||
},
|
||||
languages: {
|
||||
switch: ['-l', '--lang LANGUAGES', Array],
|
||||
description: 'Comma separated list of language codes to use for the specified action.'
|
||||
},
|
||||
output_path: {
|
||||
switch: ['-o', '--output-file OUTPUT_FILE'],
|
||||
description: 'Write a new Twine file at this location instead of replacing the original file.'
|
||||
},
|
||||
pedantic: {
|
||||
switch: ['-p', '--[no-]pedantic'],
|
||||
description: 'When validating a Twine file, perform additional checks that go beyond pure validity (like presence of tags).'
|
||||
},
|
||||
quiet: {
|
||||
switch: ['-q', '--[no-]quiet'],
|
||||
description: 'Suppress all console output except error messages.'
|
||||
},
|
||||
tags: {
|
||||
switch: ['-t', '--tags TAG1,TAG2,TAG3', Array],
|
||||
description: <<-DESC,
|
||||
Only definitions with ANY of the specified tags will be processed. Specify this option multiple
|
||||
times to only include definitions with ALL of the specified tags. Prefix a tag with ~ to include
|
||||
definitions NOT containing that tag. Omit this option to match all definitions in the Twine data file.
|
||||
DESC
|
||||
repeated: true
|
||||
},
|
||||
untagged: {
|
||||
switch: ['-u', '--[no-]untagged'],
|
||||
description: <<-DESC,
|
||||
If you have specified tags using the --tags flag, then only those tags will be selected. If you also
|
||||
want to select all definitions that are untagged, then you can specify this option to do so.
|
||||
DESC
|
||||
},
|
||||
validate: {
|
||||
switch: ['--[no-]validate'],
|
||||
description: 'Validate the Twine file before formatting it.'
|
||||
}
|
||||
}
|
||||
|
||||
COMMANDS = {
|
||||
'generate-localization-file' => {
|
||||
description: 'Generates a localization file in a certain LANGUAGE given a particular FORMAT. This script will attempt to guess both the language and the format given the filename and extension. For example, "ko.xml" will generate a Korean language file for Android.',
|
||||
arguments: [:twine_file, :output_path],
|
||||
optional_options: [
|
||||
:developer_language,
|
||||
:encoding,
|
||||
:escape_all_tags,
|
||||
:format,
|
||||
:include,
|
||||
:languages,
|
||||
:quiet,
|
||||
:tags,
|
||||
:untagged,
|
||||
:validate
|
||||
],
|
||||
option_validation: Proc.new { |options|
|
||||
if options[:languages] and options[:languages].length > 1
|
||||
raise Twine::Error.new 'specify only a single language for the `generate-localization-file` command.'
|
||||
end
|
||||
},
|
||||
example: 'twine generate-localization-file twine.txt ko.xml --tags FT'
|
||||
},
|
||||
'generate-all-localization-files' => {
|
||||
description: 'Generates all the localization files necessary for a given project. The parent directory to all of the locale-specific directories in your project should be specified as the INPUT_OR_OUTPUT_PATH. This command will most often be executed by your build script so that each build always contains the most recent translations.',
|
||||
arguments: [:twine_file, :output_path],
|
||||
optional_options: [
|
||||
:create_folders,
|
||||
:developer_language,
|
||||
:encoding,
|
||||
:escape_all_tags,
|
||||
:file_name,
|
||||
:format,
|
||||
:include,
|
||||
:quiet,
|
||||
:tags,
|
||||
:untagged,
|
||||
:validate
|
||||
],
|
||||
example: 'twine generate-all-localization-files twine.txt Resources/Locales/ --tags FT,FB'
|
||||
},
|
||||
'generate-localization-archive' => {
|
||||
description: 'Generates a zip archive of localization files in a given format. The purpose of this command is to create a very simple archive that can be handed off to a translation team. The translation team can unzip the archive, translate all of the strings in the archived files, zip everything back up, and then hand that final archive back to be consumed by the consume-localization-archive command.',
|
||||
arguments: [:twine_file, :output_path],
|
||||
required_options: [
|
||||
:format
|
||||
],
|
||||
optional_options: [
|
||||
:developer_language,
|
||||
:encoding,
|
||||
:escape_all_tags,
|
||||
:include,
|
||||
:quiet,
|
||||
:tags,
|
||||
:untagged,
|
||||
:validate
|
||||
],
|
||||
example: 'twine generate-localization-archive twine.txt LocDrop5.zip --tags FT,FB --format android --lang de,en,en-GB,ja,ko'
|
||||
},
|
||||
'consume-localization-file' => {
|
||||
description: 'Slurps all of the translations from a localization file into the specified TWINE_FILE. If you have some files returned to you by your translators you can use this command to incorporate all of their changes. This script will attempt to guess both the language and the format given the filename and extension. For example, "ja.strings" will assume that the file is a Japanese iOS strings file.',
|
||||
arguments: [:twine_file, :input_path],
|
||||
optional_options: [
|
||||
:consume_all,
|
||||
:consume_comments,
|
||||
:developer_language,
|
||||
:encoding,
|
||||
:format,
|
||||
:languages,
|
||||
:output_path,
|
||||
:quiet,
|
||||
:tags
|
||||
],
|
||||
option_validation: Proc.new { |options|
|
||||
if options[:languages] and options[:languages].length > 1
|
||||
raise Twine::Error.new 'specify only a single language for the `consume-localization-file` command.'
|
||||
end
|
||||
},
|
||||
example: 'twine consume-localization-file twine.txt ja.strings'
|
||||
},
|
||||
'consume-all-localization-files' => {
|
||||
description: 'Slurps all of the translations from a directory into the specified TWINE_FILE. If you have some files returned to you by your translators you can use this command to incorporate all of their changes. This script will attempt to guess both the language and the format given the filename and extension. For example, "ja.strings" will assume that the file is a Japanese iOS strings file.',
|
||||
arguments: [:twine_file, :input_path],
|
||||
optional_options: [
|
||||
:consume_all,
|
||||
:consume_comments,
|
||||
:developer_language,
|
||||
:encoding,
|
||||
:format,
|
||||
:output_path,
|
||||
:quiet,
|
||||
:tags
|
||||
],
|
||||
example: 'twine consume-all-localization-files twine.txt Resources/Locales/ --developer-language en --tags DefaultTag1,DefaultTag2'
|
||||
},
|
||||
'consume-localization-archive' => {
|
||||
description: 'Consumes an archive of translated files. This archive should be in the same format as the one created by the generate-localization-archive command.',
|
||||
arguments: [:twine_file, :input_path],
|
||||
optional_options: [
|
||||
:consume_all,
|
||||
:consume_comments,
|
||||
:developer_language,
|
||||
:encoding,
|
||||
:format,
|
||||
:output_path,
|
||||
:quiet,
|
||||
:tags
|
||||
],
|
||||
example: 'twine consume-localization-archive twine.txt LocDrop5.zip'
|
||||
},
|
||||
'validate-twine-file' => {
|
||||
description: 'Validates that the given Twine file is parseable, contains no duplicates, and that no key contains invalid characters. Exits with a non-zero exit code if those criteria are not met.',
|
||||
arguments: [:twine_file],
|
||||
optional_options: [
|
||||
:developer_language,
|
||||
:pedantic,
|
||||
:quiet
|
||||
],
|
||||
example: 'twine validate-twine-file twine.txt'
|
||||
}
|
||||
}
|
||||
DEPRECATED_COMMAND_MAPPINGS = {
|
||||
'generate-loc-drop' => 'generate-localization-archive', # added on 17.01.2017 - version 0.10
|
||||
'consume-loc-drop' => 'consume-localization-archive' # added on 17.01.2017 - version 0.10
|
||||
}
|
||||
|
||||
def self.parse(args)
|
||||
options = { include: :all }
|
||||
command = args.select { |a| a[0] != '-' }[0]
|
||||
args = args.reject { |a| a == command }
|
||||
|
||||
parser = OptionParser.new do |opts|
|
||||
opts.banner = 'Usage: twine COMMAND STRINGS_FILE [INPUT_OR_OUTPUT_PATH] [--lang LANG1,LANG2...] [--tags TAG1,TAG2,TAG3...] [--format FORMAT]'
|
||||
opts.separator ''
|
||||
opts.separator 'The purpose of this script is to convert back and forth between multiple data formats, allowing us to treat our strings (and translations) as data stored in a text file. We can then use the data file to create drops for the localization team, consume similar drops returned by the localization team, and create formatted string files to ship with your products.'
|
||||
opts.separator ''
|
||||
opts.separator 'Commands:'
|
||||
opts.separator ''
|
||||
opts.separator '- generate-string-file'
|
||||
opts.separator ' Generates a string file in a certain LANGUAGE given a particular FORMAT. This script will attempt to guess both the language and the format given the filename and extension. For example, "ko.xml" will generate a Korean language file for Android.'
|
||||
opts.separator ''
|
||||
opts.separator '- generate-all-string-files'
|
||||
opts.separator ' Generates all the string files necessary for a given project. The parent directory to all of the locale-specific directories in your project should be specified as the INPUT_OR_OUTPUT_PATH. This command will most often be executed by your build script so that each build always contains the most recent strings.'
|
||||
opts.separator ''
|
||||
opts.separator '- consume-string-file'
|
||||
opts.separator ' Slurps all of the strings from a translated strings file into the specified STRINGS_FILE. If you have some files returned to you by your translators you can use this command to incorporate all of their changes. This script will attempt to guess both the language and the format given the filename and extension. For example, "ja.strings" will assume that the file is a Japanese iOS strings file.'
|
||||
opts.separator ''
|
||||
opts.separator '- consume-all-string-files'
|
||||
opts.separator ' Slurps all of the strings from a directory into the specified STRINGS_FILE. If you have some files returned to you by your translators you can use this command to incorporate all of their changes. This script will attempt to guess both the language and the format given the filename and extension. For example, "ja.strings" will assume that the file is a Japanese iOS strings file.'
|
||||
opts.separator ''
|
||||
opts.separator '- generate-loc-drop'
|
||||
opts.separator ' Generates a zip archive of strings files in any format. The purpose of this command is to create a very simple archive that can be handed off to a translation team. The translation team can unzip the archive, translate all of the strings in the archived files, zip everything back up, and then hand that final archive back to be consumed by the consume-loc-drop command.'
|
||||
opts.separator ''
|
||||
opts.separator '- consume-loc-drop'
|
||||
opts.separator ' Consumes an archive of translated files. This archive should be in the same format as the one created by the generate-loc-drop command.'
|
||||
opts.separator ''
|
||||
opts.separator '- validate-strings-file'
|
||||
opts.separator ' Validates that the given strings file is parseable, contains no duplicates, and that every string has a tag. Exits with a non-zero exit code if those criteria are not met.'
|
||||
opts.separator ''
|
||||
opts.separator 'General Options:'
|
||||
opts.separator ''
|
||||
opts.on('-l', '--lang LANGUAGES', Array, 'The language code(s) to use for the specified action.') do |l|
|
||||
options[:languages] = l
|
||||
end
|
||||
opts.on('-t', '--tags TAG1,TAG2,TAG3', Array, 'The tag(s) to use for the specified action. Only strings with that tag will be processed. Omit this option to match',
|
||||
' all strings in the strings data file.') do |t|
|
||||
options[:tags] = t
|
||||
end
|
||||
opts.on('-u', '--[no-]untagged', 'If you have specified tags using the --tags flag, then only those tags will be selected. If you also want to select',
|
||||
' all strings that are untagged, then you can specify this option to do so.') do |u|
|
||||
options[:untagged] = u
|
||||
end
|
||||
formats = Formatters.formatters.map(&:format_name).map(&:downcase)
|
||||
opts.on('-f', '--format FORMAT', formats, "The file format to read or write: (#{formats.join(', ')}).",
|
||||
" Additional formatters can be placed in the formats/ directory.") do |f|
|
||||
options[:format] = f
|
||||
end
|
||||
opts.on('-a', '--[no-]consume-all', 'Normally, when consuming a string file, Twine will ignore any string keys that do not exist in your master file.') do |a|
|
||||
options[:consume_all] = true
|
||||
end
|
||||
opts.on('-i', '--include SET', [:all, :translated, :untranslated],
|
||||
"This flag will determine which strings are included when generating strings files. It's possible values:",
|
||||
" all: All strings both translated and untranslated for the specified language are included. This is the default value.",
|
||||
" translated: Only translated strings are included.",
|
||||
" untranslated: Only untranslated strings are included.") do |i|
|
||||
options[:include] = i
|
||||
end
|
||||
opts.on('-o', '--output-file OUTPUT_FILE', 'Write the new strings database to this file instead of replacing the original file. This flag is only useful when',
|
||||
' running the consume-string-file or consume-loc-drop commands.') do |o|
|
||||
options[:output_path] = o
|
||||
end
|
||||
opts.on('-n', '--file-name FILE_NAME', 'When running the generate-all-string-files command, this flag may be used to overwrite the default file name of the format.') do |n|
|
||||
options[:file_name] = n
|
||||
end
|
||||
opts.on('-r', '--[no-]create-folders', "When running the generate-all-string-files command, this flag may be used to create output folders for all languages,",
|
||||
" if they don't exist yet. As a result all languages will be exported, not only the ones where an output folder already",
|
||||
" exists.") do |r|
|
||||
options[:create_folders] = r
|
||||
end
|
||||
opts.on('-d', '--developer-language LANG', 'When writing the strings data file, set the specified language as the "developer language". In practice, this just',
|
||||
' means that this language will appear first in the strings data file. When generating files this language will be',
|
||||
' used as default language and its translations will be used if a key is not localized for the output language.') do |d|
|
||||
options[:developer_language] = d
|
||||
end
|
||||
opts.on('-c', '--[no-]consume-comments', 'Normally, when consuming a string file, Twine will ignore all comments in the file. With this flag set, any comments',
|
||||
' encountered will be read and parsed into the strings data file. This is especially useful when creating your first',
|
||||
' strings data file from an existing project.') do |c|
|
||||
options[:consume_comments] = c
|
||||
end
|
||||
opts.on('-e', '--encoding ENCODING', 'Twine defaults to encoding all output files in UTF-8. This flag will tell Twine to use an alternate encoding for these',
|
||||
' files. For example, you could use this to write Apple .strings files in UTF-16. When reading files, Twine does its best',
|
||||
" to determine the encoding automatically. However, if the files are UTF-16 without BOM, you need to specify if it's",
|
||||
' UTF-16LE or UTF16-BE.') do |e|
|
||||
options[:output_encoding] = e
|
||||
end
|
||||
opts.on('--[no-]validate', 'Validate the strings file before formatting it.') do |validate|
|
||||
options[:validate] = validate
|
||||
end
|
||||
opts.on('-p', '--[no-]pedantic', 'When validating a strings file, perform additional checks that go beyond pure validity (like presence of tags).') do |p|
|
||||
options[:pedantic] = p
|
||||
end
|
||||
opts.on('-h', '--help', 'Show this message.') do |h|
|
||||
puts opts.help
|
||||
exit
|
||||
end
|
||||
opts.on('--version', 'Print the version number and exit.') do
|
||||
puts "Twine version #{Twine::VERSION}"
|
||||
exit
|
||||
end
|
||||
opts.separator ''
|
||||
opts.separator 'Examples:'
|
||||
opts.separator ''
|
||||
opts.separator '> twine generate-string-file strings.txt ko.xml --tags FT'
|
||||
opts.separator '> twine generate-all-string-files strings.txt Resources/Locales/ --tags FT,FB'
|
||||
opts.separator '> twine consume-string-file strings.txt ja.strings'
|
||||
opts.separator '> twine consume-all-string-files strings.txt Resources/Locales/ --developer-language en --tags DefaultTag1,DefaultTag2'
|
||||
opts.separator '> twine generate-loc-drop strings.txt LocDrop5.zip --tags FT,FB --format android --lang de,en,en-GB,ja,ko'
|
||||
opts.separator '> twine consume-loc-drop strings.txt LocDrop5.zip'
|
||||
opts.separator '> twine validate-strings-file strings.txt'
|
||||
if args.any? { |a| a == '--version' }
|
||||
Twine::stdout.puts "Twine version #{Twine::VERSION}"
|
||||
return false
|
||||
end
|
||||
|
||||
mapped_command = DEPRECATED_COMMAND_MAPPINGS[command]
|
||||
if mapped_command
|
||||
Twine::stdout.puts "WARNING: Twine commands names have changed. `#{command}` is now `#{mapped_command}`. The old command is deprecated and will soon stop working. For more information please check the documentation at https://github.com/mobiata/twine"
|
||||
command = mapped_command
|
||||
end
|
||||
|
||||
if command.nil?
|
||||
print_help(args)
|
||||
return false
|
||||
elsif not COMMANDS.keys.include? command
|
||||
raise Twine::Error.new "Invalid command: #{command}"
|
||||
end
|
||||
|
||||
parse_command_options(command, args)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def self.print_help(args)
|
||||
verbose = false
|
||||
|
||||
help_parser = OptionParser.new
|
||||
help_parser.banner = 'Usage: twine [command] [options]'
|
||||
|
||||
help_parser.define('-h', '--help', 'Show this message.')
|
||||
help_parser.define('--verbose', 'More detailed help.') { verbose = true }
|
||||
|
||||
help_parser.parse!(args)
|
||||
|
||||
Twine::stdout.puts help_parser.help
|
||||
Twine::stdout.puts ''
|
||||
|
||||
|
||||
Twine::stdout.puts 'Commands:'
|
||||
|
||||
COMMANDS.each do |name, properties|
|
||||
if verbose
|
||||
Twine::stdout.puts ''
|
||||
Twine::stdout.puts ''
|
||||
Twine::stdout.puts "# #{name}"
|
||||
Twine::stdout.puts ''
|
||||
Twine::stdout.puts properties[:description]
|
||||
else
|
||||
Twine::stdout.puts "- #{name}"
|
||||
end
|
||||
end
|
||||
|
||||
Twine::stdout.puts ''
|
||||
Twine::stdout.puts 'type `twine [command] --help` for further information about a command.'
|
||||
end
|
||||
|
||||
# source: https://www.safaribooksonline.com/library/view/ruby-cookbook/0596523696/ch01s15.html
|
||||
def self.word_wrap(s, width)
|
||||
s.gsub(/(.{1,#{width}})(\s+|\Z)/, "\\1\n").rstrip
|
||||
end
|
||||
|
||||
def self.indent(string, first_line, following_lines)
|
||||
lines = string.split("\n")
|
||||
indentation = ' ' * following_lines
|
||||
lines.map! { |line| indentation + line }
|
||||
result = lines.join("\n").strip
|
||||
' ' * first_line + result
|
||||
end
|
||||
|
||||
# ensure the description forms a neat block on the right
|
||||
def self.prepare_description!(options, summary_width)
|
||||
lines = options[:description].split "\n"
|
||||
|
||||
# remove leadinge HEREDOC spaces
|
||||
space_match = lines[0].match(/^\s+/)
|
||||
if space_match
|
||||
leading_spaces = space_match[0].length
|
||||
lines.map! { |l| l[leading_spaces..-1] }
|
||||
end
|
||||
|
||||
merged_lines = []
|
||||
lines.each do |line|
|
||||
# if the line is a continuation of the previous one
|
||||
if not merged_lines.empty? and (line[0] != ' ' or line[0, 4] == ' ')
|
||||
merged_lines[-1] += ' ' + line.strip
|
||||
else
|
||||
merged_lines << line.rstrip
|
||||
end
|
||||
end
|
||||
|
||||
if IO.console
|
||||
console_width = IO.console.winsize[1]
|
||||
else
|
||||
console_width = 100
|
||||
end
|
||||
summary_width += 7 # account for description padding
|
||||
max_description_width = console_width - summary_width
|
||||
merged_lines.map! do |line|
|
||||
if line[0] == ' '
|
||||
line = word_wrap(line.strip, max_description_width - 2)
|
||||
line = indent(line, 2, 4)
|
||||
else
|
||||
line = word_wrap(line, max_description_width)
|
||||
end
|
||||
line
|
||||
end
|
||||
|
||||
options[:switch] << indent(merged_lines.join("\n"), 0, summary_width)
|
||||
end
|
||||
|
||||
def self.parse_command_options(command_name, args)
|
||||
command = COMMANDS[command_name]
|
||||
|
||||
result = {
|
||||
command: command_name
|
||||
}
|
||||
|
||||
parser = OptionParser.new
|
||||
parser.banner = "Usage: twine #{command_name} #{command[:arguments].map { |c| "[#{c}]" }.join(' ')} [options]"
|
||||
|
||||
[:required_options, :optional_options].each do |option_type|
|
||||
options = command[option_type]
|
||||
if options and options.size > 0
|
||||
parser.separator ''
|
||||
parser.separator option_type.to_s.gsub('_', ' ').capitalize + ":"
|
||||
|
||||
options.each do |option_name|
|
||||
option = OPTIONS[option_name]
|
||||
|
||||
result[option_name] = option[:default] if option[:default]
|
||||
|
||||
prepare_description!(option, parser.summary_width)
|
||||
|
||||
parser.define(*option[:switch]) do |value|
|
||||
if option[:repeated]
|
||||
result[option_name] = (result[option_name] || []) << value
|
||||
else
|
||||
result[option_name] = value
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
parser.define('-h', '--help', 'Show this message.') do
|
||||
Twine::stdout.puts parser.help
|
||||
return false
|
||||
end
|
||||
|
||||
parser.separator ''
|
||||
parser.separator 'Examples:'
|
||||
parser.separator ''
|
||||
parser.separator "> #{command[:example]}"
|
||||
|
||||
begin
|
||||
parser.parse! args
|
||||
rescue OptionParser::ParseError => e
|
||||
Twine::stderr.puts e.message
|
||||
exit false
|
||||
raise Twine::Error.new e.message
|
||||
end
|
||||
|
||||
if args.length == 0
|
||||
puts parser.help
|
||||
exit false
|
||||
arguments = args.reject { |a| a[0] == '-' }
|
||||
number_of_missing_arguments = command[:arguments].size - arguments.size
|
||||
if number_of_missing_arguments > 0
|
||||
missing_arguments = command[:arguments][-number_of_missing_arguments, number_of_missing_arguments]
|
||||
raise Twine::Error.new "#{number_of_missing_arguments} missing argument#{number_of_missing_arguments > 1 ? "s" : ""}: #{missing_arguments.join(', ')}. Check `twine #{command_name} -h`"
|
||||
end
|
||||
|
||||
number_of_needed_arguments = NEEDED_COMMAND_ARGUMENTS[args[0]]
|
||||
unless number_of_needed_arguments
|
||||
raise Twine::Error.new "Invalid command: #{args[0]}"
|
||||
end
|
||||
options[:command] = args[0]
|
||||
|
||||
if args.length < 2
|
||||
raise Twine::Error.new 'You must specify your strings file.'
|
||||
end
|
||||
options[:strings_file] = args[1]
|
||||
|
||||
if args.length < number_of_needed_arguments
|
||||
raise Twine::Error.new 'Not enough arguments.'
|
||||
elsif args.length > number_of_needed_arguments
|
||||
raise Twine::Error.new "Unknown argument: #{args[number_of_needed_arguments]}"
|
||||
if args.length > command[:arguments].size
|
||||
raise Twine::Error.new "Unknown argument: #{args[command[:arguments].size]}"
|
||||
end
|
||||
|
||||
case options[:command]
|
||||
when 'generate-string-file'
|
||||
options[:output_path] = args[2]
|
||||
if options[:languages] and options[:languages].length > 1
|
||||
raise Twine::Error.new 'Please only specify a single language for the generate-string-file command.'
|
||||
if command[:required_options]
|
||||
command[:required_options].each do |option_name|
|
||||
if result[option_name] == nil
|
||||
raise Twine::Error.new "missing option: #{OPTIONS[option_name][:switch][0]}"
|
||||
end
|
||||
end
|
||||
when 'generate-all-string-files'
|
||||
options[:output_path] = args[2]
|
||||
when 'consume-string-file'
|
||||
options[:input_path] = args[2]
|
||||
if options[:languages] and options[:languages].length > 1
|
||||
raise Twine::Error.new 'Please only specify a single language for the consume-string-file command.'
|
||||
end
|
||||
when 'consume-all-string-files'
|
||||
options[:input_path] = args[2]
|
||||
when 'generate-loc-drop'
|
||||
options[:output_path] = args[2]
|
||||
if !options[:format]
|
||||
raise Twine::Error.new 'You must specify a format.'
|
||||
end
|
||||
when 'consume-loc-drop'
|
||||
options[:input_path] = args[2]
|
||||
when 'validate-strings-file'
|
||||
end
|
||||
|
||||
return options
|
||||
command[:option_validation].call(result) if command[:option_validation]
|
||||
|
||||
command[:arguments].each do |argument_name|
|
||||
result[argument_name] = args.shift
|
||||
end
|
||||
|
||||
result
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -18,7 +18,3 @@ module Twine
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
Dir[File.join(File.dirname(__FILE__), 'formatters', '*.rb')].each do |file|
|
||||
require file
|
||||
end
|
||||
|
|
|
@ -3,11 +3,14 @@ require 'fileutils'
|
|||
module Twine
|
||||
module Formatters
|
||||
class Abstract
|
||||
attr_accessor :strings
|
||||
SUPPORTS_PLURAL = false
|
||||
LANGUAGE_CODE_WITH_OPTIONAL_REGION_CODE = "[a-z]{2}(?:-[A-Za-z]{2})?"
|
||||
|
||||
attr_accessor :twine_file
|
||||
attr_accessor :options
|
||||
|
||||
def initialize
|
||||
@strings = StringsFile.new
|
||||
@twine_file = TwineFile.new
|
||||
@options = {}
|
||||
end
|
||||
|
||||
|
@ -20,7 +23,7 @@ module Twine
|
|||
end
|
||||
|
||||
def can_handle_directory?(path)
|
||||
raise NotImplementedError.new("You must implement can_handle_directory? in your formatter class.")
|
||||
Dir.entries(path).any? { |item| /^.+#{Regexp.escape(extension)}$/.match(item) }
|
||||
end
|
||||
|
||||
def default_file_name
|
||||
|
@ -30,53 +33,58 @@ module Twine
|
|||
def set_translation_for_key(key, lang, value)
|
||||
value = value.gsub("\n", "\\n")
|
||||
|
||||
if @strings.strings_map.include?(key)
|
||||
row = @strings.strings_map[key]
|
||||
reference = @strings.strings_map[row.reference_key] if row.reference_key
|
||||
if @twine_file.definitions_by_key.include?(key)
|
||||
definition = @twine_file.definitions_by_key[key]
|
||||
reference = @twine_file.definitions_by_key[definition.reference_key] if definition.reference_key
|
||||
|
||||
if !reference or value != reference.translations[lang]
|
||||
row.translations[lang] = value
|
||||
definition.translations[lang] = value
|
||||
end
|
||||
elsif @options[:consume_all]
|
||||
Twine::stderr.puts "Adding new string '#{key}' to strings data file."
|
||||
current_section = @strings.sections.find { |s| s.name == 'Uncategorized' }
|
||||
Twine::stdout.puts "Adding new definition '#{key}' to twine file."
|
||||
current_section = @twine_file.sections.find { |s| s.name == 'Uncategorized' }
|
||||
unless current_section
|
||||
current_section = StringsSection.new('Uncategorized')
|
||||
@strings.sections.insert(0, current_section)
|
||||
current_section = TwineSection.new('Uncategorized')
|
||||
@twine_file.sections.insert(0, current_section)
|
||||
end
|
||||
current_row = StringsRow.new(key)
|
||||
current_section.rows << current_row
|
||||
current_definition = TwineDefinition.new(key)
|
||||
current_section.definitions << current_definition
|
||||
|
||||
if @options[:tags] && @options[:tags].length > 0
|
||||
current_row.tags = @options[:tags]
|
||||
current_definition.tags = @options[:tags]
|
||||
end
|
||||
|
||||
@strings.strings_map[key] = current_row
|
||||
@strings.strings_map[key].translations[lang] = value
|
||||
@twine_file.definitions_by_key[key] = current_definition
|
||||
@twine_file.definitions_by_key[key].translations[lang] = value
|
||||
else
|
||||
Twine::stderr.puts "Warning: '#{key}' not found in strings data file."
|
||||
Twine::stdout.puts "WARNING: '#{key}' not found in twine file."
|
||||
end
|
||||
if !@strings.language_codes.include?(lang)
|
||||
@strings.add_language_code(lang)
|
||||
if !@twine_file.language_codes.include?(lang)
|
||||
@twine_file.add_language_code(lang)
|
||||
end
|
||||
end
|
||||
|
||||
def set_comment_for_key(key, comment)
|
||||
return unless @options[:consume_comments]
|
||||
|
||||
if @strings.strings_map.include?(key)
|
||||
row = @strings.strings_map[key]
|
||||
if @twine_file.definitions_by_key.include?(key)
|
||||
definition = @twine_file.definitions_by_key[key]
|
||||
|
||||
reference = @strings.strings_map[row.reference_key] if row.reference_key
|
||||
reference = @twine_file.definitions_by_key[definition.reference_key] if definition.reference_key
|
||||
|
||||
if !reference or comment != reference.raw_comment
|
||||
row.comment = comment
|
||||
definition.comment = comment
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def determine_language_given_path(path)
|
||||
raise NotImplementedError.new("You must implement determine_language_given_path in your formatter class.")
|
||||
only_language_and_region = /^#{LANGUAGE_CODE_WITH_OPTIONAL_REGION_CODE}$/i
|
||||
basename = File.basename(path, File.extname(path))
|
||||
return basename if basename =~ only_language_and_region
|
||||
return basename if @twine_file.language_codes.include? basename
|
||||
|
||||
path.split(File::SEPARATOR).reverse.find { |segment| segment =~ only_language_and_region }
|
||||
end
|
||||
|
||||
def output_path_for_language(lang)
|
||||
|
@ -88,35 +96,35 @@ module Twine
|
|||
end
|
||||
|
||||
def format_file(lang)
|
||||
output_processor = Processors::OutputProcessor.new(@strings, @options)
|
||||
processed_strings = output_processor.process(lang)
|
||||
output_processor = Processors::OutputProcessor.new(@twine_file, @options)
|
||||
processed_twine_file = output_processor.process(lang)
|
||||
|
||||
return nil if processed_strings.strings_map.empty?
|
||||
return nil if processed_twine_file.definitions_by_key.empty?
|
||||
|
||||
header = format_header(lang)
|
||||
result = ""
|
||||
result += header + "\n" if header
|
||||
result += format_sections(processed_strings, lang)
|
||||
result += format_sections(processed_twine_file, lang)
|
||||
end
|
||||
|
||||
def format_header(lang)
|
||||
end
|
||||
|
||||
def format_sections(strings, lang)
|
||||
sections = strings.sections.map { |section| format_section(section, lang) }
|
||||
def format_sections(twine_file, lang)
|
||||
sections = twine_file.sections.map { |section| format_section(section, lang) }
|
||||
sections.compact.join("\n")
|
||||
end
|
||||
|
||||
def format_section_header(section)
|
||||
end
|
||||
|
||||
def should_include_row(row, lang)
|
||||
row.translated_string_for_lang(lang)
|
||||
def should_include_definition(definition, lang)
|
||||
return !definition.translation_for_lang(lang).nil?
|
||||
end
|
||||
|
||||
def format_section(section, lang)
|
||||
rows = section.rows.select { |row| should_include_row(row, lang) }
|
||||
return if rows.empty?
|
||||
definitions = section.definitions.select { |definition| should_include_definition(definition, lang) }
|
||||
return if definitions.empty?
|
||||
|
||||
result = ""
|
||||
|
||||
|
@ -125,28 +133,45 @@ module Twine
|
|||
result += "\n#{section_header}" if section_header
|
||||
end
|
||||
|
||||
rows.map! { |row| format_row(row, lang) }
|
||||
rows.compact! # remove nil entries
|
||||
rows.map! { |row| "\n#{row}" } # prepend newline
|
||||
result += rows.join
|
||||
definitions.map! { |definition| format_definition(definition, lang) }
|
||||
definitions.compact! # remove nil definitions
|
||||
definitions.map! { |definition| "\n#{definition}" } # prepend newline
|
||||
result += definitions.join
|
||||
end
|
||||
|
||||
def format_row(row, lang)
|
||||
[format_comment(row, lang), format_key_value(row, lang)].compact.join
|
||||
def format_definition(definition, lang)
|
||||
formatted_definition = [format_comment(definition, lang)]
|
||||
if self.class::SUPPORTS_PLURAL && definition.is_plural?
|
||||
formatted_definition << format_plural(definition, lang)
|
||||
else
|
||||
formatted_definition << format_key_value(definition, lang)
|
||||
end
|
||||
formatted_definition.compact.join
|
||||
end
|
||||
|
||||
def format_comment(row, lang)
|
||||
def format_comment(definition, lang)
|
||||
end
|
||||
|
||||
def format_key_value(row, lang)
|
||||
value = row.translated_string_for_lang(lang)
|
||||
key_value_pattern % { key: format_key(row.key.dup), value: format_value(value.dup) }
|
||||
def format_key_value(definition, lang)
|
||||
value = definition.translation_for_lang(lang)
|
||||
key_value_pattern % { key: format_key(definition.key.dup), value: format_value(value.dup) }
|
||||
end
|
||||
|
||||
def format_plural(definition, lang)
|
||||
plural_hash = definition.plural_translation_for_lang(lang)
|
||||
if plural_hash
|
||||
format_plural_keys(definition.key.dup, plural_hash)
|
||||
end
|
||||
end
|
||||
|
||||
def key_value_pattern
|
||||
raise NotImplementedError.new("You must implement key_value_pattern in your formatter class.")
|
||||
end
|
||||
|
||||
def format_plural_keys(key, plural_hash)
|
||||
raise NotImplementedError.new("You must implement format_plural_keys in your formatter class.")
|
||||
end
|
||||
|
||||
def format_key(key)
|
||||
key
|
||||
end
|
||||
|
|
|
@ -7,14 +7,15 @@ module Twine
|
|||
class Android < Abstract
|
||||
include Twine::Placeholders
|
||||
|
||||
LANG_MAPPINGS = Hash[
|
||||
'zh-rCN' => 'zh-Hans',
|
||||
'zh-rHK' => 'zh-Hant',
|
||||
'en-rGB' => 'en-UK',
|
||||
SUPPORTS_PLURAL = true
|
||||
LANG_CODES = Hash[
|
||||
'zh' => 'zh-Hans',
|
||||
'zh-CN' => 'zh-Hans',
|
||||
'zh-HK' => 'zh-Hant',
|
||||
# See https://developer.android.com/reference/java/util/Locale#legacy-language-codes
|
||||
'iw' => 'he',
|
||||
'in' => 'id',
|
||||
'nb' => 'no'
|
||||
# TODO: spanish
|
||||
'ji' => 'yi'
|
||||
]
|
||||
|
||||
def format_name
|
||||
|
@ -30,32 +31,35 @@ module Twine
|
|||
end
|
||||
|
||||
def default_file_name
|
||||
return 'strings.xml'
|
||||
'strings.xml'
|
||||
end
|
||||
|
||||
def determine_language_given_path(path)
|
||||
path_arr = path.split(File::SEPARATOR)
|
||||
path_arr.each do |segment|
|
||||
if segment == 'values'
|
||||
return @strings.language_codes[0]
|
||||
return @twine_file.language_codes[0]
|
||||
else
|
||||
# The language is defined by a two-letter ISO 639-1 language code, optionally followed by a two letter ISO 3166-1-alpha-2 region code (preceded by lowercase "r").
|
||||
# see http://developer.android.com/guide/topics/resources/providing-resources.html#AlternativeResources
|
||||
match = /^values-([a-z]{2}(-r[a-z]{2})?)$/i.match(segment)
|
||||
|
||||
if match
|
||||
lang = match[1]
|
||||
lang = LANG_MAPPINGS.fetch(lang, lang)
|
||||
lang.sub!('-r', '-')
|
||||
return lang
|
||||
lang = match[1].sub('-r', '-')
|
||||
return LANG_CODES.fetch(lang, lang)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return
|
||||
return super
|
||||
end
|
||||
|
||||
def output_path_for_language(lang)
|
||||
"values-" + (LANG_MAPPINGS.key(lang) || lang)
|
||||
if lang == @twine_file.language_codes[0]
|
||||
"values"
|
||||
else
|
||||
"values-#{lang}".gsub(/-(\p{Lu})/, '-r\1')
|
||||
end
|
||||
end
|
||||
|
||||
def set_translation_for_key(key, lang, value)
|
||||
|
@ -69,43 +73,33 @@ module Twine
|
|||
end
|
||||
|
||||
def read(io, lang)
|
||||
resources_regex = /<resources(?:[^>]*)>(.*)<\/resources>/m
|
||||
key_regex = /<string name="(\w+)">/
|
||||
comment_regex = /<!-- (.*) -->/
|
||||
value_regex = /<string name="\w+">(.*)<\/string>/
|
||||
key = nil
|
||||
value = nil
|
||||
document = REXML::Document.new io, :compress_whitespace => %w{ string }
|
||||
document.context[:attribute_quote] = :quote
|
||||
comment = nil
|
||||
document.root.children.each do |child|
|
||||
if child.is_a? REXML::Comment
|
||||
content = child.string.strip
|
||||
content.gsub!(/[\s]+/, ' ')
|
||||
comment = content if content.length > 0 and not content.start_with?("SECTION:")
|
||||
elsif child.is_a? REXML::Element
|
||||
next unless child.name == 'string'
|
||||
|
||||
content_match = resources_regex.match(io.read)
|
||||
if content_match
|
||||
for line in content_match[1].split(/\r?\n/)
|
||||
key_match = key_regex.match(line)
|
||||
if key_match
|
||||
key = key_match[1]
|
||||
value_match = value_regex.match(line)
|
||||
value = value_match ? value_match[1] : ""
|
||||
|
||||
set_translation_for_key(key, lang, value)
|
||||
if comment and comment.length > 0 and !comment.start_with?("SECTION:")
|
||||
set_comment_for_key(key, comment)
|
||||
end
|
||||
comment = nil
|
||||
end
|
||||
|
||||
comment_match = comment_regex.match(line)
|
||||
if comment_match
|
||||
comment = comment_match[1]
|
||||
end
|
||||
end
|
||||
key = child.attributes['name']
|
||||
|
||||
content = child.children.map(&:to_s).join
|
||||
set_translation_for_key(key, lang, content)
|
||||
set_comment_for_key(key, comment) if comment
|
||||
|
||||
comment = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def format_header(lang)
|
||||
"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Android Strings File -->\n<!-- Generated by Twine #{Twine::VERSION} -->\n<!-- Language: #{lang} -->"
|
||||
"<?xml version=\"1.0\" encoding=\"utf-8\"?>"
|
||||
end
|
||||
|
||||
def format_sections(strings, lang)
|
||||
def format_sections(twine_file, lang)
|
||||
result = '<resources>'
|
||||
|
||||
result += super + "\n"
|
||||
|
@ -114,30 +108,91 @@ module Twine
|
|||
end
|
||||
|
||||
def format_section_header(section)
|
||||
"\t<!-- SECTION: #{section.name} -->"
|
||||
"#{space(4)}<!-- SECTION: #{section.name} -->"
|
||||
end
|
||||
|
||||
def format_comment(row, lang)
|
||||
"\t<!-- #{row.comment.gsub('--', '—')} -->\n" if row.comment
|
||||
def format_comment(definition, lang)
|
||||
"#{space(4)}<!-- #{definition.comment.gsub('--', '—')} -->\n" if definition.comment
|
||||
end
|
||||
|
||||
def key_value_pattern
|
||||
"\t<string name=\"%{key}\">%{value}</string>"
|
||||
"#{space(4)}<string name=\"%{key}\">%{value}</string>"
|
||||
end
|
||||
|
||||
def format_value(value)
|
||||
# Android enforces the following rules on the values
|
||||
# 1) apostrophes and quotes must be escaped with a backslash
|
||||
value = escape_quotes(value)
|
||||
value.gsub!("'", "\\\\'")
|
||||
# 2) HTML escape the string
|
||||
value = CGI.escapeHTML(value)
|
||||
# 3) convert placeholders (e.g. %@ -> %s)
|
||||
value = convert_placeholders_from_twine_to_android(value)
|
||||
# 4) escape non resource identifier @ signs (http://developer.android.com/guide/topics/resources/accessing-resources.html#ResourcesFromXml)
|
||||
def format_plural_keys(key, plural_hash)
|
||||
result = "#{space(4)}<plurals name=\"#{key}\">\n"
|
||||
result += plural_hash.map{|quantity,value| "#{space(8)}<item quantity=\"#{quantity}\">#{escape_value(value)}</item>"}.join("\n")
|
||||
result += "\n#{space(4)}</plurals>"
|
||||
end
|
||||
|
||||
def space(level)
|
||||
' ' * level
|
||||
end
|
||||
|
||||
def gsub_unless(text, pattern, replacement)
|
||||
text.gsub(pattern) do |match|
|
||||
match_start_position = Regexp.last_match.offset(0)[0]
|
||||
yield(text[0, match_start_position]) ? match : replacement
|
||||
end
|
||||
end
|
||||
|
||||
# http://developer.android.com/guide/topics/resources/string-resource.html#FormattingAndStyling
|
||||
def escape_value(value)
|
||||
inside_cdata = /<\!\[CDATA\[((?!\]\]>).)*$/ # opening CDATA tag ('<![CDATA[') not followed by a closing tag (']]>')
|
||||
inside_opening_tag = /<(a|font|span|p)\s?((?!>).)*$/ # tag start ('<a ', '<font ', '<span ' or '<p ') not followed by a '>'
|
||||
|
||||
# escape double and single quotes and & signs
|
||||
value = gsub_unless(value, '"', '\\"') { |substring| substring =~ inside_cdata || substring =~ inside_opening_tag }
|
||||
value = gsub_unless(value, "'", "\\'") { |substring| substring =~ inside_cdata }
|
||||
value = gsub_unless(value, /&/, '&') { |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>
|
||||
value.gsub!(resource_identifier_regex, '\@')
|
||||
# 5) replace beginning and end spaces with \0020. Otherwise Android strips them.
|
||||
value.gsub(resource_identifier_regex, '\@')
|
||||
end
|
||||
|
||||
# see http://developer.android.com/guide/topics/resources/string-resource.html#FormattingAndStyling
|
||||
# however unescaped HTML markup like in "Welcome to <b>Android</b>!" is stripped when retrieved with getString() (http://stackoverflow.com/questions/9891996/)
|
||||
def format_value(value)
|
||||
value = value.dup
|
||||
|
||||
# convert placeholders (e.g. %@ -> %s)
|
||||
value = convert_placeholders_from_twine_to_android(value)
|
||||
|
||||
# capture xliff tags and replace them with a placeholder
|
||||
xliff_tags = []
|
||||
value.gsub! /<xliff:g.+?<\/xliff:g>/ do
|
||||
xliff_tags << $&
|
||||
'TWINE_XLIFF_TAG_PLACEHOLDER'
|
||||
end
|
||||
|
||||
# escape everything outside xliff tags
|
||||
value = escape_value(value)
|
||||
|
||||
# put xliff tags back into place
|
||||
xliff_tags.each do |xliff_tag|
|
||||
# escape content of xliff tags
|
||||
xliff_tag.gsub! /(<xliff:g.*?>)(.*)(<\/xliff:g>)/ do "#{$1}#{escape_value($2)}#{$3}" end
|
||||
value.sub! 'TWINE_XLIFF_TAG_PLACEHOLDER', xliff_tag
|
||||
end
|
||||
|
||||
# replace beginning and end spaces with \u0020. Otherwise Android strips them.
|
||||
value.gsub(/\A *| *\z/) { |spaces| '\u0020' * spaces.length }
|
||||
end
|
||||
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
module Twine
|
||||
module Formatters
|
||||
class Apple < Abstract
|
||||
include Twine::Placeholders
|
||||
|
||||
def format_name
|
||||
'apple'
|
||||
end
|
||||
|
@ -14,7 +16,7 @@ module Twine
|
|||
end
|
||||
|
||||
def default_file_name
|
||||
return 'Localizable.strings'
|
||||
'Localizable.strings'
|
||||
end
|
||||
|
||||
def determine_language_given_path(path)
|
||||
|
@ -22,13 +24,15 @@ module Twine
|
|||
path_arr.each do |segment|
|
||||
match = /^(.+)\.lproj$/.match(segment)
|
||||
if match
|
||||
if match[1] != "Base"
|
||||
if match[1] == "Base"
|
||||
return @options[:developer_language]
|
||||
else
|
||||
return match[1]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return
|
||||
return super
|
||||
end
|
||||
|
||||
def output_path_for_language(lang)
|
||||
|
@ -61,20 +65,21 @@ module Twine
|
|||
end
|
||||
end
|
||||
|
||||
def format_header(lang)
|
||||
"/**\n * Apple Strings File\n * Generated by Twine #{Twine::VERSION}\n * Language: #{lang}\n */"
|
||||
def format_file(lang)
|
||||
result = super
|
||||
result += "\n"
|
||||
end
|
||||
|
||||
def format_section_header(section)
|
||||
"/********** #{section.name} **********/\n"
|
||||
"\n/********** #{section.name} **********/\n"
|
||||
end
|
||||
|
||||
def key_value_pattern
|
||||
"\"%{key}\" = \"%{value}\";\n"
|
||||
"\"%{key}\" = \"%{value}\";"
|
||||
end
|
||||
|
||||
def format_comment(row, lang)
|
||||
"/* #{row.comment.gsub('*/', '* /')} */\n" if row.comment
|
||||
def format_comment(definition, lang)
|
||||
"\n/* #{definition.comment.gsub('*/', '* /')} */\n" if definition.comment
|
||||
end
|
||||
|
||||
def format_key(key)
|
||||
|
@ -82,8 +87,14 @@ module Twine
|
|||
end
|
||||
|
||||
def format_value(value)
|
||||
# Replace Android's %s with iOS %@
|
||||
value = convert_placeholders_from_android_to_twine(value)
|
||||
escape_quotes(value)
|
||||
end
|
||||
|
||||
def should_include_definition(definition, lang)
|
||||
return !definition.is_plural? && super
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
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'
|
||||
|
@ -9,32 +10,15 @@ module Twine
|
|||
'.po'
|
||||
end
|
||||
|
||||
def can_handle_directory?(path)
|
||||
Dir.entries(path).any? { |item| /^.+\.po$/.match(item) }
|
||||
end
|
||||
|
||||
def default_file_name
|
||||
return 'strings.po'
|
||||
end
|
||||
|
||||
def determine_language_given_path(path)
|
||||
path_arr = path.split(File::SEPARATOR)
|
||||
path_arr.each do |segment|
|
||||
match = /(..)\.po$/.match(segment)
|
||||
if match
|
||||
return match[1]
|
||||
end
|
||||
end
|
||||
|
||||
return
|
||||
'strings.po'
|
||||
end
|
||||
|
||||
def read(io, lang)
|
||||
comment_regex = /#\. *"?(.*)"?$/
|
||||
key_regex = /msgid *"(.*)"$/
|
||||
value_regex = /msgstr *"(.*)"$/m
|
||||
comment_regex = /^\s*#\. *"?(.*)"?$/
|
||||
key_regex = /^msgid *"(.*)"$/
|
||||
value_regex = /^msgstr *"(.*)"$/m
|
||||
|
||||
last_comment = nil
|
||||
while line = io.gets
|
||||
comment_match = comment_regex.match(line)
|
||||
if comment_match
|
||||
|
@ -63,26 +47,27 @@ module Twine
|
|||
end
|
||||
|
||||
def format_file(lang)
|
||||
@default_lang = @strings.language_codes[0]
|
||||
@default_lang = @twine_file.language_codes[0]
|
||||
result = super
|
||||
@default_lang = nil
|
||||
result
|
||||
end
|
||||
|
||||
def format_header(lang)
|
||||
"##\n # Django Strings File\n # Generated by Twine #{Twine::VERSION}\n # Language: #{lang}\n"
|
||||
# see https://www.gnu.org/software/trans-coord/manual/gnun/html_node/PO-Header.html for details
|
||||
"# Django Strings File\n# Generated by Twine\n# Language: #{lang}\nmsgid \"\"\nmsgstr \"\"\n\"Content-Type: text/plain; charset=UTF-8\\n\""
|
||||
end
|
||||
|
||||
def format_section_header(section)
|
||||
"#--------- #{section.name} ---------#\n"
|
||||
"# --------- #{section.name} --------- #\n"
|
||||
end
|
||||
|
||||
def format_row(row, lang)
|
||||
[format_comment(row, lang), format_base_translation(row), format_key_value(row, lang)].compact.join
|
||||
def format_definition(definition, lang)
|
||||
[format_comment(definition, lang), format_base_translation(definition), format_key_value(definition, lang)].compact.join
|
||||
end
|
||||
|
||||
def format_base_translation(row)
|
||||
base_translation = row.translations[@default_lang]
|
||||
def format_base_translation(definition)
|
||||
base_translation = definition.translations[@default_lang]
|
||||
"# base translation: \"#{base_translation}\"\n" if base_translation
|
||||
end
|
||||
|
||||
|
@ -91,8 +76,8 @@ module Twine
|
|||
"msgstr \"%{value}\"\n"
|
||||
end
|
||||
|
||||
def format_comment(row, lang)
|
||||
"#. #{escape_quotes(row.comment)}\n" if row.comment
|
||||
def format_comment(definition, lang)
|
||||
"#. #{escape_quotes(definition.comment)}\n" if definition.comment
|
||||
end
|
||||
|
||||
def format_key(key)
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
module Twine
|
||||
module Formatters
|
||||
class Flash < Abstract
|
||||
include Twine::Placeholders
|
||||
|
||||
def format_name
|
||||
'flash'
|
||||
end
|
||||
|
@ -9,16 +11,13 @@ module Twine
|
|||
'.properties'
|
||||
end
|
||||
|
||||
def can_handle_directory?(path)
|
||||
return false
|
||||
end
|
||||
|
||||
def default_file_name
|
||||
return 'resources.properties'
|
||||
'resources.properties'
|
||||
end
|
||||
|
||||
def determine_language_given_path(path)
|
||||
return
|
||||
def set_translation_for_key(key, lang, value)
|
||||
value = convert_placeholders_from_flash_to_twine(value)
|
||||
super(key, lang, value)
|
||||
end
|
||||
|
||||
def read(io, lang)
|
||||
|
@ -28,36 +27,30 @@ module Twine
|
|||
if match
|
||||
key = match[1]
|
||||
value = match[2].strip
|
||||
value.gsub!(/\{[0-9]\}/, '%@')
|
||||
|
||||
set_translation_for_key(key, lang, value)
|
||||
if last_comment
|
||||
set_comment_for_key(key, last_comment)
|
||||
end
|
||||
set_comment_for_key(key, last_comment) if last_comment
|
||||
end
|
||||
|
||||
match = /# *(.*)/.match(line)
|
||||
if match
|
||||
last_comment = match[1]
|
||||
else
|
||||
last_comment = nil
|
||||
end
|
||||
last_comment = match ? match[1] : nil
|
||||
end
|
||||
end
|
||||
|
||||
def format_sections(strings, lang)
|
||||
def format_sections(twine_file, lang)
|
||||
super + "\n"
|
||||
end
|
||||
|
||||
def format_header(lang)
|
||||
"## Flash Strings File\n## Generated by Twine #{Twine::VERSION}\n## Language: #{lang}"
|
||||
"## Flash Strings File\n## Generated by Twine\n## Language: #{lang}"
|
||||
end
|
||||
|
||||
def format_section_header(section)
|
||||
"## #{section.name} ##\n"
|
||||
end
|
||||
|
||||
def format_comment(row, lang)
|
||||
"# #{row.comment}\n" if row.comment
|
||||
def format_comment(definition, lang)
|
||||
"# #{definition.comment}\n" if definition.comment
|
||||
end
|
||||
|
||||
def key_value_pattern
|
||||
|
@ -65,8 +58,7 @@ module Twine
|
|||
end
|
||||
|
||||
def format_value(value)
|
||||
placeHolderNumber = -1
|
||||
value.gsub(/%[d@]/) { placeHolderNumber += 1; '{%d}' % placeHolderNumber }
|
||||
convert_placeholders_from_twine_to_flash(value)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -11,24 +11,8 @@ module Twine
|
|||
'.po'
|
||||
end
|
||||
|
||||
def can_handle_directory?(path)
|
||||
Dir.entries(path).any? { |item| /^.+\.po$/.match(item) }
|
||||
end
|
||||
|
||||
def default_file_name
|
||||
return 'strings.po'
|
||||
end
|
||||
|
||||
def determine_language_given_path(path)
|
||||
path_arr = path.split(File::SEPARATOR)
|
||||
path_arr.each do |segment|
|
||||
match = /(..)\.po$/.match(segment)
|
||||
if match
|
||||
return match[1]
|
||||
end
|
||||
end
|
||||
|
||||
return
|
||||
'strings.po'
|
||||
end
|
||||
|
||||
def read(io, lang)
|
||||
|
@ -64,43 +48,43 @@ module Twine
|
|||
end
|
||||
|
||||
def format_file(lang)
|
||||
@default_lang = strings.language_codes[0]
|
||||
@default_lang = twine_file.language_codes[0]
|
||||
result = super
|
||||
@default_lang = nil
|
||||
result
|
||||
end
|
||||
|
||||
def format_header(lang)
|
||||
"msgid \"\"\nmsgstr \"\"\n\"Language: #{lang}\\n\"\n\"X-Generator: Twine #{Twine::VERSION}\\n\"\n"
|
||||
"msgid \"\"\nmsgstr \"\"\n\"Language: #{lang}\"\n\"X-Generator: Twine #{Twine::VERSION}\"\n"
|
||||
end
|
||||
|
||||
def format_section_header(section)
|
||||
"# SECTION: #{section.name}"
|
||||
end
|
||||
|
||||
def should_include_row(row, lang)
|
||||
super and row.translated_string_for_lang(@default_lang)
|
||||
def should_include_definition(definition, lang)
|
||||
super and !definition.translation_for_lang(@default_lang).nil?
|
||||
end
|
||||
|
||||
def format_comment(row, lang)
|
||||
"#. \"#{escape_quotes(row.comment)}\"\n" if row.comment
|
||||
def format_comment(definition, lang)
|
||||
"#. \"#{escape_quotes(definition.comment)}\"\n" if definition.comment
|
||||
end
|
||||
|
||||
def format_key_value(row, lang)
|
||||
value = row.translated_string_for_lang(lang)
|
||||
[format_key(row.key.dup), format_base_translation(row), format_value(value.dup)].compact.join
|
||||
def format_key_value(definition, lang)
|
||||
value = definition.translation_for_lang(lang)
|
||||
[format_key(definition.key.dup), format_base_translation(definition), format_value(value.dup)].compact.join
|
||||
end
|
||||
|
||||
def format_key(key)
|
||||
"msgctxt \"#{key}\"\n"
|
||||
"msgctxt \"#{escape_quotes(key)}\"\n"
|
||||
end
|
||||
|
||||
def format_base_translation(row)
|
||||
"msgid \"#{row.translations[@default_lang]}\"\n"
|
||||
def format_base_translation(definition)
|
||||
"msgid \"#{escape_quotes(definition.translations[@default_lang])}\"\n"
|
||||
end
|
||||
|
||||
def format_value(value)
|
||||
"msgstr \"#{value}\"\n"
|
||||
"msgstr \"#{escape_quotes(value)}\"\n"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -9,31 +9,22 @@ module Twine
|
|||
'.json'
|
||||
end
|
||||
|
||||
def can_handle_directory?(path)
|
||||
Dir.entries(path).any? { |item| /^.+\.json$/.match(item) }
|
||||
end
|
||||
|
||||
def default_file_name
|
||||
return 'localize.json'
|
||||
'localize.json'
|
||||
end
|
||||
|
||||
def determine_language_given_path(path)
|
||||
path_arr = path.split(File::SEPARATOR)
|
||||
path_arr.each do |segment|
|
||||
match = /^((.+)-)?([^-]+)\.json$/.match(segment)
|
||||
if match
|
||||
return match[3]
|
||||
end
|
||||
end
|
||||
match = /^.+([a-z]{2}-[A-Z]{2})\.json$/.match File.basename(path)
|
||||
return match[1] if match
|
||||
|
||||
return
|
||||
return super
|
||||
end
|
||||
|
||||
def read(io, lang)
|
||||
begin
|
||||
require "json"
|
||||
rescue LoadError
|
||||
raise Twine::Error.new "You must run 'gem install json' in order to read or write jquery-localize files."
|
||||
raise Twine::Error.new "You must run `gem install json` in order to read or write jquery-localize files."
|
||||
end
|
||||
|
||||
json = JSON.load(io)
|
||||
|
@ -48,8 +39,9 @@ module Twine
|
|||
"{\n#{super}\n}\n"
|
||||
end
|
||||
|
||||
def format_sections(strings, lang)
|
||||
sections = strings.sections.map { |section| format_section(section, lang) }
|
||||
def format_sections(twine_file, lang)
|
||||
sections = twine_file.sections.map { |section| format_section(section, lang) }
|
||||
sections.delete_if(&:empty?)
|
||||
sections.join(",\n\n")
|
||||
end
|
||||
|
||||
|
@ -57,11 +49,11 @@ module Twine
|
|||
end
|
||||
|
||||
def format_section(section, lang)
|
||||
rows = section.rows.dup
|
||||
definitions = section.definitions.dup
|
||||
|
||||
rows.map! { |row| format_row(row, lang) }
|
||||
rows.compact! # remove nil entries
|
||||
rows.join(",\n")
|
||||
definitions.map! { |definition| format_definition(definition, lang) }
|
||||
definitions.compact! # remove nil definitions
|
||||
definitions.join(",\n")
|
||||
end
|
||||
|
||||
def key_value_pattern
|
||||
|
|
|
@ -33,7 +33,7 @@ module Twine
|
|||
end
|
||||
|
||||
def default_file_name
|
||||
return 'strings.xml'
|
||||
'strings.xml'
|
||||
end
|
||||
|
||||
def determine_language_given_path(path)
|
||||
|
@ -91,10 +91,10 @@ module Twine
|
|||
end
|
||||
|
||||
def format_header(lang)
|
||||
"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Tizen Strings File -->\n<!-- Generated by Twine #{Twine::VERSION} -->\n<!-- Language: #{lang} -->"
|
||||
"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Tizen Strings File -->\n<!-- Generated by Twine -->\n<!-- Language: #{lang} -->"
|
||||
end
|
||||
|
||||
def format_sections(strings, lang)
|
||||
def format_sections(twine_file, lang)
|
||||
result = '<string_table Bversion="2.0.0.201311071819" Dversion="20120315">'
|
||||
|
||||
result += super + "\n"
|
||||
|
@ -106,8 +106,8 @@ module Twine
|
|||
"\t<!-- SECTION: #{section.name} -->"
|
||||
end
|
||||
|
||||
def format_comment(row, lang)
|
||||
"\t<!-- #{row.comment.gsub('--', '—')} -->\n" if row.comment
|
||||
def format_comment(definition, lang)
|
||||
"\t<!-- #{definition.comment.gsub('--', '—')} -->\n" if definition.comment
|
||||
end
|
||||
|
||||
def key_value_pattern
|
||||
|
|
|
@ -2,48 +2,62 @@ module Twine
|
|||
module Processors
|
||||
|
||||
class OutputProcessor
|
||||
def initialize(strings, options)
|
||||
@strings = strings
|
||||
def initialize(twine_file, options)
|
||||
@twine_file = twine_file
|
||||
@options = options
|
||||
end
|
||||
|
||||
def default_language
|
||||
@options[:developer_language] || @strings.language_codes[0]
|
||||
@options[:developer_language] || @twine_file.language_codes[0]
|
||||
end
|
||||
|
||||
def fallback_languages(language)
|
||||
fallback_mapping = {
|
||||
'zh-CN' => 'zh-Hans', # if we don't have a zh-CN translation, try zh-Hans before en
|
||||
'zh-TW' => 'zh-Hant' # if we don't have a zh-TW translation, try zh-Hant before en
|
||||
}
|
||||
|
||||
[fallback_mapping[language], default_language].flatten.compact
|
||||
# Regional dialect fallbacks to generic language (for example: 'es-MX' to 'es' instead of default 'en').
|
||||
if language.match(/([a-zA-Z]{2})-[a-zA-Z]+/)
|
||||
generic_language = language.gsub(/([a-zA-Z])-[a-zA-Z]+/, '\1')
|
||||
end
|
||||
|
||||
[fallback_mapping[language], generic_language, default_language].flatten.compact
|
||||
end
|
||||
|
||||
def process(language)
|
||||
result = StringsFile.new
|
||||
result = TwineFile.new
|
||||
|
||||
result.language_codes.concat @strings.language_codes
|
||||
@strings.sections.each do |section|
|
||||
new_section = StringsSection.new section.name
|
||||
result.language_codes.concat @twine_file.language_codes
|
||||
@twine_file.sections.each do |section|
|
||||
new_section = TwineSection.new section.name
|
||||
|
||||
section.rows.each do |row|
|
||||
next unless row.matches_tags?(@options[:tags], @options[:untagged])
|
||||
section.definitions.each do |definition|
|
||||
next unless definition.matches_tags?(@options[:tags], @options[:untagged])
|
||||
|
||||
value = row.translated_string_for_lang(language)
|
||||
value = definition.translation_for_lang(language)
|
||||
|
||||
next if value && @options[:include] == :untranslated
|
||||
|
||||
if value.nil? && @options[:include] != :translated
|
||||
value = row.translated_string_for_lang(fallback_languages(language))
|
||||
value = definition.translation_for_lang(fallback_languages(language))
|
||||
end
|
||||
|
||||
next unless value
|
||||
|
||||
new_row = row.dup
|
||||
new_row.translations[language] = value
|
||||
new_definition = definition.dup
|
||||
new_definition.translations[language] = value
|
||||
|
||||
new_section.rows << new_row
|
||||
result.strings_map[new_row.key] = new_row
|
||||
if definition.is_plural?
|
||||
# If definition is plural, but no translation found -> create
|
||||
# Then check 'other' key
|
||||
if !(new_definition.plural_translations[language] ||= {}).key? 'other'
|
||||
new_definition.plural_translations[language]['other'] = value
|
||||
end
|
||||
end
|
||||
|
||||
new_section.definitions << new_definition
|
||||
result.definitions_by_key[new_definition.key] = new_definition
|
||||
end
|
||||
|
||||
result.sections << new_section
|
||||
|
|
|
@ -2,34 +2,42 @@ module Twine
|
|||
module Placeholders
|
||||
extend self
|
||||
|
||||
PLACEHOLDER_FLAGS_WIDTH_PRECISION_LENGTH = '([-+ 0#])?(\d+|\*)?(\.(\d+|\*))?(hh?|ll?|L|z|j|t)?'
|
||||
# Note: the ` ` (single space) flag is NOT supported
|
||||
PLACEHOLDER_FLAGS_WIDTH_PRECISION_LENGTH = '([-+0#])?(\d+|\*)?(\.(\d+|\*))?(hh?|ll?|L|z|j|t|q)?'
|
||||
PLACEHOLDER_PARAMETER_FLAGS_WIDTH_PRECISION_LENGTH = '(\d+\$)?' + PLACEHOLDER_FLAGS_WIDTH_PRECISION_LENGTH
|
||||
PLACEHOLDER_TYPES = '[diufFeEgGxXoscpaA]'
|
||||
PLACEHOLDER_REGEX = /%#{PLACEHOLDER_PARAMETER_FLAGS_WIDTH_PRECISION_LENGTH}#{PLACEHOLDER_TYPES}/
|
||||
|
||||
def number_of_twine_placeholders(input)
|
||||
input.scan(PLACEHOLDER_REGEX).size
|
||||
end
|
||||
|
||||
def convert_twine_string_placeholder(input)
|
||||
# %@ -> %s
|
||||
input.gsub(/(%#{PLACEHOLDER_PARAMETER_FLAGS_WIDTH_PRECISION_LENGTH})@/, '\1s')
|
||||
end
|
||||
|
||||
# http://developer.android.com/guide/topics/resources/string-resource.html#FormattingAndStyling
|
||||
# http://stackoverflow.com/questions/4414389/android-xml-percent-symbol
|
||||
# https://github.com/mobiata/twine/pull/106
|
||||
def convert_placeholders_from_twine_to_android(input)
|
||||
placeholder_types = '[diufFeEgGxXoscpaA]'
|
||||
|
||||
# %@ -> %s
|
||||
value = input.gsub(/(%#{PLACEHOLDER_PARAMETER_FLAGS_WIDTH_PRECISION_LENGTH})@/, '\1s')
|
||||
value = convert_twine_string_placeholder(input)
|
||||
|
||||
placeholder_syntax = PLACEHOLDER_PARAMETER_FLAGS_WIDTH_PRECISION_LENGTH + placeholder_types
|
||||
placeholder_regex = /%#{placeholder_syntax}/
|
||||
|
||||
number_of_placeholders = value.scan(placeholder_regex).size
|
||||
number_of_placeholders = number_of_twine_placeholders(value)
|
||||
|
||||
return value if number_of_placeholders == 0
|
||||
|
||||
# got placeholders -> need to double single percent signs
|
||||
# % -> %% (but %% -> %%, %d -> %d)
|
||||
placeholder_syntax = PLACEHOLDER_PARAMETER_FLAGS_WIDTH_PRECISION_LENGTH + PLACEHOLDER_TYPES
|
||||
single_percent_regex = /([^%])(%)(?!(%|#{placeholder_syntax}))/
|
||||
value.gsub! single_percent_regex, '\1%%'
|
||||
|
||||
return value if number_of_placeholders < 2
|
||||
|
||||
# number placeholders
|
||||
non_numbered_placeholder_regex = /%(#{PLACEHOLDER_FLAGS_WIDTH_PRECISION_LENGTH}#{placeholder_types})/
|
||||
non_numbered_placeholder_regex = /%(#{PLACEHOLDER_FLAGS_WIDTH_PRECISION_LENGTH}#{PLACEHOLDER_TYPES})/
|
||||
|
||||
number_of_non_numbered_placeholders = value.scan(non_numbered_placeholder_regex).size
|
||||
|
||||
|
@ -50,5 +58,25 @@ module Twine
|
|||
# %s -> %@
|
||||
input.gsub(placeholder_regex, '\1@')
|
||||
end
|
||||
|
||||
# http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/mx/resources/IResourceManager.html#getString()
|
||||
# http://soenkerohde.com/2008/07/flex-localization/comment-page-1/
|
||||
def convert_placeholders_from_twine_to_flash(input)
|
||||
value = convert_twine_string_placeholder(input)
|
||||
|
||||
value.gsub(PLACEHOLDER_REGEX).each_with_index do |match, index|
|
||||
"{#{index}}"
|
||||
end
|
||||
end
|
||||
|
||||
def convert_placeholders_from_flash_to_twine(input)
|
||||
input.gsub /\{\d+\}/, '%@'
|
||||
end
|
||||
|
||||
# Python supports placeholders in the form of `%(amount)03d`
|
||||
# see https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting
|
||||
def contains_python_specific_placeholder(input)
|
||||
/%\([a-zA-Z0-9_-]+\)#{PLACEHOLDER_PARAMETER_FLAGS_WIDTH_PRECISION_LENGTH}#{PLACEHOLDER_TYPES}/.match(input) != nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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,45 +5,58 @@ Twine::Plugin.new # Initialize plugins first in Runner.
|
|||
|
||||
module Twine
|
||||
class Runner
|
||||
class NullOutput
|
||||
def puts(message)
|
||||
end
|
||||
def string
|
||||
""
|
||||
end
|
||||
end
|
||||
|
||||
def self.run(args)
|
||||
options = CLI.parse(args)
|
||||
|
||||
return unless options
|
||||
|
||||
strings = StringsFile.new
|
||||
strings.read options[:strings_file]
|
||||
runner = new(options, strings)
|
||||
twine_file = TwineFile.new
|
||||
twine_file.read options[:twine_file]
|
||||
runner = new(options, twine_file)
|
||||
|
||||
case options[:command]
|
||||
when 'generate-string-file'
|
||||
runner.generate_string_file
|
||||
when 'generate-all-string-files'
|
||||
runner.generate_all_string_files
|
||||
when 'consume-string-file'
|
||||
runner.consume_string_file
|
||||
when 'consume-all-string-files'
|
||||
runner.consume_all_string_files
|
||||
when 'generate-loc-drop'
|
||||
runner.generate_loc_drop
|
||||
when 'consume-loc-drop'
|
||||
runner.consume_loc_drop
|
||||
when 'validate-strings-file'
|
||||
runner.validate_strings_file
|
||||
when 'generate-localization-file'
|
||||
runner.generate_localization_file
|
||||
when 'generate-all-localization-files'
|
||||
runner.generate_all_localization_files
|
||||
when 'consume-localization-file'
|
||||
runner.consume_localization_file
|
||||
when 'consume-all-localization-files'
|
||||
runner.consume_all_localization_files
|
||||
when 'generate-localization-archive'
|
||||
runner.generate_localization_archive
|
||||
when 'consume-localization-archive'
|
||||
runner.consume_localization_archive
|
||||
when 'validate-twine-file'
|
||||
runner.validate_twine_file
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(options = {}, strings = StringsFile.new)
|
||||
def initialize(options = {}, twine_file = TwineFile.new)
|
||||
@options = options
|
||||
@strings = strings
|
||||
end
|
||||
|
||||
def write_strings_data(path)
|
||||
if @options[:developer_language]
|
||||
@strings.set_developer_language_code(@options[:developer_language])
|
||||
@twine_file = twine_file
|
||||
if @options[:quite]
|
||||
Twine::stdout = NullOutput.new
|
||||
end
|
||||
@strings.write(path)
|
||||
end
|
||||
|
||||
def generate_string_file
|
||||
validate_strings_file if @options[:validate]
|
||||
def write_twine_data(path)
|
||||
if @options[:developer_language]
|
||||
@twine_file.set_developer_language_code(@options[:developer_language])
|
||||
end
|
||||
@twine_file.write(path)
|
||||
end
|
||||
|
||||
def generate_localization_file
|
||||
validate_twine_file if @options[:validate]
|
||||
|
||||
lang = nil
|
||||
lang = @options[:languages][0] if @options[:languages]
|
||||
|
@ -51,13 +64,13 @@ module Twine
|
|||
formatter, lang = prepare_read_write(@options[:output_path], lang)
|
||||
output = formatter.format_file(lang)
|
||||
|
||||
raise Twine::Error.new "Nothing to generate! The resulting file would not contain any strings." unless output
|
||||
raise Twine::Error.new "Nothing to generate! The resulting file would not contain any translations." unless output
|
||||
|
||||
IO.write(@options[:output_path], output, encoding: encoding)
|
||||
IO.write(@options[:output_path], output, encoding: output_encoding)
|
||||
end
|
||||
|
||||
def generate_all_string_files
|
||||
validate_strings_file if @options[:validate]
|
||||
def generate_all_localization_files
|
||||
validate_twine_file if @options[:validate]
|
||||
|
||||
if !File.directory?(@options[:output_path])
|
||||
if @options[:create_folders]
|
||||
|
@ -67,16 +80,19 @@ module Twine
|
|||
end
|
||||
end
|
||||
|
||||
formatter_for_directory = find_formatter { |f| f.can_handle_directory?(@options[:output_path]) }
|
||||
formatter = formatter_for_format(@options[:format]) || formatter_for_directory
|
||||
if @options[:format]
|
||||
formatter = formatter_for_format(@options[:format])
|
||||
else
|
||||
formatter = find_formatter { |f| f.can_handle_directory?(@options[:output_path]) }
|
||||
end
|
||||
|
||||
unless formatter
|
||||
raise Twine::Error.new "Could not determine format given the contents of #{@options[:output_path]}"
|
||||
raise Twine::Error.new "Could not determine format given the contents of #{@options[:output_path]}. Try using `--format`."
|
||||
end
|
||||
|
||||
file_name = @options[:file_name] || formatter.default_file_name
|
||||
if @options[:create_folders]
|
||||
@strings.language_codes.each do |lang|
|
||||
@twine_file.language_codes.each do |lang|
|
||||
output_path = File.join(@options[:output_path], formatter.output_path_for_language(lang))
|
||||
|
||||
FileUtils.mkdir_p(output_path)
|
||||
|
@ -85,11 +101,11 @@ module Twine
|
|||
|
||||
output = formatter.format_file(lang)
|
||||
unless output
|
||||
Twine::stderr.puts "Skipping file at path #{file_path} since it would not contain any strings."
|
||||
Twine::stdout.puts "Skipping file at path #{file_path} since it would not contain any translations."
|
||||
next
|
||||
end
|
||||
|
||||
IO.write(file_path, output, encoding: encoding)
|
||||
IO.write(file_path, output, encoding: output_encoding)
|
||||
end
|
||||
else
|
||||
language_found = false
|
||||
|
@ -107,11 +123,11 @@ module Twine
|
|||
file_path = File.join(output_path, file_name)
|
||||
output = formatter.format_file(lang)
|
||||
unless output
|
||||
Twine::stderr.puts "Skipping file at path #{file_path} since it would not contain any strings."
|
||||
Twine::stdout.puts "Skipping file at path #{file_path} since it would not contain any translations."
|
||||
next
|
||||
end
|
||||
|
||||
IO.write(file_path, output, encoding: encoding)
|
||||
IO.write(file_path, output, encoding: output_encoding)
|
||||
end
|
||||
|
||||
unless language_found
|
||||
|
@ -121,38 +137,8 @@ module Twine
|
|||
|
||||
end
|
||||
|
||||
def consume_string_file
|
||||
lang = nil
|
||||
if @options[:languages]
|
||||
lang = @options[:languages][0]
|
||||
end
|
||||
|
||||
read_string_file(@options[:input_path], lang)
|
||||
output_path = @options[:output_path] || @options[:strings_file]
|
||||
write_strings_data(output_path)
|
||||
end
|
||||
|
||||
def consume_all_string_files
|
||||
if !File.directory?(@options[:input_path])
|
||||
raise Twine::Error.new("Directory does not exist: #{@options[:output_path]}")
|
||||
end
|
||||
|
||||
Dir.glob(File.join(@options[:input_path], "**/*")) do |item|
|
||||
if File.file?(item)
|
||||
begin
|
||||
read_string_file(item)
|
||||
rescue Twine::Error => e
|
||||
Twine::stderr.puts "#{e.message}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
output_path = @options[:output_path] || @options[:strings_file]
|
||||
write_strings_data(output_path)
|
||||
end
|
||||
|
||||
def generate_loc_drop
|
||||
validate_strings_file if @options[:validate]
|
||||
def generate_localization_archive
|
||||
validate_twine_file if @options[:validate]
|
||||
|
||||
require_rubyzip
|
||||
|
||||
|
@ -165,7 +151,7 @@ module Twine
|
|||
zipfile.mkdir('Locales')
|
||||
|
||||
formatter = formatter_for_format(@options[:format])
|
||||
@strings.language_codes.each do |lang|
|
||||
@twine_file.language_codes.each do |lang|
|
||||
if @options[:languages] == nil || @options[:languages].length == 0 || @options[:languages].include?(lang)
|
||||
file_name = lang + formatter.extension
|
||||
temp_path = File.join(temp_dir, file_name)
|
||||
|
@ -173,11 +159,11 @@ module Twine
|
|||
|
||||
output = formatter.format_file(lang)
|
||||
unless output
|
||||
Twine::stderr.puts "Skipping file #{file_name} since it would not contain any strings."
|
||||
Twine::stdout.puts "Skipping file #{file_name} since it would not contain any translations."
|
||||
next
|
||||
end
|
||||
|
||||
IO.write(temp_path, output, encoding: encoding)
|
||||
IO.write(temp_path, output, encoding: output_encoding)
|
||||
zipfile.add(zip_path, temp_path)
|
||||
end
|
||||
end
|
||||
|
@ -185,13 +171,44 @@ module Twine
|
|||
end
|
||||
end
|
||||
|
||||
def consume_loc_drop
|
||||
def consume_localization_file
|
||||
lang = nil
|
||||
if @options[:languages]
|
||||
lang = @options[:languages][0]
|
||||
end
|
||||
|
||||
read_localization_file(@options[:input_path], lang)
|
||||
output_path = @options[:output_path] || @options[:twine_file]
|
||||
write_twine_data(output_path)
|
||||
end
|
||||
|
||||
def consume_all_localization_files
|
||||
if !File.directory?(@options[:input_path])
|
||||
raise Twine::Error.new("Directory does not exist: #{@options[:input_path]}")
|
||||
end
|
||||
|
||||
Dir.glob(File.join(@options[:input_path], "**/*")) do |item|
|
||||
if File.file?(item)
|
||||
begin
|
||||
read_localization_file(item)
|
||||
rescue Twine::Error => e
|
||||
Twine::stderr.puts "#{e.message}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
output_path = @options[:output_path] || @options[:twine_file]
|
||||
write_twine_data(output_path)
|
||||
end
|
||||
|
||||
def consume_localization_archive
|
||||
require_rubyzip
|
||||
|
||||
if !File.file?(@options[:input_path])
|
||||
raise Twine::Error.new("File does not exist: #{@options[:input_path]}")
|
||||
end
|
||||
|
||||
error_encountered = false
|
||||
Dir.mktmpdir do |temp_dir|
|
||||
Zip::File.open(@options[:input_path]) do |zipfile|
|
||||
zipfile.each do |entry|
|
||||
|
@ -201,36 +218,44 @@ module Twine
|
|||
FileUtils.mkdir_p(File.dirname(real_path))
|
||||
zipfile.extract(entry.name, real_path)
|
||||
begin
|
||||
read_string_file(real_path)
|
||||
read_localization_file(real_path)
|
||||
rescue Twine::Error => e
|
||||
Twine::stderr.puts "#{e.message}"
|
||||
error_encountered = true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
output_path = @options[:output_path] || @options[:strings_file]
|
||||
write_strings_data(output_path)
|
||||
output_path = @options[:output_path] || @options[:twine_file]
|
||||
write_twine_data(output_path)
|
||||
|
||||
if error_encountered
|
||||
raise Twine::Error.new("At least one file could not be consumed")
|
||||
end
|
||||
end
|
||||
|
||||
def validate_strings_file
|
||||
total_strings = 0
|
||||
def validate_twine_file
|
||||
total_definitions = 0
|
||||
all_keys = Set.new
|
||||
duplicate_keys = Set.new
|
||||
keys_without_tags = Set.new
|
||||
invalid_keys = Set.new
|
||||
keys_with_python_only_placeholders = Set.new
|
||||
valid_key_regex = /^[A-Za-z0-9_]+$/
|
||||
|
||||
@strings.sections.each do |section|
|
||||
section.rows.each do |row|
|
||||
total_strings += 1
|
||||
@twine_file.sections.each do |section|
|
||||
section.definitions.each do |definition|
|
||||
total_definitions += 1
|
||||
|
||||
duplicate_keys.add(row.key) if all_keys.include? row.key
|
||||
all_keys.add(row.key)
|
||||
duplicate_keys.add(definition.key) if all_keys.include? definition.key
|
||||
all_keys.add(definition.key)
|
||||
|
||||
keys_without_tags.add(row.key) if row.tags == nil or row.tags.length == 0
|
||||
keys_without_tags.add(definition.key) if definition.tags == nil or definition.tags.length == 0
|
||||
|
||||
invalid_keys << row.key unless row.key =~ valid_key_regex
|
||||
invalid_keys << definition.key unless definition.key =~ valid_key_regex
|
||||
|
||||
keys_with_python_only_placeholders << definition.key if definition.translations.values.any? { |v| Placeholders.contains_python_specific_placeholder(v) }
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -238,14 +263,14 @@ module Twine
|
|||
join_keys = lambda { |set| set.map { |k| " " + k }.join("\n") }
|
||||
|
||||
unless duplicate_keys.empty?
|
||||
errors << "Found duplicate string key(s):\n#{join_keys.call(duplicate_keys)}"
|
||||
errors << "Found duplicate key(s):\n#{join_keys.call(duplicate_keys)}"
|
||||
end
|
||||
|
||||
if @options[:pedantic]
|
||||
if keys_without_tags.length == total_strings
|
||||
errors << "None of your strings have tags."
|
||||
if keys_without_tags.length == total_definitions
|
||||
errors << "None of your definitions have tags."
|
||||
elsif keys_without_tags.length > 0
|
||||
errors << "Found strings without tags:\n#{join_keys.call(keys_without_tags)}"
|
||||
errors << "Found definitions without tags:\n#{join_keys.call(keys_without_tags)}"
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -253,71 +278,78 @@ module Twine
|
|||
errors << "Found key(s) with invalid characters:\n#{join_keys.call(invalid_keys)}"
|
||||
end
|
||||
|
||||
unless keys_with_python_only_placeholders.empty?
|
||||
errors << "Found key(s) with placeholders that are only supported by Python:\n#{join_keys.call(keys_with_python_only_placeholders)}"
|
||||
end
|
||||
|
||||
raise Twine::Error.new errors.join("\n\n") unless errors.empty?
|
||||
|
||||
Twine::stdout.puts "#{@options[:strings_file]} is valid."
|
||||
Twine::stdout.puts "#{@options[:twine_file]} is valid."
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def encoding
|
||||
@options[:output_encoding] || 'UTF-8'
|
||||
def output_encoding
|
||||
@options[:encoding] || 'UTF-8'
|
||||
end
|
||||
|
||||
def require_rubyzip
|
||||
begin
|
||||
require 'zip'
|
||||
rescue LoadError
|
||||
raise Twine::Error.new "You must run 'gem install rubyzip' in order to create or consume localization drops."
|
||||
raise Twine::Error.new "You must run 'gem install rubyzip' in order to create or consume localization archives."
|
||||
end
|
||||
end
|
||||
|
||||
def determine_language_given_path(path)
|
||||
code = File.basename(path, File.extname(path))
|
||||
return code if @strings.language_codes.include? code
|
||||
end
|
||||
|
||||
def formatter_for_format(format)
|
||||
find_formatter { |f| f.format_name == format }
|
||||
end
|
||||
|
||||
def find_formatter(&block)
|
||||
formatter = Formatters.formatters.find &block
|
||||
return nil unless formatter
|
||||
formatter.strings = @strings
|
||||
formatters = Formatters.formatters.select(&block)
|
||||
if formatters.empty?
|
||||
return nil
|
||||
elsif formatters.size > 1
|
||||
raise Twine::Error.new("Unable to determine format. Candidates are: #{formatters.map(&:format_name).join(', ')}. Please specify the format you want using `--format`")
|
||||
end
|
||||
formatter = formatters.first
|
||||
formatter.twine_file = @twine_file
|
||||
formatter.options = @options
|
||||
formatter
|
||||
end
|
||||
|
||||
def read_string_file(path, lang = nil)
|
||||
def read_localization_file(path, lang = nil)
|
||||
unless File.file?(path)
|
||||
raise Twine::Error.new("File does not exist: #{path}")
|
||||
end
|
||||
|
||||
formatter, lang = prepare_read_write(path, lang)
|
||||
|
||||
encoding = @options[:encoding] || Twine::Encoding.encoding_for_path(path)
|
||||
external_encoding = @options[:encoding] || Twine::Encoding.encoding_for_path(path)
|
||||
|
||||
IO.open(IO.sysopen(path, 'rb'), 'rb', external_encoding: encoding, internal_encoding: 'UTF-8') do |io|
|
||||
IO.open(IO.sysopen(path, 'rb'), 'rb', external_encoding: external_encoding, internal_encoding: 'UTF-8') do |io|
|
||||
io.read(2) if Twine::Encoding.has_bom?(path)
|
||||
formatter.read(io, lang)
|
||||
end
|
||||
end
|
||||
|
||||
def prepare_read_write(path, lang)
|
||||
formatter_for_path = find_formatter { |f| f.extension == File.extname(path) }
|
||||
formatter = formatter_for_format(@options[:format]) || formatter_for_path
|
||||
if @options[:format]
|
||||
formatter = formatter_for_format(@options[:format])
|
||||
else
|
||||
formatter = find_formatter { |f| f.extension == File.extname(path) }
|
||||
end
|
||||
|
||||
unless formatter
|
||||
raise Twine::Error.new "Unable to determine format of #{path}"
|
||||
raise Twine::Error.new "Unable to determine format of #{path}. Try using `--format`."
|
||||
end
|
||||
|
||||
lang = lang || determine_language_given_path(path) || formatter.determine_language_given_path(path)
|
||||
lang = lang || formatter.determine_language_given_path(path)
|
||||
unless lang
|
||||
raise Twine::Error.new "Unable to determine language for #{path}"
|
||||
raise Twine::Error.new "Unable to determine language for #{path}. Try using `--lang`."
|
||||
end
|
||||
|
||||
@strings.language_codes << lang unless @strings.language_codes.include? lang
|
||||
@twine_file.language_codes << lang unless @twine_file.language_codes.include? lang
|
||||
|
||||
return formatter, lang
|
||||
end
|
||||
|
|
|
@ -1,19 +1,13 @@
|
|||
module Twine
|
||||
class StringsSection
|
||||
attr_reader :name
|
||||
attr_reader :rows
|
||||
class TwineDefinition
|
||||
PLURAL_KEYS = %w(zero one two few many other)
|
||||
|
||||
def initialize(name)
|
||||
@name = name
|
||||
@rows = []
|
||||
end
|
||||
end
|
||||
|
||||
class StringsRow
|
||||
attr_reader :key
|
||||
attr_accessor :comment
|
||||
attr_accessor :tags
|
||||
attr_reader :translations
|
||||
attr_reader :plural_translations
|
||||
attr_reader :is_plural
|
||||
attr_accessor :reference
|
||||
attr_accessor :reference_key
|
||||
|
||||
|
@ -22,6 +16,7 @@ module Twine
|
|||
@comment = nil
|
||||
@tags = nil
|
||||
@translations = {}
|
||||
@plural_translations = {}
|
||||
end
|
||||
|
||||
def comment
|
||||
|
@ -32,34 +27,59 @@ module Twine
|
|||
@comment
|
||||
end
|
||||
|
||||
# [['tag1', 'tag2'], ['~tag3']] == (tag1 OR tag2) AND (!tag3)
|
||||
def matches_tags?(tags, include_untagged)
|
||||
if tags == nil || tags.empty?
|
||||
# The user did not specify any tags. Everything passes.
|
||||
if tags == nil || tags.empty? # The user did not specify any tags. Everything passes.
|
||||
return true
|
||||
elsif @tags == nil
|
||||
# This row has no tags.
|
||||
elsif @tags == nil # This definition has no tags -> check reference (if any)
|
||||
return reference ? reference.matches_tags?(tags, include_untagged) : include_untagged
|
||||
elsif @tags.empty?
|
||||
return include_untagged
|
||||
else
|
||||
return !(tags & @tags).empty?
|
||||
return tags.all? do |set|
|
||||
regular_tags, negated_tags = set.partition { |tag| tag[0] != '~' }
|
||||
negated_tags.map! { |tag| tag[1..-1] }
|
||||
matches_regular_tags = (!regular_tags.empty? && !(regular_tags & @tags).empty?)
|
||||
matches_negated_tags = (!negated_tags.empty? && (negated_tags & @tags).empty?)
|
||||
matches_regular_tags or matches_negated_tags
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
def translated_string_for_lang(lang)
|
||||
translation = [lang].flatten.map { |l| @translations[l] }.first
|
||||
def translation_for_lang(lang)
|
||||
translation = [lang].flatten.map { |l| @translations[l] }.compact.first
|
||||
|
||||
translation = reference.translated_string_for_lang(lang) if translation.nil? && reference
|
||||
translation = reference.translation_for_lang(lang) if translation.nil? && reference
|
||||
|
||||
return translation
|
||||
end
|
||||
|
||||
def plural_translation_for_lang(lang)
|
||||
if @plural_translations.has_key? lang
|
||||
@plural_translations[lang].dup.sort_by { |key,_| TwineDefinition::PLURAL_KEYS.index(key) }.to_h
|
||||
end
|
||||
end
|
||||
|
||||
def is_plural?
|
||||
!@plural_translations.empty?
|
||||
end
|
||||
end
|
||||
|
||||
class StringsFile
|
||||
class TwineSection
|
||||
attr_reader :name
|
||||
attr_reader :definitions
|
||||
|
||||
def initialize(name)
|
||||
@name = name
|
||||
@definitions = []
|
||||
end
|
||||
end
|
||||
|
||||
class TwineFile
|
||||
attr_reader :sections
|
||||
attr_reader :strings_map
|
||||
attr_reader :definitions_by_key
|
||||
attr_reader :language_codes
|
||||
|
||||
private
|
||||
|
@ -73,7 +93,7 @@ module Twine
|
|||
|
||||
def initialize
|
||||
@sections = []
|
||||
@strings_map = {}
|
||||
@definitions_by_key = {}
|
||||
@language_codes = []
|
||||
end
|
||||
|
||||
|
@ -102,7 +122,7 @@ module Twine
|
|||
File.open(path, 'r:UTF-8') do |f|
|
||||
line_num = 0
|
||||
current_section = nil
|
||||
current_row = nil
|
||||
current_definition = nil
|
||||
while line = f.gets
|
||||
parsed = false
|
||||
line.strip!
|
||||
|
@ -115,42 +135,54 @@ module Twine
|
|||
if line.length > 4 && line[0, 2] == '[['
|
||||
match = /^\[\[(.+)\]\]$/.match(line)
|
||||
if match
|
||||
current_section = StringsSection.new(match[1])
|
||||
current_section = TwineSection.new(match[1])
|
||||
@sections << current_section
|
||||
parsed = true
|
||||
end
|
||||
elsif line.length > 2 && line[0, 1] == '['
|
||||
key = match_key(line)
|
||||
if key
|
||||
current_row = StringsRow.new(key)
|
||||
@strings_map[current_row.key] = current_row
|
||||
current_definition = TwineDefinition.new(key)
|
||||
@definitions_by_key[current_definition.key] = current_definition
|
||||
if !current_section
|
||||
current_section = StringsSection.new('')
|
||||
current_section = TwineSection.new('')
|
||||
@sections << current_section
|
||||
end
|
||||
current_section.rows << current_row
|
||||
current_section.definitions << current_definition
|
||||
parsed = true
|
||||
end
|
||||
else
|
||||
match = /^([^=]+)=(.*)$/.match(line)
|
||||
match = /^([^:=]+)(?::([^=]+))?=(.*)$/.match(line)
|
||||
if match
|
||||
key = match[1].strip
|
||||
value = match[2].strip
|
||||
|
||||
plural_key = match[2].to_s.strip
|
||||
value = match[3].strip
|
||||
|
||||
value = value[1..-2] if value[0] == '`' && value[-1] == '`'
|
||||
|
||||
case key
|
||||
when 'comment'
|
||||
current_row.comment = value
|
||||
current_definition.comment = value
|
||||
when 'tags'
|
||||
current_row.tags = value.split(',')
|
||||
current_definition.tags = value.split(',')
|
||||
when 'ref'
|
||||
current_row.reference_key = value if value
|
||||
current_definition.reference_key = value if value
|
||||
else
|
||||
if !@language_codes.include? key
|
||||
add_language_code(key)
|
||||
end
|
||||
current_row.translations[key] = value
|
||||
# Providing backward compatibility
|
||||
# for formatters without plural support
|
||||
if plural_key.empty? || plural_key == 'other'
|
||||
current_definition.translations[key] = value
|
||||
end
|
||||
if !plural_key.empty?
|
||||
if !TwineDefinition::PLURAL_KEYS.include? plural_key
|
||||
warn("Unknown plural key #{plural_key}")
|
||||
next
|
||||
end
|
||||
(current_definition.plural_translations[key] ||= {})[plural_key] = value
|
||||
end
|
||||
end
|
||||
parsed = true
|
||||
end
|
||||
|
@ -163,9 +195,9 @@ module Twine
|
|||
end
|
||||
|
||||
# resolve_references
|
||||
@strings_map.each do |key, row|
|
||||
next unless row.reference_key
|
||||
row.reference = @strings_map[row.reference_key]
|
||||
@definitions_by_key.each do |key, definition|
|
||||
next unless definition.reference_key
|
||||
definition.reference = @definitions_by_key[definition.reference_key]
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -180,26 +212,26 @@ module Twine
|
|||
|
||||
f.puts "[[#{section.name}]]"
|
||||
|
||||
section.rows.each do |row|
|
||||
f.puts "\t[#{row.key}]"
|
||||
section.definitions.each do |definition|
|
||||
f.puts "\t[#{definition.key}]"
|
||||
|
||||
value = write_value(row, dev_lang, f)
|
||||
if !value && !row.reference_key
|
||||
puts "Warning: #{row.key} does not exist in developer language '#{dev_lang}'"
|
||||
value = write_value(definition, dev_lang, f)
|
||||
if !value && !definition.reference_key
|
||||
Twine::stdout.puts "WARNING: #{definition.key} does not exist in developer language '#{dev_lang}'"
|
||||
end
|
||||
|
||||
if row.reference_key
|
||||
f.puts "\t\tref = #{row.reference_key}"
|
||||
if definition.reference_key
|
||||
f.puts "\t\tref = #{definition.reference_key}"
|
||||
end
|
||||
if row.tags && row.tags.length > 0
|
||||
tag_str = row.tags.join(',')
|
||||
if definition.tags && definition.tags.length > 0
|
||||
tag_str = definition.tags.join(',')
|
||||
f.puts "\t\ttags = #{tag_str}"
|
||||
end
|
||||
if row.raw_comment and row.raw_comment.length > 0
|
||||
f.puts "\t\tcomment = #{row.raw_comment}"
|
||||
if definition.raw_comment and definition.raw_comment.length > 0
|
||||
f.puts "\t\tcomment = #{definition.raw_comment}"
|
||||
end
|
||||
@language_codes[1..-1].each do |lang|
|
||||
write_value(row, lang, f)
|
||||
write_value(definition, lang, f)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -208,8 +240,8 @@ module Twine
|
|||
|
||||
private
|
||||
|
||||
def write_value(row, language, file)
|
||||
value = row.translations[language]
|
||||
def write_value(definition, language, file)
|
||||
value = definition.translations[language]
|
||||
return nil unless value
|
||||
|
||||
if value[0] == ' ' || value[-1] == ' ' || (value[0] == '`' && value[-1] == '`')
|
|
@ -1,3 +1,3 @@
|
|||
module Twine
|
||||
VERSION = '0.9.0'
|
||||
VERSION = '1.1.2-om'
|
||||
end
|
||||
|
|
14
test/command_test.rb
Normal file
14
test/command_test.rb
Normal file
|
@ -0,0 +1,14 @@
|
|||
require 'twine_test'
|
||||
|
||||
class CommandTest < TwineTest
|
||||
def prepare_mock_formatter(formatter_class, clear_other_formatters = true)
|
||||
twine_file = Twine::TwineFile.new
|
||||
twine_file.language_codes.concat KNOWN_LANGUAGES
|
||||
|
||||
formatter = formatter_class.new
|
||||
formatter.twine_file = twine_file
|
||||
Twine::Formatters.formatters.clear if clear_other_formatters
|
||||
Twine::Formatters.formatters << formatter
|
||||
formatter
|
||||
end
|
||||
end
|
|
@ -1,14 +0,0 @@
|
|||
require 'twine_test_case'
|
||||
|
||||
class CommandTestCase < TwineTestCase
|
||||
def prepare_mock_formatter(formatter_class)
|
||||
strings = Twine::StringsFile.new
|
||||
strings.language_codes.concat KNOWN_LANGUAGES
|
||||
|
||||
formatter = formatter_class.new
|
||||
formatter.strings = strings
|
||||
Twine::Formatters.formatters.clear
|
||||
Twine::Formatters.formatters << formatter
|
||||
formatter
|
||||
end
|
||||
end
|
BIN
test/fixtures/consume_loc_drop.zip
vendored
BIN
test/fixtures/consume_loc_drop.zip
vendored
Binary file not shown.
BIN
test/fixtures/consume_localization_archive.zip
vendored
Normal file
BIN
test/fixtures/consume_localization_archive.zip
vendored
Normal file
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 */
|
||||
|
|
15
test/fixtures/formatter_django.po
vendored
15
test/fixtures/formatter_django.po
vendored
|
@ -1,10 +1,11 @@
|
|||
##
|
||||
# Django Strings File
|
||||
# Generated by Twine <%= Twine::VERSION %>
|
||||
# Language: en
|
||||
# Django Strings File
|
||||
# Generated by Twine
|
||||
# Language: en
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
|
||||
|
||||
#--------- Section 1 ---------#
|
||||
# --------- Section 1 --------- #
|
||||
|
||||
#. comment key1
|
||||
# base translation: "value1-english"
|
||||
|
@ -16,7 +17,7 @@ msgid "key2"
|
|||
msgstr "value2-english"
|
||||
|
||||
|
||||
#--------- Section 2 ---------#
|
||||
# --------- Section 2 --------- #
|
||||
|
||||
# base translation: "value3-english"
|
||||
msgid "key3"
|
||||
|
|
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 -->
|
||||
|
|
|
@ -1,164 +1,164 @@
|
|||
require 'twine_test_case'
|
||||
require 'twine_test'
|
||||
|
||||
class TestAbstractFormatter < TwineTestCase
|
||||
class SetTranslation < TwineTestCase
|
||||
class TestAbstractFormatter < TwineTest
|
||||
class SetTranslation < TwineTest
|
||||
def setup
|
||||
super
|
||||
|
||||
@strings = build_twine_file 'en', 'fr' do
|
||||
@twine_file = build_twine_file 'en', 'fr' do
|
||||
add_section 'Section' do
|
||||
add_row key1: 'value1-english'
|
||||
add_row key2: { en: 'value2-english', fr: 'value2-french' }
|
||||
add_definition key1: 'value1-english'
|
||||
add_definition key2: { en: 'value2-english', fr: 'value2-french' }
|
||||
end
|
||||
end
|
||||
|
||||
@formatter = Twine::Formatters::Abstract.new
|
||||
@formatter.strings = @strings
|
||||
@formatter.twine_file = @twine_file
|
||||
end
|
||||
|
||||
def test_set_translation_updates_existing_value
|
||||
@formatter.set_translation_for_key 'key1', 'en', 'value1-english-updated'
|
||||
|
||||
assert_equal 'value1-english-updated', @strings.strings_map['key1'].translations['en']
|
||||
assert_equal 'value1-english-updated', @twine_file.definitions_by_key['key1'].translations['en']
|
||||
end
|
||||
|
||||
def test_set_translation_does_not_alter_other_language
|
||||
@formatter.set_translation_for_key 'key2', 'en', 'value2-english-updated'
|
||||
|
||||
assert_equal 'value2-french', @strings.strings_map['key2'].translations['fr']
|
||||
assert_equal 'value2-french', @twine_file.definitions_by_key['key2'].translations['fr']
|
||||
end
|
||||
|
||||
def test_set_translation_escapes_newlines
|
||||
@formatter.set_translation_for_key 'key1', 'en', "new\nline"
|
||||
|
||||
assert_equal 'new\nline', @strings.strings_map['key1'].translations['en']
|
||||
assert_equal 'new\nline', @twine_file.definitions_by_key['key1'].translations['en']
|
||||
end
|
||||
|
||||
def test_set_translation_adds_translation_to_existing_key
|
||||
@formatter.set_translation_for_key 'key1', 'fr', 'value1-french'
|
||||
|
||||
assert_equal 'value1-french', @strings.strings_map['key1'].translations['fr']
|
||||
assert_equal 'value1-french', @twine_file.definitions_by_key['key1'].translations['fr']
|
||||
end
|
||||
|
||||
def test_set_translation_does_not_add_new_key
|
||||
@formatter.set_translation_for_key 'new-key', 'en', 'new-key-english'
|
||||
|
||||
assert_nil @strings.strings_map['new-key']
|
||||
assert_nil @twine_file.definitions_by_key['new-key']
|
||||
end
|
||||
|
||||
def test_set_translation_consume_all_adds_new_key
|
||||
formatter = Twine::Formatters::Abstract.new
|
||||
formatter.strings = @strings
|
||||
formatter.twine_file = @twine_file
|
||||
formatter.options = { consume_all: true }
|
||||
formatter.set_translation_for_key 'new-key', 'en', 'new-key-english'
|
||||
|
||||
assert_equal 'new-key-english', @strings.strings_map['new-key'].translations['en']
|
||||
assert_equal 'new-key-english', @twine_file.definitions_by_key['new-key'].translations['en']
|
||||
end
|
||||
|
||||
def test_set_translation_consume_all_adds_tags
|
||||
random_tag = SecureRandom.uuid
|
||||
formatter = Twine::Formatters::Abstract.new
|
||||
formatter.strings = @strings
|
||||
formatter.twine_file = @twine_file
|
||||
formatter.options = { consume_all: true, tags: [random_tag] }
|
||||
formatter.set_translation_for_key 'new-key', 'en', 'new-key-english'
|
||||
|
||||
assert_equal [random_tag], @strings.strings_map['new-key'].tags
|
||||
assert_equal [random_tag], @twine_file.definitions_by_key['new-key'].tags
|
||||
end
|
||||
|
||||
def test_set_translation_adds_new_keys_to_category_uncategoriezed
|
||||
formatter = Twine::Formatters::Abstract.new
|
||||
formatter.strings = @strings
|
||||
formatter.twine_file = @twine_file
|
||||
formatter.options = { consume_all: true }
|
||||
formatter.set_translation_for_key 'new-key', 'en', 'new-key-english'
|
||||
|
||||
assert_equal 'Uncategorized', @strings.sections[0].name
|
||||
assert_equal 'new-key', @strings.sections[0].rows[0].key
|
||||
assert_equal 'Uncategorized', @twine_file.sections[0].name
|
||||
assert_equal 'new-key', @twine_file.sections[0].definitions[0].key
|
||||
end
|
||||
end
|
||||
|
||||
class ValueReference < TwineTestCase
|
||||
class ValueReference < TwineTest
|
||||
def setup
|
||||
super
|
||||
|
||||
@strings = build_twine_file 'en', 'fr' do
|
||||
@twine_file = build_twine_file 'en', 'fr' do
|
||||
add_section 'Section' do
|
||||
add_row refkey: 'ref-value'
|
||||
add_row key: :refkey
|
||||
add_definition refkey: 'ref-value'
|
||||
add_definition key: :refkey
|
||||
end
|
||||
end
|
||||
|
||||
@formatter = Twine::Formatters::Abstract.new
|
||||
@formatter.strings = @strings
|
||||
@formatter.twine_file = @twine_file
|
||||
end
|
||||
|
||||
def test_set_translation_does_not_add_unchanged_translation
|
||||
@formatter.set_translation_for_key 'key', 'en', 'ref-value'
|
||||
|
||||
assert_nil @strings.strings_map['key'].translations['en']
|
||||
assert_nil @twine_file.definitions_by_key['key'].translations['en']
|
||||
end
|
||||
|
||||
def test_set_translation_adds_changed_translation
|
||||
@formatter.set_translation_for_key 'key', 'en', 'changed value'
|
||||
|
||||
assert_equal 'changed value', @strings.strings_map['key'].translations['en']
|
||||
assert_equal 'changed value', @twine_file.definitions_by_key['key'].translations['en']
|
||||
end
|
||||
end
|
||||
|
||||
class SetComment < TwineTestCase
|
||||
class SetComment < TwineTest
|
||||
def setup
|
||||
super
|
||||
|
||||
@strings = build_twine_file 'en' do
|
||||
@twine_file = build_twine_file 'en' do
|
||||
add_section 'Section' do
|
||||
add_row key: 'value'
|
||||
add_definition key: 'value'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_set_comment_for_key_does_not_update_comment
|
||||
formatter = Twine::Formatters::Abstract.new
|
||||
formatter.strings = @strings
|
||||
formatter.twine_file = @twine_file
|
||||
formatter.set_comment_for_key('key', 'comment')
|
||||
|
||||
assert_nil formatter.strings.strings_map['key'].comment
|
||||
assert_nil formatter.twine_file.definitions_by_key['key'].comment
|
||||
end
|
||||
|
||||
def test_set_comment_for_key_updates_comment_with_update_comments
|
||||
formatter = Twine::Formatters::Abstract.new
|
||||
formatter.strings = @strings
|
||||
formatter.twine_file = @twine_file
|
||||
formatter.options = { consume_comments: true }
|
||||
formatter.set_comment_for_key('key', 'comment')
|
||||
|
||||
assert_equal 'comment', formatter.strings.strings_map['key'].comment
|
||||
assert_equal 'comment', formatter.twine_file.definitions_by_key['key'].comment
|
||||
end
|
||||
end
|
||||
|
||||
class CommentReference < TwineTestCase
|
||||
class CommentReference < TwineTest
|
||||
def setup
|
||||
super
|
||||
|
||||
@strings = build_twine_file 'en' do
|
||||
@twine_file = build_twine_file 'en' do
|
||||
add_section 'Section' do
|
||||
add_row refkey: 'ref-value', comment: 'reference comment'
|
||||
add_row key: 'value', ref: :refkey
|
||||
add_definition refkey: 'ref-value', comment: 'reference comment'
|
||||
add_definition key: 'value', ref: :refkey
|
||||
end
|
||||
end
|
||||
|
||||
@formatter = Twine::Formatters::Abstract.new
|
||||
@formatter.strings = @strings
|
||||
@formatter.twine_file = @twine_file
|
||||
@formatter.options = { consume_comments: true }
|
||||
end
|
||||
|
||||
def test_set_comment_does_not_add_unchanged_comment
|
||||
@formatter.set_comment_for_key 'key', 'reference comment'
|
||||
|
||||
assert_nil @strings.strings_map['key'].raw_comment
|
||||
assert_nil @twine_file.definitions_by_key['key'].raw_comment
|
||||
end
|
||||
|
||||
def test_set_comment_adds_changed_comment
|
||||
@formatter.set_comment_for_key 'key', 'changed comment'
|
||||
|
||||
assert_equal 'changed comment', @strings.strings_map['key'].raw_comment
|
||||
assert_equal 'changed comment', @twine_file.definitions_by_key['key'].raw_comment
|
||||
end
|
||||
end
|
||||
|
||||
|
|
677
test/test_cli.rb
677
test/test_cli.rb
|
@ -1,10 +1,10 @@
|
|||
require 'twine_test_case'
|
||||
require 'twine_test'
|
||||
|
||||
class CLITestCase < TwineTestCase
|
||||
class CLITest < TwineTest
|
||||
def setup
|
||||
super
|
||||
super()
|
||||
|
||||
@strings_file_path = File.join @output_dir, SecureRandom.uuid
|
||||
@twine_file_path = File.join @output_dir, SecureRandom.uuid
|
||||
@input_path = File.join @output_dir, SecureRandom.uuid
|
||||
@input_dir = @output_dir
|
||||
end
|
||||
|
@ -13,284 +13,455 @@ class CLITestCase < TwineTestCase
|
|||
@options = Twine::CLI::parse command.split
|
||||
end
|
||||
|
||||
class TestValidateStringsFile < CLITestCase
|
||||
def test_command
|
||||
parse "validate-strings-file #{@strings_file_path}"
|
||||
|
||||
assert_equal 'validate-strings-file', @options[:command]
|
||||
assert_equal @strings_file_path, @options[:strings_file]
|
||||
end
|
||||
|
||||
def test_pedantic
|
||||
parse "validate-strings-file #{@strings_file_path} --pedantic"
|
||||
assert @options[:pedantic]
|
||||
end
|
||||
|
||||
def test_missing_parameter
|
||||
assert_raises Twine::Error do
|
||||
parse 'validate-strings-file'
|
||||
end
|
||||
end
|
||||
|
||||
def test_extra_parameter
|
||||
assert_raises Twine::Error do
|
||||
parse 'validate-strings-file strings extra'
|
||||
end
|
||||
end
|
||||
def parse_with(parameters)
|
||||
raise "you need to implement `parse_with` in your test class"
|
||||
end
|
||||
|
||||
class TestGenerateStringFile < CLITestCase
|
||||
def test_command
|
||||
parse "generate-string-file #{@strings_file_path} #{@output_path}"
|
||||
|
||||
assert_equal 'generate-string-file', @options[:command]
|
||||
assert_equal @strings_file_path, @options[:strings_file]
|
||||
assert_equal @output_path, @options[:output_path]
|
||||
end
|
||||
|
||||
def test_missing_parameter
|
||||
assert_raises Twine::Error do
|
||||
parse 'generate-string-file strings'
|
||||
end
|
||||
end
|
||||
|
||||
def test_validate
|
||||
parse "generate-string-file #{@strings_file_path} #{@output_path} --validate"
|
||||
assert @options[:validate]
|
||||
end
|
||||
|
||||
def test_extra_parameter
|
||||
assert_raises Twine::Error do
|
||||
parse 'generate-string-file strings output extra'
|
||||
end
|
||||
end
|
||||
|
||||
def test_only_allows_one_language
|
||||
assert_raises Twine::Error do
|
||||
parse "generate-string-file strings output --lang en,fr"
|
||||
end
|
||||
end
|
||||
def assert_help
|
||||
parse_with '--help'
|
||||
assert_equal @options, false
|
||||
assert_match /Usage: twine.*Examples:/m, Twine::stdout.string
|
||||
end
|
||||
|
||||
class TestGenerateAllStringFiles < CLITestCase
|
||||
def test_command
|
||||
parse "generate-all-string-files #{@strings_file_path} #{@output_dir}"
|
||||
|
||||
assert_equal 'generate-all-string-files', @options[:command]
|
||||
assert_equal @strings_file_path, @options[:strings_file]
|
||||
assert_equal @output_dir, @options[:output_path]
|
||||
end
|
||||
|
||||
def test_missing_parameter
|
||||
assert_raises Twine::Error do
|
||||
parse "generate-all-string-files strings"
|
||||
end
|
||||
end
|
||||
|
||||
def test_validate
|
||||
parse "generate-all-string-files #{@strings_file_path} #{@output_dir} --validate"
|
||||
assert @options[:validate]
|
||||
end
|
||||
|
||||
def test_extra_parameter
|
||||
assert_raises Twine::Error do
|
||||
parse "generate-all-string-files strings output extra"
|
||||
end
|
||||
end
|
||||
def assert_option_consume_all
|
||||
parse_with '--consume-all'
|
||||
assert @options[:consume_all]
|
||||
parse_with '--no-consume-all'
|
||||
refute @options[:consume_all]
|
||||
end
|
||||
|
||||
class TestConsumeStringFile < CLITestCase
|
||||
def test_command
|
||||
parse "consume-string-file #{@strings_file_path} #{@input_path}"
|
||||
|
||||
assert_equal 'consume-string-file', @options[:command]
|
||||
assert_equal @strings_file_path, @options[:strings_file]
|
||||
assert_equal @input_path, @options[:input_path]
|
||||
end
|
||||
|
||||
def test_missing_parameter
|
||||
assert_raises Twine::Error do
|
||||
parse "consume-string-file strings"
|
||||
end
|
||||
end
|
||||
|
||||
def test_extra_parameter
|
||||
assert_raises Twine::Error do
|
||||
parse "consume-string-file strings output extra"
|
||||
end
|
||||
end
|
||||
|
||||
def test_only_allows_one_language
|
||||
assert_raises Twine::Error do
|
||||
parse "consume-string-file strings output --lang en,fr"
|
||||
end
|
||||
end
|
||||
def assert_option_consume_comments
|
||||
parse_with '--consume-comments'
|
||||
assert @options[:consume_comments]
|
||||
parse_with '--no-consume-comments'
|
||||
refute @options[:consume_comments]
|
||||
end
|
||||
|
||||
class TestConsumeAllStringFiles < CLITestCase
|
||||
def test_command
|
||||
parse "consume-all-string-files #{@strings_file_path} #{@input_dir}"
|
||||
|
||||
assert_equal 'consume-all-string-files', @options[:command]
|
||||
assert_equal @strings_file_path, @options[:strings_file]
|
||||
assert_equal @input_dir, @options[:input_path]
|
||||
end
|
||||
|
||||
def test_missing_parameter
|
||||
assert_raises Twine::Error do
|
||||
parse "consume-all-string-files strings"
|
||||
end
|
||||
end
|
||||
|
||||
def test_extra_parameter
|
||||
assert_raises Twine::Error do
|
||||
parse "consume-all-string-files strings output extra"
|
||||
end
|
||||
end
|
||||
def assert_option_developer_language
|
||||
random_language = KNOWN_LANGUAGES.sample
|
||||
parse_with "--developer-language #{random_language}"
|
||||
assert_equal random_language, @options[:developer_language]
|
||||
end
|
||||
|
||||
class TestGenerateLocDrop < CLITestCase
|
||||
def test_command
|
||||
parse "generate-loc-drop #{@strings_file_path} #{@output_path} --format apple"
|
||||
|
||||
assert_equal 'generate-loc-drop', @options[:command]
|
||||
assert_equal @strings_file_path, @options[:strings_file]
|
||||
assert_equal @output_path, @options[:output_path]
|
||||
end
|
||||
|
||||
def test_missing_parameter
|
||||
assert_raises Twine::Error do
|
||||
parse "generate-loc-drop strings --format apple"
|
||||
end
|
||||
end
|
||||
|
||||
def test_validate
|
||||
parse "generate-loc-drop #{@strings_file_path} #{@output_path} --format apple --validate"
|
||||
assert @options[:validate]
|
||||
end
|
||||
|
||||
def test_extra_parameter
|
||||
assert_raises Twine::Error do
|
||||
parse "generate-loc-drop strings output extra --format apple"
|
||||
end
|
||||
end
|
||||
|
||||
def test_format_needed
|
||||
assert_raises Twine::Error do
|
||||
parse "generate-loc-drop strings output"
|
||||
end
|
||||
end
|
||||
def assert_option_encoding
|
||||
parse_with '--encoding UTF16'
|
||||
assert_equal 'UTF16', @options[:encoding]
|
||||
end
|
||||
|
||||
class TestConsumeLocDrop < CLITestCase
|
||||
def test_command
|
||||
parse "consume-loc-drop #{@strings_file_path} #{@input_path}"
|
||||
|
||||
assert_equal 'consume-loc-drop', @options[:command]
|
||||
assert_equal @strings_file_path, @options[:strings_file]
|
||||
assert_equal @input_path, @options[:input_path]
|
||||
end
|
||||
|
||||
def test_missing_parameter
|
||||
assert_raises Twine::Error do
|
||||
parse "consume-loc-drop strings"
|
||||
end
|
||||
end
|
||||
|
||||
def test_extra_parameter
|
||||
assert_raises Twine::Error do
|
||||
parse "consume-loc-drop strings input extra"
|
||||
end
|
||||
end
|
||||
def assert_option_escape_all_tags
|
||||
parse_with "--escape-all-tags"
|
||||
assert @options[:escape_all_tags]
|
||||
parse_with "--no-escape-all-tags"
|
||||
refute @options[:escape_all_tags]
|
||||
end
|
||||
|
||||
class TestParameters < CLITestCase
|
||||
def parse_with(parameter)
|
||||
parse 'validate-strings-file input.txt ' + parameter
|
||||
end
|
||||
def assert_option_format
|
||||
random_format = Twine::Formatters.formatters.sample.format_name.downcase
|
||||
parse_with "--format #{random_format}"
|
||||
assert_equal random_format, @options[:format]
|
||||
end
|
||||
|
||||
def test_default_options
|
||||
parse_with ''
|
||||
expected = {command: 'validate-strings-file', strings_file: 'input.txt', include: :all}
|
||||
assert_equal expected, @options
|
||||
end
|
||||
def assert_option_include
|
||||
random_set = [:all, :translated, :untranslated].sample
|
||||
parse_with "--include #{random_set}"
|
||||
assert_equal random_set, @options[:include]
|
||||
end
|
||||
|
||||
def test_create_folders
|
||||
parse_with '--create-folders'
|
||||
assert @options[:create_folders]
|
||||
end
|
||||
def assert_option_single_language
|
||||
random_language = KNOWN_LANGUAGES.sample
|
||||
parse_with "--lang #{random_language}"
|
||||
assert_equal [random_language], @options[:languages]
|
||||
end
|
||||
|
||||
def test_consume_all
|
||||
parse_with '--consume-all'
|
||||
assert @options[:consume_all]
|
||||
end
|
||||
def assert_option_multiple_languages
|
||||
random_languages = KNOWN_LANGUAGES.shuffle[0, 3]
|
||||
parse_with "--lang #{random_languages.join(',')}"
|
||||
assert_equal random_languages.sort, @options[:languages].sort
|
||||
end
|
||||
|
||||
def test_consume_comments
|
||||
parse_with '--consume-comments'
|
||||
assert @options[:consume_comments]
|
||||
end
|
||||
def assert_option_languages
|
||||
assert_option_single_language
|
||||
assert_option_multiple_languages
|
||||
end
|
||||
|
||||
def test_untagged
|
||||
parse_with '--untagged'
|
||||
assert @options[:untagged]
|
||||
end
|
||||
def assert_option_output_path
|
||||
parse_with "--output-file #{@output_path}"
|
||||
assert_equal @output_path, @options[:output_path]
|
||||
end
|
||||
|
||||
def test_developer_language
|
||||
random_language = KNOWN_LANGUAGES.sample
|
||||
parse_with "--developer-lang #{random_language}"
|
||||
assert_equal random_language, @options[:developer_language]
|
||||
end
|
||||
def assert_option_quiet
|
||||
parse_with '--quiet'
|
||||
assert @options[:quiet]
|
||||
parse_with '--no-quiet'
|
||||
refute @options[:quiet]
|
||||
end
|
||||
|
||||
def test_single_language
|
||||
random_language = KNOWN_LANGUAGES.sample
|
||||
parse_with "--lang #{random_language}"
|
||||
assert_equal [random_language], @options[:languages]
|
||||
end
|
||||
def assert_option_tags
|
||||
# single tag
|
||||
random_tag = "tag#{rand(100)}"
|
||||
parse_with "--tags #{random_tag}"
|
||||
assert_equal [[random_tag]], @options[:tags]
|
||||
|
||||
def test_multiple_languages
|
||||
random_languages = KNOWN_LANGUAGES.shuffle[0, 3]
|
||||
parse_with "--lang #{random_languages.join(',')}"
|
||||
assert_equal random_languages.sort, @options[:languages].sort
|
||||
end
|
||||
# multiple OR tags
|
||||
random_tags = ["tag#{rand(100)}", "tag#{rand(100)}", "tag#{rand(100)}"]
|
||||
parse_with "--tags #{random_tags.join(',')}"
|
||||
sorted_tags = @options[:tags].map { |tags| tags.sort }
|
||||
assert_equal [random_tags.sort], sorted_tags
|
||||
|
||||
def test_single_tag
|
||||
random_tag = "tag#{rand(100)}"
|
||||
parse_with "--tags #{random_tag}"
|
||||
assert_equal [random_tag], @options[:tags]
|
||||
end
|
||||
# multiple AND tags
|
||||
random_tag_1 = "tag#{rand(100)}"
|
||||
random_tag_2 = "tag#{rand(100)}"
|
||||
parse_with "--tags #{random_tag_1} --tags #{random_tag_2}"
|
||||
assert_equal [[random_tag_1], [random_tag_2]], @options[:tags]
|
||||
|
||||
# NOT tag
|
||||
random_tag = "~tag#{rand(100)}"
|
||||
parse_with "--tags #{random_tag}"
|
||||
assert_equal [[random_tag]], @options[:tags]
|
||||
end
|
||||
|
||||
def test_multiple_tags
|
||||
random_tags = ([nil] * 3).map { "tag#{rand(100)}" }
|
||||
parse_with "--tags #{random_tags.join(',')}"
|
||||
assert_equal random_tags.sort, @options[:tags].sort
|
||||
end
|
||||
def assert_option_untagged
|
||||
parse_with '--untagged'
|
||||
assert @options[:untagged]
|
||||
parse_with '--no-untagged'
|
||||
refute @options[:untagged]
|
||||
end
|
||||
|
||||
def test_format
|
||||
random_format = Twine::Formatters.formatters.sample.format_name.downcase
|
||||
parse_with "--format #{random_format}"
|
||||
assert_equal random_format, @options[:format]
|
||||
end
|
||||
def assert_option_validate
|
||||
parse_with "--validate"
|
||||
assert @options[:validate]
|
||||
parse_with "--no-validate"
|
||||
refute @options[:validate]
|
||||
end
|
||||
end
|
||||
|
||||
def test_include
|
||||
random_set = [:all, :translated, :untranslated].sample
|
||||
parse_with "--include #{random_set}"
|
||||
assert_equal random_set, @options[:include]
|
||||
end
|
||||
class TestCLI < CLITest
|
||||
def test_version
|
||||
parse "--version"
|
||||
|
||||
def test_output_path
|
||||
parse_with "--output-file #{@output_path}"
|
||||
assert_equal @output_path, @options[:output_path]
|
||||
end
|
||||
assert_equal @options, false
|
||||
assert_equal "Twine version #{Twine::VERSION}\n", Twine::stdout.string
|
||||
end
|
||||
|
||||
def test_file_name
|
||||
random_filename = "#{rand(10000)}"
|
||||
parse_with "--file-name #{random_filename}"
|
||||
assert_equal random_filename, @options[:file_name]
|
||||
end
|
||||
def test_help
|
||||
parse ""
|
||||
assert_match 'Usage: twine', Twine::stdout.string
|
||||
end
|
||||
|
||||
def test_encoding
|
||||
parse_with '--encoding UTF16'
|
||||
assert_equal 'UTF16', @options[:output_encoding]
|
||||
def test_invalid_command
|
||||
assert_raises Twine::Error do
|
||||
parse "not a command"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class TestGenerateLocalizationFileCLI < CLITest
|
||||
def parse_with(parameters)
|
||||
parse "generate-localization-file #{@twine_file_path} #{@output_path} " + parameters
|
||||
end
|
||||
|
||||
def test_command
|
||||
parse_with ""
|
||||
|
||||
assert_equal 'generate-localization-file', @options[:command]
|
||||
assert_equal @twine_file_path, @options[:twine_file]
|
||||
assert_equal @output_path, @options[:output_path]
|
||||
end
|
||||
|
||||
def test_missing_argument
|
||||
assert_raises Twine::Error do
|
||||
parse "generate-localization-file #{@twine_file_path}"
|
||||
end
|
||||
end
|
||||
|
||||
def test_extra_argument
|
||||
assert_raises Twine::Error do
|
||||
parse_with "extra"
|
||||
end
|
||||
end
|
||||
|
||||
def test_options
|
||||
assert_help
|
||||
assert_option_developer_language
|
||||
assert_option_encoding
|
||||
assert_option_escape_all_tags
|
||||
assert_option_format
|
||||
assert_option_include
|
||||
assert_option_single_language
|
||||
assert_raises(Twine::Error) { assert_option_multiple_languages }
|
||||
assert_option_quiet
|
||||
assert_option_tags
|
||||
assert_option_untagged
|
||||
assert_option_validate
|
||||
end
|
||||
end
|
||||
|
||||
class TestGenerateAllLocalizationFilesCLI < CLITest
|
||||
def parse_with(parameters)
|
||||
parse "generate-all-localization-files #{@twine_file_path} #{@output_dir} " + parameters
|
||||
end
|
||||
|
||||
def test_command
|
||||
parse_with ""
|
||||
|
||||
assert_equal 'generate-all-localization-files', @options[:command]
|
||||
assert_equal @twine_file_path, @options[:twine_file]
|
||||
assert_equal @output_dir, @options[:output_path]
|
||||
end
|
||||
|
||||
def test_missing_argument
|
||||
assert_raises Twine::Error do
|
||||
parse "generate-all-localization-files twine_file"
|
||||
end
|
||||
end
|
||||
|
||||
def test_extra_arguemnt
|
||||
assert_raises Twine::Error do
|
||||
parse_with "extra"
|
||||
end
|
||||
end
|
||||
|
||||
def test_options
|
||||
assert_help
|
||||
assert_option_developer_language
|
||||
assert_option_encoding
|
||||
assert_option_escape_all_tags
|
||||
assert_option_format
|
||||
assert_option_include
|
||||
assert_option_quiet
|
||||
assert_option_tags
|
||||
assert_option_untagged
|
||||
assert_option_validate
|
||||
end
|
||||
|
||||
def test_option_create_folders
|
||||
parse_with '--create-folders'
|
||||
assert @options[:create_folders]
|
||||
parse_with '--no-create-folders'
|
||||
refute @options[:create_folders]
|
||||
end
|
||||
|
||||
def test_option_file_name
|
||||
random_filename = "#{rand(10000)}"
|
||||
parse_with "--file-name #{random_filename}"
|
||||
assert_equal random_filename, @options[:file_name]
|
||||
end
|
||||
end
|
||||
|
||||
class TestGenerateLocalizationArchiveCLI < CLITest
|
||||
def parse_with(parameters)
|
||||
parse "generate-localization-archive #{@twine_file_path} #{@output_path} --format apple " + parameters
|
||||
end
|
||||
|
||||
def test_command
|
||||
parse_with ""
|
||||
|
||||
assert_equal 'generate-localization-archive', @options[:command]
|
||||
assert_equal @twine_file_path, @options[:twine_file]
|
||||
assert_equal @output_path, @options[:output_path]
|
||||
end
|
||||
|
||||
def test_missing_argument
|
||||
assert_raises Twine::Error do
|
||||
parse "generate-localization-archive twine_file --format apple"
|
||||
end
|
||||
end
|
||||
|
||||
def test_extra_argument
|
||||
assert_raises Twine::Error do
|
||||
parse_with "extra"
|
||||
end
|
||||
end
|
||||
|
||||
def test_options
|
||||
assert_help
|
||||
assert_option_developer_language
|
||||
assert_option_encoding
|
||||
assert_option_escape_all_tags
|
||||
assert_option_include
|
||||
assert_option_quiet
|
||||
assert_option_tags
|
||||
assert_option_untagged
|
||||
assert_option_validate
|
||||
end
|
||||
|
||||
def test_option_format_required
|
||||
assert_raises Twine::Error do
|
||||
parse "generate-localization-archive twine_file output"
|
||||
end
|
||||
end
|
||||
|
||||
def test_supports_deprecated_command
|
||||
parse "generate-loc-drop #{@twine_file_path} #{@output_path} --format apple"
|
||||
assert_equal 'generate-localization-archive', @options[:command]
|
||||
end
|
||||
|
||||
def test_deprecated_command_prints_warning
|
||||
parse "generate-loc-drop #{@twine_file_path} #{@output_path} --format apple"
|
||||
assert_match "WARNING: Twine commands names have changed.", Twine::stdout.string
|
||||
end
|
||||
end
|
||||
|
||||
class TestConsumeLocalizationFileCLI < CLITest
|
||||
def parse_with(parameters)
|
||||
parse "consume-localization-file #{@twine_file_path} #{@input_path} " + parameters
|
||||
end
|
||||
|
||||
def test_command
|
||||
parse_with ""
|
||||
|
||||
assert_equal 'consume-localization-file', @options[:command]
|
||||
assert_equal @twine_file_path, @options[:twine_file]
|
||||
assert_equal @input_path, @options[:input_path]
|
||||
end
|
||||
|
||||
def test_missing_argument
|
||||
assert_raises Twine::Error do
|
||||
parse "consume-localization-file twine_file"
|
||||
end
|
||||
end
|
||||
|
||||
def test_extra_argument
|
||||
assert_raises Twine::Error do
|
||||
parse_with "extra"
|
||||
end
|
||||
end
|
||||
|
||||
def test_options
|
||||
assert_help
|
||||
assert_option_consume_all
|
||||
assert_option_consume_comments
|
||||
assert_option_developer_language
|
||||
assert_option_encoding
|
||||
assert_option_format
|
||||
assert_option_single_language
|
||||
assert_raises(Twine::Error) { assert_option_multiple_languages }
|
||||
assert_option_output_path
|
||||
assert_option_quiet
|
||||
assert_option_tags
|
||||
end
|
||||
end
|
||||
|
||||
class TestConsumeAllLocalizationFilesCLI < CLITest
|
||||
def parse_with(parameters)
|
||||
parse "consume-all-localization-files #{@twine_file_path} #{@input_dir} " + parameters
|
||||
end
|
||||
|
||||
def test_command
|
||||
parse_with ""
|
||||
|
||||
assert_equal 'consume-all-localization-files', @options[:command]
|
||||
assert_equal @twine_file_path, @options[:twine_file]
|
||||
assert_equal @input_dir, @options[:input_path]
|
||||
end
|
||||
|
||||
def test_missing_argument
|
||||
assert_raises Twine::Error do
|
||||
parse "consume-all-localization-files twine_file"
|
||||
end
|
||||
end
|
||||
|
||||
def test_extra_argument
|
||||
assert_raises Twine::Error do
|
||||
parse_with "extra"
|
||||
end
|
||||
end
|
||||
|
||||
def test_options
|
||||
assert_help
|
||||
assert_option_consume_all
|
||||
assert_option_consume_comments
|
||||
assert_option_developer_language
|
||||
assert_option_encoding
|
||||
assert_option_format
|
||||
assert_option_output_path
|
||||
assert_option_quiet
|
||||
assert_option_tags
|
||||
end
|
||||
end
|
||||
|
||||
class TestConsumeLocalizationArchiveCLI < CLITest
|
||||
def parse_with(parameters)
|
||||
parse "consume-localization-archive #{@twine_file_path} #{@input_path} " + parameters
|
||||
end
|
||||
|
||||
def test_command
|
||||
parse_with ""
|
||||
|
||||
assert_equal 'consume-localization-archive', @options[:command]
|
||||
assert_equal @twine_file_path, @options[:twine_file]
|
||||
assert_equal @input_path, @options[:input_path]
|
||||
end
|
||||
|
||||
def test_missing_argument
|
||||
assert_raises Twine::Error do
|
||||
parse "consume-localization-archive twine_file"
|
||||
end
|
||||
end
|
||||
|
||||
def test_extra_argument
|
||||
assert_raises Twine::Error do
|
||||
parse_with "extra"
|
||||
end
|
||||
end
|
||||
|
||||
def test_options
|
||||
assert_help
|
||||
assert_option_consume_all
|
||||
assert_option_consume_comments
|
||||
assert_option_developer_language
|
||||
assert_option_encoding
|
||||
assert_option_format
|
||||
assert_option_output_path
|
||||
assert_option_quiet
|
||||
assert_option_tags
|
||||
end
|
||||
|
||||
def test_supports_deprecated_command
|
||||
parse "consume-loc-drop #{@twine_file_path} #{@input_path}"
|
||||
assert_equal 'consume-localization-archive', @options[:command]
|
||||
end
|
||||
|
||||
def test_deprecated_command_prints_warning
|
||||
parse "consume-loc-drop #{@twine_file_path} #{@input_path}"
|
||||
assert_match "WARNING: Twine commands names have changed.", Twine::stdout.string
|
||||
end
|
||||
end
|
||||
|
||||
class TestValidateTwineFileCLI < CLITest
|
||||
def parse_with(parameters)
|
||||
parse "validate-twine-file #{@twine_file_path} " + parameters
|
||||
end
|
||||
|
||||
def test_command
|
||||
parse_with ""
|
||||
|
||||
assert_equal 'validate-twine-file', @options[:command]
|
||||
assert_equal @twine_file_path, @options[:twine_file]
|
||||
end
|
||||
|
||||
def test_missing_argument
|
||||
assert_raises Twine::Error do
|
||||
parse 'validate-twine-file'
|
||||
end
|
||||
end
|
||||
|
||||
def test_extra_argument
|
||||
assert_raises Twine::Error do
|
||||
parse_with 'extra'
|
||||
end
|
||||
end
|
||||
|
||||
def test_options
|
||||
assert_help
|
||||
assert_option_developer_language
|
||||
assert_option_quiet
|
||||
end
|
||||
|
||||
def test_option_pedantic
|
||||
parse "validate-twine-file #{@twine_file_path} --pedantic"
|
||||
assert @options[:pedantic]
|
||||
parse "validate-twine-file #{@twine_file_path} --no-pedantic"
|
||||
refute @options[:pedantic]
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
require 'command_test_case'
|
||||
|
||||
class TestConsumeLocDrop < CommandTestCase
|
||||
def setup
|
||||
super
|
||||
|
||||
options = {}
|
||||
options[:input_path] = fixture_path 'consume_loc_drop.zip'
|
||||
options[:output_path] = @output_path
|
||||
options[:format] = 'apple'
|
||||
|
||||
@twine_file = build_twine_file 'en', 'es' do
|
||||
add_section 'Section' do
|
||||
add_row key1: 'value1'
|
||||
end
|
||||
end
|
||||
|
||||
@runner = Twine::Runner.new(options, @twine_file)
|
||||
end
|
||||
|
||||
def test_consumes_zip_file
|
||||
@runner.consume_loc_drop
|
||||
|
||||
assert @twine_file.strings_map['key1'].translations['en'], 'value1-english'
|
||||
assert @twine_file.strings_map['key1'].translations['es'], 'value1-spanish'
|
||||
end
|
||||
end
|
33
test/test_consume_localization_archive.rb
Normal file
33
test/test_consume_localization_archive.rb
Normal file
|
@ -0,0 +1,33 @@
|
|||
require 'command_test'
|
||||
|
||||
class TestConsumeLocalizationArchive < CommandTest
|
||||
def setup
|
||||
super
|
||||
|
||||
@twine_file = build_twine_file 'en', 'es' do
|
||||
add_section 'Section' do
|
||||
add_definition key1: 'value1'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def new_runner(options = {})
|
||||
options[:input_path] = fixture_path 'consume_localization_archive.zip'
|
||||
options[:output_path] = @output_path
|
||||
|
||||
Twine::Runner.new(options, @twine_file)
|
||||
end
|
||||
|
||||
def test_consumes_zip_file
|
||||
new_runner(format: 'android').consume_localization_archive
|
||||
|
||||
assert @twine_file.definitions_by_key['key1'].translations['en'], 'value1-english'
|
||||
assert @twine_file.definitions_by_key['key1'].translations['es'], 'value1-spanish'
|
||||
end
|
||||
|
||||
def test_raises_error_if_format_ambiguous
|
||||
assert_raises Twine::Error do
|
||||
new_runner.consume_localization_archive
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,6 +1,6 @@
|
|||
require 'command_test_case'
|
||||
require 'command_test'
|
||||
|
||||
class TestConsumeStringFile < CommandTestCase
|
||||
class TestConsumeLocalizationFile < CommandTest
|
||||
def new_runner(language, file)
|
||||
options = {}
|
||||
options[:output_path] = File.join(@output_dir, file) if file
|
||||
|
@ -8,10 +8,10 @@ class TestConsumeStringFile < CommandTestCase
|
|||
FileUtils.touch options[:input_path]
|
||||
options[:languages] = language if language
|
||||
|
||||
@strings = Twine::StringsFile.new
|
||||
@strings.language_codes.concat KNOWN_LANGUAGES
|
||||
@twine_file = Twine::TwineFile.new
|
||||
@twine_file.language_codes.concat KNOWN_LANGUAGES
|
||||
|
||||
Twine::Runner.new(options, @strings)
|
||||
Twine::Runner.new(options, @twine_file)
|
||||
end
|
||||
|
||||
def prepare_mock_read_formatter(formatter_class)
|
||||
|
@ -22,25 +22,25 @@ class TestConsumeStringFile < CommandTestCase
|
|||
def test_deducts_android_format_from_output_path
|
||||
prepare_mock_read_formatter Twine::Formatters::Android
|
||||
|
||||
new_runner('fr', 'fr.xml').consume_string_file
|
||||
new_runner('fr', 'fr.xml').consume_localization_file
|
||||
end
|
||||
|
||||
def test_deducts_apple_format_from_output_path
|
||||
prepare_mock_read_formatter Twine::Formatters::Apple
|
||||
|
||||
new_runner('fr', 'fr.strings').consume_string_file
|
||||
new_runner('fr', 'fr.strings').consume_localization_file
|
||||
end
|
||||
|
||||
def test_deducts_jquery_format_from_output_path
|
||||
prepare_mock_read_formatter Twine::Formatters::JQuery
|
||||
|
||||
new_runner('fr', 'fr.json').consume_string_file
|
||||
new_runner('fr', 'fr.json').consume_localization_file
|
||||
end
|
||||
|
||||
def test_deducts_gettext_format_from_output_path
|
||||
prepare_mock_read_formatter Twine::Formatters::Gettext
|
||||
|
||||
new_runner('fr', 'fr.po').consume_string_file
|
||||
new_runner('fr', 'fr.po').consume_localization_file
|
||||
end
|
||||
|
||||
def test_deducts_language_from_input_path
|
||||
|
@ -48,10 +48,10 @@ class TestConsumeStringFile < CommandTestCase
|
|||
formatter = prepare_mock_formatter Twine::Formatters::Android
|
||||
formatter.expects(:read).with(anything, random_language)
|
||||
|
||||
new_runner(nil, "#{random_language}.xml").consume_string_file
|
||||
new_runner(nil, "#{random_language}.xml").consume_localization_file
|
||||
end
|
||||
|
||||
class TestEncodings < CommandTestCase
|
||||
class TestEncodings < CommandTest
|
||||
class DummyFormatter < Twine::Formatters::Abstract
|
||||
attr_reader :content
|
||||
|
||||
|
@ -75,10 +75,10 @@ class TestConsumeStringFile < CommandTestCase
|
|||
options[:encoding] = encoding if encoding
|
||||
options[:languages] = 'en'
|
||||
|
||||
@strings = Twine::StringsFile.new
|
||||
@strings.language_codes.concat KNOWN_LANGUAGES
|
||||
@twine_file = Twine::TwineFile.new
|
||||
@twine_file.language_codes.concat KNOWN_LANGUAGES
|
||||
|
||||
Twine::Runner.new(options, @strings)
|
||||
Twine::Runner.new(options, @twine_file)
|
||||
end
|
||||
|
||||
def setup
|
||||
|
@ -88,31 +88,31 @@ class TestConsumeStringFile < CommandTestCase
|
|||
|
||||
def test_reads_utf8
|
||||
formatter = prepare_mock_formatter DummyFormatter
|
||||
new_runner(fixture_path('enc_utf8.dummy')).consume_string_file
|
||||
new_runner(fixture_path('enc_utf8.dummy')).consume_localization_file
|
||||
assert_equal @expected_content, formatter.content
|
||||
end
|
||||
|
||||
def test_reads_utf16le_bom
|
||||
formatter = prepare_mock_formatter DummyFormatter
|
||||
new_runner(fixture_path('enc_utf16le_bom.dummy')).consume_string_file
|
||||
new_runner(fixture_path('enc_utf16le_bom.dummy')).consume_localization_file
|
||||
assert_equal @expected_content, formatter.content
|
||||
end
|
||||
|
||||
def test_reads_utf16be_bom
|
||||
formatter = prepare_mock_formatter DummyFormatter
|
||||
new_runner(fixture_path('enc_utf16be_bom.dummy')).consume_string_file
|
||||
new_runner(fixture_path('enc_utf16be_bom.dummy')).consume_localization_file
|
||||
assert_equal @expected_content, formatter.content
|
||||
end
|
||||
|
||||
def test_reads_utf16le
|
||||
formatter = prepare_mock_formatter DummyFormatter
|
||||
new_runner(fixture_path('enc_utf16le.dummy'), 'UTF-16LE').consume_string_file
|
||||
new_runner(fixture_path('enc_utf16le.dummy'), 'UTF-16LE').consume_localization_file
|
||||
assert_equal @expected_content, formatter.content
|
||||
end
|
||||
|
||||
def test_reads_utf16be
|
||||
formatter = prepare_mock_formatter DummyFormatter
|
||||
new_runner(fixture_path('enc_utf16be.dummy'), 'UTF-16BE').consume_string_file
|
||||
new_runner(fixture_path('enc_utf16be.dummy'), 'UTF-16BE').consume_localization_file
|
||||
assert_equal @expected_content, formatter.content
|
||||
end
|
||||
end
|
|
@ -1,44 +1,148 @@
|
|||
require 'twine_test_case'
|
||||
require 'twine_test'
|
||||
|
||||
class FormatterTest < TwineTestCase
|
||||
class FormatterTest < TwineTest
|
||||
def setup(formatter_class)
|
||||
super()
|
||||
|
||||
@twine_file = build_twine_file 'en' do
|
||||
add_section 'Section 1' do
|
||||
add_row key1: 'value1-english', comment: 'comment key1'
|
||||
add_row key2: 'value2-english'
|
||||
add_definition key1: 'value1-english', comment: 'comment key1'
|
||||
add_definition key2: 'value2-english'
|
||||
end
|
||||
|
||||
add_section 'Section 2' do
|
||||
add_row key3: 'value3-english'
|
||||
add_row key4: 'value4-english', comment: 'comment key4'
|
||||
add_definition key3: 'value3-english'
|
||||
add_definition key4: 'value4-english', comment: 'comment key4'
|
||||
end
|
||||
end
|
||||
|
||||
@strings = Twine::StringsFile.new
|
||||
@empty_twine_file = Twine::TwineFile.new
|
||||
@formatter = formatter_class.new
|
||||
@formatter.strings = @strings
|
||||
@formatter.twine_file = @empty_twine_file
|
||||
@formatter.options = { consume_all: true, consume_comments: true }
|
||||
end
|
||||
|
||||
def assert_translations_read_correctly
|
||||
1.upto(4) do |i|
|
||||
assert_equal "value#{i}-english", @strings.strings_map["key#{i}"].translations['en']
|
||||
assert_equal "value#{i}-english", @empty_twine_file.definitions_by_key["key#{i}"].translations['en']
|
||||
end
|
||||
end
|
||||
|
||||
def assert_file_contents_read_correctly
|
||||
assert_translations_read_correctly
|
||||
|
||||
assert_equal "comment key1", @strings.strings_map["key1"].comment
|
||||
assert_equal "comment key4", @strings.strings_map["key4"].comment
|
||||
assert_equal "comment key1", @empty_twine_file.definitions_by_key["key1"].comment
|
||||
assert_equal "comment key4", @empty_twine_file.definitions_by_key["key4"].comment
|
||||
end
|
||||
end
|
||||
|
||||
class TestAndroidFormatter < FormatterTest
|
||||
def setup
|
||||
super Twine::Formatters::Android
|
||||
|
||||
@escape_test_values = {
|
||||
'this & that' => 'this & that',
|
||||
'this < that' => 'this < that',
|
||||
"it's complicated" => "it\\'s complicated",
|
||||
'a "good" way' => 'a \"good\" way',
|
||||
|
||||
'<b>bold</b>' => '<b>bold</b>',
|
||||
'<em>bold</em>' => '<em>bold</em>',
|
||||
|
||||
'<i>italic</i>' => '<i>italic</i>',
|
||||
'<cite>italic</cite>' => '<cite>italic</cite>',
|
||||
'<dfn>italic</dfn>' => '<dfn>italic</dfn>',
|
||||
|
||||
'<big>larger</big>' => '<big>larger</big>',
|
||||
'<small>smaller</small>' => '<small>smaller</small>',
|
||||
|
||||
'<font color="#45C1D0">F</font>' => '<font color="#45C1D0">F</font>',
|
||||
|
||||
'<tt>monospaced</tt>' => '<tt>monospaced</tt>',
|
||||
|
||||
'<s>strike</s>' => '<s>strike</s>',
|
||||
'<strike>strike</strike>' => '<strike>strike</strike>',
|
||||
'<del>strike</del>' => '<del>strike</del>',
|
||||
|
||||
'<u>underline</u>' => '<u>underline</u>',
|
||||
|
||||
'<super>superscript</super>'=> '<super>superscript</super>',
|
||||
|
||||
'<sub>subscript</sub>' => '<sub>subscript</sub>',
|
||||
|
||||
'<ul>bullet point</ul>' => '<ul>bullet point</ul>',
|
||||
'<li>bullet point</li>' => '<li>bullet point</li>',
|
||||
|
||||
'<br>line break' => '<br>line break',
|
||||
|
||||
'<div>division</div>' => '<div>division</div>',
|
||||
|
||||
'<span style="color:#45C1D0">inline</span>' => '<span style="color:#45C1D0">inline</span>',
|
||||
|
||||
'<p>para</p>' => '<p>para</p>',
|
||||
'<p dir="ltr">para</p>' => '<p dir="ltr">para</p>',
|
||||
|
||||
'<b>%@</b>' => '<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
|
||||
|
@ -47,29 +151,105 @@ class TestAndroidFormatter < FormatterTest
|
|||
assert_file_contents_read_correctly
|
||||
end
|
||||
|
||||
def test_read_multiline_translation
|
||||
content = <<-EOCONTENT
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="foo">This is
|
||||
a string</string>
|
||||
</resources>
|
||||
EOCONTENT
|
||||
|
||||
io = StringIO.new(content)
|
||||
|
||||
@formatter.read io, 'en'
|
||||
|
||||
assert_equal 'This is\n a string', @empty_twine_file.definitions_by_key["foo"].translations['en']
|
||||
end
|
||||
|
||||
def test_read_multiline_comment
|
||||
content = <<-EOCONTENT
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- multiline
|
||||
comment -->
|
||||
<string name="foo">This is
|
||||
a string</string>
|
||||
</resources>
|
||||
EOCONTENT
|
||||
|
||||
io = StringIO.new(content)
|
||||
|
||||
@formatter.read io, 'en'
|
||||
|
||||
assert_equal 'multiline comment', @empty_twine_file.definitions_by_key["foo"].comment
|
||||
end
|
||||
|
||||
def test_read_html_tags
|
||||
content = <<-EOCONTENT
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="foo">Hello, <b>BOLD</b></string>
|
||||
</resources>
|
||||
EOCONTENT
|
||||
|
||||
io = StringIO.new(content)
|
||||
|
||||
@formatter.read io, 'en'
|
||||
|
||||
assert_equal 'Hello, <b>BOLD</b>', @empty_twine_file.definitions_by_key["foo"].translations['en']
|
||||
end
|
||||
|
||||
def test_double_quotes_are_not_modified
|
||||
content = <<-EOCONTENT
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="foo">Hello, <a href="http://www.foo.com">BOLD</a></string>
|
||||
</resources>
|
||||
EOCONTENT
|
||||
|
||||
io = StringIO.new(content)
|
||||
|
||||
@formatter.read io, 'en'
|
||||
|
||||
assert_equal 'Hello, <a href="http://www.foo.com">BOLD</a>', @empty_twine_file.definitions_by_key["foo"].translations['en']
|
||||
end
|
||||
|
||||
def test_set_translation_converts_leading_spaces
|
||||
@formatter.set_translation_for_key 'key1', 'en', "\u0020value"
|
||||
assert_equal ' value', @strings.strings_map['key1'].translations['en']
|
||||
assert_equal ' value', @empty_twine_file.definitions_by_key['key1'].translations['en']
|
||||
end
|
||||
|
||||
def test_set_translation_coverts_trailing_spaces
|
||||
@formatter.set_translation_for_key 'key1', 'en', "value\u0020\u0020"
|
||||
assert_equal 'value ', @strings.strings_map['key1'].translations['en']
|
||||
assert_equal 'value ', @empty_twine_file.definitions_by_key['key1'].translations['en']
|
||||
end
|
||||
|
||||
def test_set_translation_converts_string_placeholders
|
||||
@formatter.set_translation_for_key 'key1', 'en', "value %s"
|
||||
assert_equal 'value %@', @strings.strings_map['key1'].translations['en']
|
||||
assert_equal 'value %@', @empty_twine_file.definitions_by_key['key1'].translations['en']
|
||||
end
|
||||
|
||||
def test_set_translation_unescapes_at_signs
|
||||
@formatter.set_translation_for_key 'key1', 'en', '\@value'
|
||||
assert_equal '@value', @strings.strings_map['key1'].translations['en']
|
||||
assert_equal '@value', @empty_twine_file.definitions_by_key['key1'].translations['en']
|
||||
end
|
||||
|
||||
def test_set_translation_unescaping
|
||||
@escape_test_values.each do |expected, input|
|
||||
@formatter.set_translation_for_key 'key1', 'en', input
|
||||
assert_equal expected, @empty_twine_file.definitions_by_key['key1'].translations['en']
|
||||
end
|
||||
|
||||
@escape_all_test_values.each do |expected, input|
|
||||
@formatter.set_translation_for_key 'key1', 'en', input
|
||||
assert_equal expected, @empty_twine_file.definitions_by_key['key1'].translations['en']
|
||||
end
|
||||
end
|
||||
|
||||
def test_format_file
|
||||
formatter = Twine::Formatters::Android.new
|
||||
formatter.strings = @twine_file
|
||||
formatter.twine_file = @twine_file
|
||||
assert_equal content('formatter_android.xml'), formatter.format_file('en')
|
||||
end
|
||||
|
||||
|
@ -85,10 +265,19 @@ class TestAndroidFormatter < FormatterTest
|
|||
assert_equal "value\\u0020", @formatter.format_value('value ')
|
||||
end
|
||||
|
||||
def test_format_value_escapes_single_quotes
|
||||
skip 'not working with ruby 2.0'
|
||||
# http://stackoverflow.com/questions/18735608/cgiescapehtml-is-escaping-single-quote
|
||||
assert_equal "not \\'so\\' easy", @formatter.format_value("not 'so' easy")
|
||||
def test_format_value_string_placeholder
|
||||
assert_equal "The file %s could not be found.", @formatter.format_value("The file %@ could not be found.")
|
||||
end
|
||||
|
||||
def test_format_value_escaping
|
||||
@escape_test_values.each do |input, expected|
|
||||
assert_equal expected, @formatter.format_value(input)
|
||||
end
|
||||
|
||||
@formatter.options.merge!({ escape_all_tags: true })
|
||||
@escape_all_test_values.each do |input, expected|
|
||||
assert_equal expected, @formatter.format_value(input)
|
||||
end
|
||||
end
|
||||
|
||||
def test_format_value_escapes_non_resource_identifier_at_signs
|
||||
|
@ -100,8 +289,24 @@ class TestAndroidFormatter < FormatterTest
|
|||
assert_equal identifier, @formatter.format_value(identifier)
|
||||
end
|
||||
|
||||
def test_deducts_language_from_filename
|
||||
language = KNOWN_LANGUAGES.sample
|
||||
assert_equal language, @formatter.determine_language_given_path("#{language}.xml")
|
||||
end
|
||||
|
||||
def test_recognize_every_twine_language_from_filename
|
||||
twine_file = build_twine_file "not-a-lang-code" do
|
||||
add_section "Section" do
|
||||
add_definition key: "value"
|
||||
end
|
||||
end
|
||||
|
||||
@formatter.twine_file = twine_file
|
||||
assert_equal "not-a-lang-code", @formatter.determine_language_given_path("not-a-lang-code.xml")
|
||||
end
|
||||
|
||||
def test_deducts_language_from_resource_folder
|
||||
language = %w(en de fr).sample
|
||||
language = KNOWN_LANGUAGES.sample
|
||||
assert_equal language, @formatter.determine_language_given_path("res/values-#{language}")
|
||||
end
|
||||
|
||||
|
@ -109,10 +314,6 @@ class TestAndroidFormatter < FormatterTest
|
|||
assert_equal 'de-AT', @formatter.determine_language_given_path("res/values-de-rAT")
|
||||
end
|
||||
|
||||
def test_maps_laguage_deducted_from_resource_folder
|
||||
assert_equal 'zh-Hans', @formatter.determine_language_given_path("res/values-zh-rCN")
|
||||
end
|
||||
|
||||
def test_does_not_deduct_language_from_device_capability_resource_folder
|
||||
assert_nil @formatter.determine_language_given_path('res/values-w820dp')
|
||||
end
|
||||
|
@ -121,8 +322,15 @@ class TestAndroidFormatter < FormatterTest
|
|||
assert_equal 'values-en', @formatter.output_path_for_language('en')
|
||||
end
|
||||
|
||||
def test_output_path_language_mappings
|
||||
assert_equal 'values-zh-rCN', @formatter.output_path_for_language('zh-Hans')
|
||||
def test_output_path_with_region
|
||||
assert_equal 'values-en-rGB', @formatter.output_path_for_language('en-GB')
|
||||
end
|
||||
|
||||
def test_output_path_respects_default_lang
|
||||
@formatter.twine_file.language_codes.concat KNOWN_LANGUAGES
|
||||
non_default_language = KNOWN_LANGUAGES[1..-1].sample
|
||||
assert_equal 'values', @formatter.output_path_for_language(KNOWN_LANGUAGES[0])
|
||||
assert_equal "values-#{non_default_language}", @formatter.output_path_for_language(non_default_language)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -137,49 +345,75 @@ class TestAppleFormatter < FormatterTest
|
|||
assert_file_contents_read_correctly
|
||||
end
|
||||
|
||||
def test_deducts_language_from_filename
|
||||
language = KNOWN_LANGUAGES.sample
|
||||
assert_equal language, @formatter.determine_language_given_path("#{language}.strings")
|
||||
end
|
||||
|
||||
def test_recognize_every_twine_language_from_filename
|
||||
twine_file = build_twine_file "not-a-lang-code" do
|
||||
add_section "Section" do
|
||||
add_definition key: "value"
|
||||
end
|
||||
end
|
||||
|
||||
@formatter.twine_file = twine_file
|
||||
assert_equal "not-a-lang-code", @formatter.determine_language_given_path("not-a-lang-code.strings")
|
||||
end
|
||||
|
||||
def test_deducts_language_from_resource_folder
|
||||
language = %w(en de fr).sample
|
||||
assert_equal language, @formatter.determine_language_given_path("#{language}.lproj/Localizable.strings")
|
||||
end
|
||||
|
||||
def test_deducts_base_language_from_resource_folder
|
||||
@formatter.options = { consume_all: true, consume_comments: true, developer_language: 'en' }
|
||||
assert_equal 'en', @formatter.determine_language_given_path('Base.lproj/Localizations.strings')
|
||||
end
|
||||
|
||||
def test_reads_quoted_keys
|
||||
@formatter.read StringIO.new('"key" = "value"'), 'en'
|
||||
assert_equal 'value', @strings.strings_map['key'].translations['en']
|
||||
assert_equal 'value', @empty_twine_file.definitions_by_key['key'].translations['en']
|
||||
end
|
||||
|
||||
def test_reads_unquoted_keys
|
||||
@formatter.read StringIO.new('key = "value"'), 'en'
|
||||
assert_equal 'value', @strings.strings_map['key'].translations['en']
|
||||
assert_equal 'value', @empty_twine_file.definitions_by_key['key'].translations['en']
|
||||
end
|
||||
|
||||
def test_ignores_leading_whitespace_before_quoted_keys
|
||||
@formatter.read StringIO.new("\t \"key\" = \"value\""), 'en'
|
||||
assert_equal 'value', @strings.strings_map['key'].translations['en']
|
||||
assert_equal 'value', @empty_twine_file.definitions_by_key['key'].translations['en']
|
||||
end
|
||||
|
||||
def test_ignores_leading_whitespace_before_unquoted_keys
|
||||
@formatter.read StringIO.new("\t key = \"value\""), 'en'
|
||||
assert_equal 'value', @strings.strings_map['key'].translations['en']
|
||||
assert_equal 'value', @empty_twine_file.definitions_by_key['key'].translations['en']
|
||||
end
|
||||
|
||||
def test_allows_quotes_in_quoted_keys
|
||||
@formatter.read StringIO.new('"ke\"y" = "value"'), 'en'
|
||||
assert_equal 'value', @strings.strings_map['ke"y'].translations['en']
|
||||
assert_equal 'value', @empty_twine_file.definitions_by_key['ke"y'].translations['en']
|
||||
end
|
||||
|
||||
def test_does_not_allow_quotes_in_quoted_keys
|
||||
@formatter.read StringIO.new('ke"y = "value"'), 'en'
|
||||
assert_nil @strings.strings_map['key']
|
||||
assert_nil @empty_twine_file.definitions_by_key['key']
|
||||
end
|
||||
|
||||
def test_allows_equal_signs_in_quoted_keys
|
||||
@formatter.read StringIO.new('"k=ey" = "value"'), 'en'
|
||||
assert_equal 'value', @strings.strings_map['k=ey'].translations['en']
|
||||
assert_equal 'value', @empty_twine_file.definitions_by_key['k=ey'].translations['en']
|
||||
end
|
||||
|
||||
def test_does_not_allow_equal_signs_in_unquoted_keys
|
||||
@formatter.read StringIO.new('k=ey = "value"'), 'en'
|
||||
assert_nil @strings.strings_map['key']
|
||||
assert_nil @empty_twine_file.definitions_by_key['key']
|
||||
end
|
||||
|
||||
def test_format_file
|
||||
formatter = Twine::Formatters::Apple.new
|
||||
formatter.strings = @twine_file
|
||||
formatter.twine_file = @twine_file
|
||||
assert_equal content('formatter_apple.strings'), formatter.format_file('en')
|
||||
end
|
||||
|
||||
|
@ -210,17 +444,45 @@ class TestJQueryFormatter < FormatterTest
|
|||
|
||||
def test_format_file
|
||||
formatter = Twine::Formatters::JQuery.new
|
||||
formatter.strings = @twine_file
|
||||
formatter.twine_file = @twine_file
|
||||
assert_equal content('formatter_jquery.json'), formatter.format_file('en')
|
||||
end
|
||||
|
||||
def test_empty_sections_are_removed
|
||||
@twine_file = build_twine_file 'en' do
|
||||
add_section 'Section 1' do
|
||||
end
|
||||
|
||||
add_section 'Section 2' do
|
||||
add_definition key: 'value'
|
||||
end
|
||||
end
|
||||
formatter = Twine::Formatters::JQuery.new
|
||||
formatter.twine_file = @twine_file
|
||||
refute_includes formatter.format_file('en'), ','
|
||||
end
|
||||
|
||||
def test_format_value_with_newline
|
||||
assert_equal "value\nwith\nline\nbreaks", @formatter.format_value("value\nwith\nline\nbreaks")
|
||||
end
|
||||
|
||||
def test_deducts_language_from_filename
|
||||
language = KNOWN_LANGUAGES.sample
|
||||
assert_equal language, @formatter.determine_language_given_path("#{language}.json")
|
||||
end
|
||||
|
||||
def test_deducts_language_from_extended_filename
|
||||
language = KNOWN_LANGUAGES.sample
|
||||
assert_equal language, @formatter.determine_language_given_path("something-#{language}.json")
|
||||
end
|
||||
|
||||
def test_deducts_language_from_path
|
||||
language = %w(en-GB de fr).sample
|
||||
assert_equal language, @formatter.determine_language_given_path("/output/#{language}/#{@formatter.default_file_name}")
|
||||
end
|
||||
end
|
||||
|
||||
class TestGettextFormatter < FormatterTest
|
||||
|
||||
def setup
|
||||
super Twine::Formatters::Gettext
|
||||
end
|
||||
|
@ -234,15 +496,34 @@ class TestGettextFormatter < FormatterTest
|
|||
def test_read_with_multiple_line_value
|
||||
@formatter.read content_io('gettext_multiline.po'), 'en'
|
||||
|
||||
assert_equal 'multiline\nstring', @strings.strings_map['key1'].translations['en']
|
||||
assert_equal 'multiline\nstring', @empty_twine_file.definitions_by_key['key1'].translations['en']
|
||||
end
|
||||
|
||||
def test_format_file
|
||||
formatter = Twine::Formatters::Gettext.new
|
||||
formatter.strings = @twine_file
|
||||
formatter.twine_file = @twine_file
|
||||
assert_equal content('formatter_gettext.po'), formatter.format_file('en')
|
||||
end
|
||||
|
||||
def test_deducts_language_and_region
|
||||
language = "en-GB"
|
||||
assert_equal language, @formatter.determine_language_given_path("#{language}.po")
|
||||
end
|
||||
|
||||
def test_deducts_language_from_path
|
||||
language = %w(en-GB de fr).sample
|
||||
assert_equal language, @formatter.determine_language_given_path("/output/#{language}/#{@formatter.default_file_name}")
|
||||
end
|
||||
|
||||
def test_quoted_strings
|
||||
formatter = Twine::Formatters::Gettext.new
|
||||
formatter.twine_file = build_twine_file "not-a-lang-code" do
|
||||
add_section "Section" do
|
||||
add_definition key: "foo \"bar\" baz"
|
||||
end
|
||||
end
|
||||
assert_equal content('formatter_gettext_quotes.po'), formatter.format_file('en')
|
||||
end
|
||||
end
|
||||
|
||||
class TestTizenFormatter < FormatterTest
|
||||
|
@ -260,10 +541,9 @@ class TestTizenFormatter < FormatterTest
|
|||
|
||||
def test_format_file
|
||||
formatter = Twine::Formatters::Tizen.new
|
||||
formatter.strings = @twine_file
|
||||
formatter.twine_file = @twine_file
|
||||
assert_equal content('formatter_tizen.xml'), formatter.format_file('en')
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
class TestDjangoFormatter < FormatterTest
|
||||
|
@ -279,9 +559,32 @@ class TestDjangoFormatter < FormatterTest
|
|||
|
||||
def test_format_file
|
||||
formatter = Twine::Formatters::Django.new
|
||||
formatter.strings = @twine_file
|
||||
formatter.twine_file = @twine_file
|
||||
assert_equal content('formatter_django.po'), formatter.format_file('en')
|
||||
end
|
||||
|
||||
def test_deducts_language_and_region
|
||||
language = "en-GB"
|
||||
assert_equal language, @formatter.determine_language_given_path("#{language}.po")
|
||||
end
|
||||
|
||||
def test_deducts_language_from_path
|
||||
language = %w(en-GB de fr).sample
|
||||
assert_equal language, @formatter.determine_language_given_path("/output/#{language}/#{@formatter.default_file_name}")
|
||||
end
|
||||
|
||||
def test_ignores_commented_out_strings
|
||||
content = <<-EOCONTENT
|
||||
#~ msgid "foo"
|
||||
#~ msgstr "This should be ignored"
|
||||
EOCONTENT
|
||||
|
||||
io = StringIO.new(content)
|
||||
|
||||
@formatter.read io, 'en'
|
||||
|
||||
assert_nil @empty_twine_file.definitions_by_key["foo"]
|
||||
end
|
||||
end
|
||||
|
||||
class TestFlashFormatter < FormatterTest
|
||||
|
@ -295,9 +598,27 @@ class TestFlashFormatter < FormatterTest
|
|||
assert_file_contents_read_correctly
|
||||
end
|
||||
|
||||
def test_set_translation_converts_placeholders
|
||||
@formatter.set_translation_for_key 'key1', 'en', "value {#{rand(10)}}"
|
||||
assert_equal 'value %@', @empty_twine_file.definitions_by_key['key1'].translations['en']
|
||||
end
|
||||
|
||||
def test_format_file
|
||||
formatter = Twine::Formatters::Flash.new
|
||||
formatter.strings = @twine_file
|
||||
formatter.twine_file = @twine_file
|
||||
assert_equal content('formatter_flash.properties'), formatter.format_file('en')
|
||||
end
|
||||
|
||||
def test_format_value_converts_placeholders
|
||||
assert_equal "value {0}", @formatter.format_value('value %d')
|
||||
end
|
||||
|
||||
def test_deducts_language_from_resource_folder
|
||||
language = %w(en de fr).sample
|
||||
assert_equal language, @formatter.determine_language_given_path("locale/#{language}/#{@formatter.default_file_name}")
|
||||
end
|
||||
|
||||
def test_deducts_language_and_region_from_resource_folder
|
||||
assert_equal 'de-AT', @formatter.determine_language_given_path("locale/de-AT/#{@formatter.default_file_name}")
|
||||
end
|
||||
end
|
||||
|
|
140
test/test_generate_all_localization_files.rb
Normal file
140
test/test_generate_all_localization_files.rb
Normal file
|
@ -0,0 +1,140 @@
|
|||
require 'command_test'
|
||||
|
||||
class TestGenerateAllLocalizationFiles < CommandTest
|
||||
def new_runner(create_folders, twine_file = nil, options = {})
|
||||
default_options = {}
|
||||
default_options[:output_path] = @output_dir
|
||||
default_options[:format] = 'apple'
|
||||
default_options[:create_folders] = create_folders
|
||||
|
||||
options = default_options.merge options
|
||||
|
||||
unless twine_file
|
||||
twine_file = build_twine_file 'en', 'es' do
|
||||
add_section 'Section' do
|
||||
add_definition key: 'value'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Twine::Runner.new(options, twine_file)
|
||||
end
|
||||
|
||||
class TestFormatterSelection < TestGenerateAllLocalizationFiles
|
||||
def setup
|
||||
super
|
||||
Dir.mkdir File.join @output_dir, 'values-en'
|
||||
end
|
||||
|
||||
def new_runner(options = {})
|
||||
super(true, nil, options)
|
||||
end
|
||||
|
||||
def test_returns_error_for_ambiguous_output_path
|
||||
assert_raises Twine::Error do
|
||||
new_runner(format: nil).generate_all_localization_files
|
||||
end
|
||||
end
|
||||
|
||||
def test_uses_specified_formatter_to_resolve_ambiguity
|
||||
# implicit assert that this call doesn't raise an exception
|
||||
new_runner(format: 'android').generate_all_localization_files
|
||||
end
|
||||
end
|
||||
|
||||
class TestDoNotCreateFolders < TestGenerateAllLocalizationFiles
|
||||
def new_runner(twine_file = nil, options = {})
|
||||
super(false, twine_file, options)
|
||||
end
|
||||
|
||||
def test_fails_if_output_folder_does_not_exist
|
||||
assert_raises Twine::Error do
|
||||
new_runner.generate_all_localization_files
|
||||
end
|
||||
end
|
||||
|
||||
def test_does_not_create_language_folders
|
||||
Dir.mkdir File.join @output_dir, 'en.lproj'
|
||||
new_runner.generate_all_localization_files
|
||||
refute File.exist?(File.join(@output_dir, 'es.lproj')), "language folder should not be created"
|
||||
end
|
||||
|
||||
def test_prints_empty_file_warnings
|
||||
Dir.mkdir File.join @output_dir, 'en.lproj'
|
||||
empty_twine_file = build_twine_file('en') {}
|
||||
new_runner(empty_twine_file).generate_all_localization_files
|
||||
assert_match "Skipping file at path", Twine::stdout.string
|
||||
end
|
||||
|
||||
def test_does_not_print_empty_file_warnings_if_quite
|
||||
Dir.mkdir File.join @output_dir, 'en.lproj'
|
||||
empty_twine_file = build_twine_file('en') {}
|
||||
new_runner(empty_twine_file, quite: true).generate_all_localization_files
|
||||
refute_match "Skipping file at path", Twine::stdout.string
|
||||
end
|
||||
end
|
||||
|
||||
class TestCreateFolders < TestGenerateAllLocalizationFiles
|
||||
def new_runner(twine_file = nil, options = {})
|
||||
super(true, twine_file, options)
|
||||
end
|
||||
|
||||
def test_creates_output_folder
|
||||
FileUtils.remove_entry_secure @output_dir
|
||||
new_runner.generate_all_localization_files
|
||||
assert File.exist? @output_dir
|
||||
end
|
||||
|
||||
def test_creates_language_folders
|
||||
new_runner.generate_all_localization_files
|
||||
assert File.exist?(File.join(@output_dir, 'en.lproj')), "language folder 'en.lproj' should be created"
|
||||
assert File.exist?(File.join(@output_dir, 'es.lproj')), "language folder 'es.lproj' should be created"
|
||||
end
|
||||
|
||||
def test_prints_empty_file_warnings
|
||||
empty_twine_file = build_twine_file('en') {}
|
||||
new_runner(empty_twine_file).generate_all_localization_files
|
||||
|
||||
assert_match "Skipping file at path", Twine::stdout.string
|
||||
end
|
||||
|
||||
def test_does_not_print_empty_file_warnings_if_quite
|
||||
empty_twine_file = build_twine_file('en') {}
|
||||
new_runner(empty_twine_file, quite: true).generate_all_localization_files
|
||||
|
||||
refute_match "Skipping file at path", Twine::stdout.string
|
||||
end
|
||||
end
|
||||
|
||||
class TestValidate < CommandTest
|
||||
def new_runner(validate)
|
||||
Dir.mkdir File.join @output_dir, 'values-en'
|
||||
|
||||
options = {}
|
||||
options[:output_path] = @output_dir
|
||||
options[:format] = 'android'
|
||||
options[:validate] = validate
|
||||
|
||||
twine_file = build_twine_file 'en' do
|
||||
add_section 'Section' do
|
||||
add_definition key: 'value'
|
||||
add_definition key: 'value'
|
||||
end
|
||||
end
|
||||
|
||||
Twine::Runner.new(options, twine_file)
|
||||
end
|
||||
|
||||
def test_does_not_validate_twine_file
|
||||
prepare_mock_formatter Twine::Formatters::Android
|
||||
|
||||
new_runner(false).generate_all_localization_files
|
||||
end
|
||||
|
||||
def test_validates_twine_file_if_validate
|
||||
assert_raises Twine::Error do
|
||||
new_runner(true).generate_all_localization_files
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,102 +0,0 @@
|
|||
require 'command_test_case'
|
||||
|
||||
class TestGenerateAllStringFiles < CommandTestCase
|
||||
def new_runner(create_folders, twine_file = nil)
|
||||
options = {}
|
||||
options[:output_path] = @output_dir
|
||||
options[:format] = 'apple'
|
||||
options[:create_folders] = create_folders
|
||||
|
||||
unless twine_file
|
||||
twine_file = build_twine_file 'en', 'es' do
|
||||
add_section 'Section' do
|
||||
add_row key: 'value'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Twine::Runner.new(options, twine_file)
|
||||
end
|
||||
|
||||
class TestDoNotCreateFolders < TestGenerateAllStringFiles
|
||||
def new_runner(twine_file = nil)
|
||||
super(false, twine_file)
|
||||
end
|
||||
|
||||
def test_fails_if_output_folder_does_not_exist
|
||||
assert_raises Twine::Error do
|
||||
new_runner.generate_all_string_files
|
||||
end
|
||||
end
|
||||
|
||||
def test_does_not_create_language_folders
|
||||
Dir.mkdir File.join @output_dir, 'en.lproj'
|
||||
new_runner.generate_all_string_files
|
||||
refute File.exists?(File.join(@output_dir, 'es.lproj')), "language folder should not be created"
|
||||
end
|
||||
|
||||
def test_prints_empty_file_warnings
|
||||
Dir.mkdir File.join @output_dir, 'en.lproj'
|
||||
empty_twine_file = build_twine_file('en') {}
|
||||
new_runner(empty_twine_file).generate_all_string_files
|
||||
assert_match "Skipping file at path", Twine::stderr.string
|
||||
end
|
||||
end
|
||||
|
||||
class TestCreateFolders < TestGenerateAllStringFiles
|
||||
def new_runner(twine_file = nil)
|
||||
super(true, twine_file)
|
||||
end
|
||||
|
||||
def test_creates_output_folder
|
||||
FileUtils.remove_entry_secure @output_dir
|
||||
new_runner.generate_all_string_files
|
||||
assert File.exists? @output_dir
|
||||
end
|
||||
|
||||
def test_creates_language_folders
|
||||
new_runner.generate_all_string_files
|
||||
assert File.exists?(File.join(@output_dir, 'en.lproj')), "language folder 'en.lproj' should be created"
|
||||
assert File.exists?(File.join(@output_dir, 'es.lproj')), "language folder 'es.lproj' should be created"
|
||||
end
|
||||
|
||||
def test_prints_empty_file_warnings
|
||||
empty_twine_file = build_twine_file('en') {}
|
||||
new_runner(empty_twine_file).generate_all_string_files
|
||||
|
||||
assert_match "Skipping file at path", Twine::stderr.string
|
||||
end
|
||||
end
|
||||
|
||||
class TestValidate < CommandTestCase
|
||||
def new_runner(validate)
|
||||
Dir.mkdir File.join @output_dir, 'values-en'
|
||||
|
||||
options = {}
|
||||
options[:output_path] = @output_dir
|
||||
options[:format] = 'android'
|
||||
options[:validate] = validate
|
||||
|
||||
twine_file = build_twine_file 'en' do
|
||||
add_section 'Section' do
|
||||
add_row key: 'value'
|
||||
add_row key: 'value'
|
||||
end
|
||||
end
|
||||
|
||||
Twine::Runner.new(options, twine_file)
|
||||
end
|
||||
|
||||
def test_does_not_validate_strings_file
|
||||
prepare_mock_formatter Twine::Formatters::Android
|
||||
|
||||
new_runner(false).generate_all_string_files
|
||||
end
|
||||
|
||||
def test_validates_strings_file_if_validate
|
||||
assert_raises Twine::Error do
|
||||
new_runner(true).generate_all_string_files
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,15 +1,14 @@
|
|||
require 'command_test_case'
|
||||
require 'command_test'
|
||||
|
||||
class TestGenerateLocDrop < CommandTestCase
|
||||
def new_runner(twine_file = nil)
|
||||
options = {}
|
||||
class TestGenerateLocalizationArchive < CommandTest
|
||||
def new_runner(twine_file = nil, options = {})
|
||||
options[:output_path] = @output_path
|
||||
options[:format] = 'apple'
|
||||
|
||||
unless twine_file
|
||||
twine_file = build_twine_file 'en', 'fr' do
|
||||
add_section 'Section' do
|
||||
add_row key: 'value'
|
||||
add_definition key: 'value'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -18,13 +17,13 @@ class TestGenerateLocDrop < CommandTestCase
|
|||
end
|
||||
|
||||
def test_generates_zip_file
|
||||
new_runner.generate_loc_drop
|
||||
new_runner.generate_localization_archive
|
||||
|
||||
assert File.exists?(@output_path), "zip file should exist"
|
||||
assert File.exist?(@output_path), "zip file should exist"
|
||||
end
|
||||
|
||||
def test_zip_file_structure
|
||||
new_runner.generate_loc_drop
|
||||
new_runner.generate_localization_archive
|
||||
|
||||
names = []
|
||||
Zip::File.open(@output_path) do |zipfile|
|
||||
|
@ -39,16 +38,22 @@ class TestGenerateLocDrop < CommandTestCase
|
|||
formatter = prepare_mock_formatter Twine::Formatters::Apple
|
||||
formatter.expects(:format_file).twice
|
||||
|
||||
new_runner.generate_loc_drop
|
||||
new_runner.generate_localization_archive
|
||||
end
|
||||
|
||||
def test_prints_empty_file_warnings
|
||||
empty_twine_file = build_twine_file('en') {}
|
||||
new_runner(empty_twine_file).generate_loc_drop
|
||||
assert_match "Skipping file", Twine::stderr.string
|
||||
new_runner(empty_twine_file).generate_localization_archive
|
||||
assert_match "Skipping file", Twine::stdout.string
|
||||
end
|
||||
|
||||
class TestValidate < CommandTestCase
|
||||
def test_does_not_print_empty_file_warnings_if_quite
|
||||
empty_twine_file = build_twine_file('en') {}
|
||||
new_runner(empty_twine_file, quite: true).generate_localization_archive
|
||||
refute_match "Skipping file", Twine::stdout.string
|
||||
end
|
||||
|
||||
class TestValidate < CommandTest
|
||||
def new_runner(validate)
|
||||
options = {}
|
||||
options[:output_path] = @output_path
|
||||
|
@ -57,23 +62,23 @@ class TestGenerateLocDrop < CommandTestCase
|
|||
|
||||
twine_file = build_twine_file 'en' do
|
||||
add_section 'Section' do
|
||||
add_row key: 'value'
|
||||
add_row key: 'value'
|
||||
add_definition key: 'value'
|
||||
add_definition key: 'value'
|
||||
end
|
||||
end
|
||||
|
||||
Twine::Runner.new(options, twine_file)
|
||||
end
|
||||
|
||||
def test_does_not_validate_strings_file
|
||||
def test_does_not_validate_twine_file
|
||||
prepare_mock_formatter Twine::Formatters::Android
|
||||
|
||||
new_runner(false).generate_loc_drop
|
||||
new_runner(false).generate_localization_archive
|
||||
end
|
||||
|
||||
def test_validates_strings_file_if_validate
|
||||
def test_validates_twine_file_if_validate
|
||||
assert_raises Twine::Error do
|
||||
new_runner(true).generate_loc_drop
|
||||
new_runner(true).generate_localization_archive
|
||||
end
|
||||
end
|
||||
end
|
119
test/test_generate_localization_file.rb
Normal file
119
test/test_generate_localization_file.rb
Normal file
|
@ -0,0 +1,119 @@
|
|||
require 'command_test'
|
||||
|
||||
class TestGenerateLocalizationFile < CommandTest
|
||||
def new_runner(language, file, options = {})
|
||||
options[:output_path] = File.join(@output_dir, file) if file
|
||||
options[:languages] = language if language
|
||||
|
||||
twine_file = Twine::TwineFile.new
|
||||
twine_file.language_codes.concat KNOWN_LANGUAGES
|
||||
|
||||
Twine::Runner.new(options, twine_file)
|
||||
end
|
||||
|
||||
def prepare_mock_format_file_formatter(formatter_class)
|
||||
formatter = prepare_mock_formatter(formatter_class)
|
||||
formatter.expects(:format_file).returns(true)
|
||||
end
|
||||
|
||||
def test_deducts_android_format_from_output_path
|
||||
prepare_mock_format_file_formatter Twine::Formatters::Android
|
||||
|
||||
new_runner('fr', 'fr.xml').generate_localization_file
|
||||
end
|
||||
|
||||
def test_deducts_apple_format_from_output_path
|
||||
prepare_mock_format_file_formatter Twine::Formatters::Apple
|
||||
|
||||
new_runner('fr', 'fr.strings').generate_localization_file
|
||||
end
|
||||
|
||||
def test_deducts_jquery_format_from_output_path
|
||||
prepare_mock_format_file_formatter Twine::Formatters::JQuery
|
||||
|
||||
new_runner('fr', 'fr.json').generate_localization_file
|
||||
end
|
||||
|
||||
def test_deducts_gettext_format_from_output_path
|
||||
prepare_mock_format_file_formatter Twine::Formatters::Gettext
|
||||
|
||||
new_runner('fr', 'fr.po').generate_localization_file
|
||||
end
|
||||
|
||||
def test_deducts_django_format_from_output_path
|
||||
prepare_mock_format_file_formatter Twine::Formatters::Django
|
||||
|
||||
new_runner('fr', 'fr.po').generate_localization_file
|
||||
end
|
||||
|
||||
def test_returns_error_for_ambiguous_output_path
|
||||
# both Gettext and Django use .po
|
||||
gettext_formatter = prepare_mock_formatter(Twine::Formatters::Gettext)
|
||||
gettext_formatter.stubs(:format_file).returns(true)
|
||||
django_formatter = prepare_mock_formatter(Twine::Formatters::Django, false)
|
||||
django_formatter.stubs(:format_file).returns(true)
|
||||
|
||||
assert_raises Twine::Error do
|
||||
new_runner('fr', 'fr.po').generate_localization_file
|
||||
end
|
||||
end
|
||||
|
||||
def test_uses_specified_formatter_to_resolve_ambiguity
|
||||
# both Android and Tizen use .xml
|
||||
android_formatter = prepare_mock_formatter(Twine::Formatters::Android)
|
||||
android_formatter.stubs(:format_file).returns(true)
|
||||
tizen_formatter = prepare_mock_formatter(Twine::Formatters::Tizen, false)
|
||||
tizen_formatter.stubs(:format_file).returns(true)
|
||||
|
||||
# implicit assert that this call doesn't raise an exception
|
||||
new_runner('fr', 'fr.xml', format: 'android').generate_localization_file
|
||||
end
|
||||
|
||||
def test_deducts_language_from_output_path
|
||||
random_language = KNOWN_LANGUAGES.sample
|
||||
formatter = prepare_mock_formatter Twine::Formatters::Android
|
||||
formatter.expects(:format_file).with(random_language).returns(true)
|
||||
|
||||
new_runner(nil, "#{random_language}.xml").generate_localization_file
|
||||
end
|
||||
|
||||
def test_returns_error_if_nothing_written
|
||||
formatter = prepare_mock_formatter Twine::Formatters::Android
|
||||
formatter.expects(:format_file).returns(false)
|
||||
|
||||
assert_raises Twine::Error do
|
||||
new_runner('fr', 'fr.xml').generate_localization_file
|
||||
end
|
||||
end
|
||||
|
||||
class TestValidate < CommandTest
|
||||
def new_runner(validate)
|
||||
options = {}
|
||||
options[:output_path] = @output_path
|
||||
options[:languages] = ['en']
|
||||
options[:format] = 'android'
|
||||
options[:validate] = validate
|
||||
|
||||
twine_file = build_twine_file 'en' do
|
||||
add_section 'Section' do
|
||||
add_definition key: 'value'
|
||||
add_definition key: 'value'
|
||||
end
|
||||
end
|
||||
|
||||
Twine::Runner.new(options, twine_file)
|
||||
end
|
||||
|
||||
def test_does_not_validate_twine_file
|
||||
prepare_mock_formatter Twine::Formatters::Android
|
||||
|
||||
new_runner(false).generate_localization_file
|
||||
end
|
||||
|
||||
def test_validates_twine_file_if_validate
|
||||
assert_raises Twine::Error do
|
||||
new_runner(true).generate_localization_file
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,91 +0,0 @@
|
|||
require 'command_test_case'
|
||||
|
||||
class TestGenerateStringFile < CommandTestCase
|
||||
def new_runner(language, file)
|
||||
options = {}
|
||||
options[:output_path] = File.join(@output_dir, file) if file
|
||||
options[:languages] = language if language
|
||||
|
||||
strings = Twine::StringsFile.new
|
||||
strings.language_codes.concat KNOWN_LANGUAGES
|
||||
|
||||
Twine::Runner.new(options, strings)
|
||||
end
|
||||
|
||||
def prepare_mock_format_file_formatter(formatter_class)
|
||||
formatter = prepare_mock_formatter(formatter_class)
|
||||
formatter.expects(:format_file).returns(true)
|
||||
end
|
||||
|
||||
def test_deducts_android_format_from_output_path
|
||||
prepare_mock_format_file_formatter Twine::Formatters::Android
|
||||
|
||||
new_runner('fr', 'fr.xml').generate_string_file
|
||||
end
|
||||
|
||||
def test_deducts_apple_format_from_output_path
|
||||
prepare_mock_format_file_formatter Twine::Formatters::Apple
|
||||
|
||||
new_runner('fr', 'fr.strings').generate_string_file
|
||||
end
|
||||
|
||||
def test_deducts_jquery_format_from_output_path
|
||||
prepare_mock_format_file_formatter Twine::Formatters::JQuery
|
||||
|
||||
new_runner('fr', 'fr.json').generate_string_file
|
||||
end
|
||||
|
||||
def test_deducts_gettext_format_from_output_path
|
||||
prepare_mock_format_file_formatter Twine::Formatters::Gettext
|
||||
|
||||
new_runner('fr', 'fr.po').generate_string_file
|
||||
end
|
||||
|
||||
def test_deducts_language_from_output_path
|
||||
random_language = KNOWN_LANGUAGES.sample
|
||||
formatter = prepare_mock_formatter Twine::Formatters::Android
|
||||
formatter.expects(:format_file).with(random_language).returns(true)
|
||||
|
||||
new_runner(nil, "#{random_language}.xml").generate_string_file
|
||||
end
|
||||
|
||||
def test_returns_error_if_nothing_written
|
||||
formatter = prepare_mock_formatter Twine::Formatters::Android
|
||||
formatter.expects(:format_file).returns(false)
|
||||
|
||||
assert_raises Twine::Error do
|
||||
new_runner('fr', 'fr.xml').generate_string_file
|
||||
end
|
||||
end
|
||||
|
||||
class TestValidate < CommandTestCase
|
||||
def new_runner(validate)
|
||||
options = {}
|
||||
options[:output_path] = @output_path
|
||||
options[:languages] = ['en']
|
||||
options[:format] = 'android'
|
||||
options[:validate] = validate
|
||||
|
||||
twine_file = build_twine_file 'en' do
|
||||
add_section 'Section' do
|
||||
add_row key: 'value'
|
||||
add_row key: 'value'
|
||||
end
|
||||
end
|
||||
|
||||
Twine::Runner.new(options, twine_file)
|
||||
end
|
||||
|
||||
def test_does_not_validate_strings_file
|
||||
prepare_mock_formatter Twine::Formatters::Android
|
||||
|
||||
new_runner(false).generate_string_file
|
||||
end
|
||||
|
||||
def test_validates_strings_file_if_validate
|
||||
assert_raises Twine::Error do
|
||||
new_runner(true).generate_string_file
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,84 +1,84 @@
|
|||
require 'twine_test_case'
|
||||
require 'twine_test'
|
||||
|
||||
class TestOutputProcessor < TwineTestCase
|
||||
class TestOutputProcessor < TwineTest
|
||||
def setup
|
||||
super
|
||||
|
||||
@strings = build_twine_file 'en', 'fr' do
|
||||
@twine_file = build_twine_file 'en', 'fr' do
|
||||
add_section 'Section' do
|
||||
add_row key1: 'value1', tags: ['tag1']
|
||||
add_row key2: 'value2', tags: ['tag1', 'tag2']
|
||||
add_row key3: 'value3', tags: ['tag2']
|
||||
add_row key4: { en: 'value4-en', fr: 'value4-fr' }
|
||||
add_definition key1: 'value1', tags: ['tag1']
|
||||
add_definition key2: 'value2', tags: ['tag1', 'tag2']
|
||||
add_definition key3: 'value3', tags: ['tag2']
|
||||
add_definition key4: { en: 'value4-en', fr: 'value4-fr' }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_includes_all_keys_by_default
|
||||
processor = Twine::Processors::OutputProcessor.new(@strings, {})
|
||||
processor = Twine::Processors::OutputProcessor.new(@twine_file, {})
|
||||
result = processor.process('en')
|
||||
|
||||
assert_equal %w(key1 key2 key3 key4), result.strings_map.keys.sort
|
||||
assert_equal %w(key1 key2 key3 key4), result.definitions_by_key.keys.sort
|
||||
end
|
||||
|
||||
def test_filter_by_tag
|
||||
processor = Twine::Processors::OutputProcessor.new(@strings, { tags: ['tag1'] })
|
||||
processor = Twine::Processors::OutputProcessor.new(@twine_file, { tags: [['tag1']] })
|
||||
result = processor.process('en')
|
||||
|
||||
assert_equal %w(key1 key2), result.strings_map.keys.sort
|
||||
assert_equal %w(key1 key2), result.definitions_by_key.keys.sort
|
||||
end
|
||||
|
||||
def test_filter_by_multiple_tags
|
||||
processor = Twine::Processors::OutputProcessor.new(@strings, { tags: ['tag1', 'tag2'] })
|
||||
processor = Twine::Processors::OutputProcessor.new(@twine_file, { tags: [['tag1', 'tag2']] })
|
||||
result = processor.process('en')
|
||||
|
||||
assert_equal %w(key1 key2 key3), result.strings_map.keys.sort
|
||||
assert_equal %w(key1 key2 key3), result.definitions_by_key.keys.sort
|
||||
end
|
||||
|
||||
def test_filter_untagged
|
||||
processor = Twine::Processors::OutputProcessor.new(@strings, { tags: ['tag1'], untagged: true })
|
||||
processor = Twine::Processors::OutputProcessor.new(@twine_file, { tags: [['tag1']], untagged: true })
|
||||
result = processor.process('en')
|
||||
|
||||
assert_equal %w(key1 key2 key4), result.strings_map.keys.sort
|
||||
assert_equal %w(key1 key2 key4), result.definitions_by_key.keys.sort
|
||||
end
|
||||
|
||||
def test_include_translated
|
||||
processor = Twine::Processors::OutputProcessor.new(@strings, { include: :translated })
|
||||
processor = Twine::Processors::OutputProcessor.new(@twine_file, { include: :translated })
|
||||
result = processor.process('fr')
|
||||
|
||||
assert_equal %w(key4), result.strings_map.keys.sort
|
||||
assert_equal %w(key4), result.definitions_by_key.keys.sort
|
||||
end
|
||||
|
||||
def test_include_untranslated
|
||||
processor = Twine::Processors::OutputProcessor.new(@strings, { include: :untranslated })
|
||||
processor = Twine::Processors::OutputProcessor.new(@twine_file, { include: :untranslated })
|
||||
result = processor.process('fr')
|
||||
|
||||
assert_equal %w(key1 key2 key3), result.strings_map.keys.sort
|
||||
assert_equal %w(key1 key2 key3), result.definitions_by_key.keys.sort
|
||||
end
|
||||
|
||||
class TranslationFallback < TwineTestCase
|
||||
class TranslationFallback < TwineTest
|
||||
def setup
|
||||
super
|
||||
|
||||
@strings = build_twine_file 'en', 'fr', 'de' do
|
||||
@twine_file = build_twine_file 'en', 'fr', 'de' do
|
||||
add_section 'Section' do
|
||||
add_row key1: { en: 'value1-en', fr: 'value1-fr' }
|
||||
add_definition key1: { en: 'value1-en', fr: 'value1-fr' }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_fallback_to_default_language
|
||||
processor = Twine::Processors::OutputProcessor.new(@strings, {})
|
||||
processor = Twine::Processors::OutputProcessor.new(@twine_file, {})
|
||||
result = processor.process('de')
|
||||
|
||||
assert_equal 'value1-en', result.strings_map['key1'].translations['de']
|
||||
assert_equal 'value1-en', result.definitions_by_key['key1'].translations['de']
|
||||
end
|
||||
|
||||
def test_fallback_to_developer_language
|
||||
processor = Twine::Processors::OutputProcessor.new(@strings, {developer_language: 'fr'})
|
||||
processor = Twine::Processors::OutputProcessor.new(@twine_file, {developer_language: 'fr'})
|
||||
result = processor.process('de')
|
||||
|
||||
assert_equal 'value1-fr', result.strings_map['key1'].translations['de']
|
||||
assert_equal 'value1-fr', result.definitions_by_key['key1'].translations['de']
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
require 'twine_test_case'
|
||||
require 'twine_test'
|
||||
|
||||
class PlaceholderTestCase < TwineTestCase
|
||||
class PlaceholderTest < TwineTest
|
||||
def assert_starts_with(prefix, value)
|
||||
msg = message(nil) { "Expected #{mu_pp(value)} to start with #{mu_pp(prefix)}" }
|
||||
assert value.start_with?(prefix), msg
|
||||
|
@ -11,23 +11,25 @@ class PlaceholderTestCase < TwineTestCase
|
|||
lucky = lambda { rand > 0.5 }
|
||||
placeholder = '%'
|
||||
placeholder += (rand * 20).to_i.to_s + '$' if lucky.call
|
||||
placeholder += '-+ 0#'.chars.to_a.sample if lucky.call
|
||||
placeholder += '-+0#'.chars.to_a.sample if lucky.call
|
||||
placeholder += (0.upto(20).map(&:to_s) << "*").sample if lucky.call
|
||||
placeholder += '.' + (0.upto(20).map(&:to_s) << "*").sample if lucky.call
|
||||
placeholder += %w(h hh l ll L z j t).sample if lucky.call
|
||||
placeholder += type || 'diufFeEgGxXocpaA'.chars.to_a.sample # this does not contain s or @ because strings are a special case
|
||||
end
|
||||
end
|
||||
|
||||
class PlaceholderTest < TwineTestCase
|
||||
class ToAndroid < PlaceholderTestCase
|
||||
class ToAndroid < PlaceholderTest
|
||||
def to_android(value)
|
||||
Twine::Placeholders.convert_placeholders_from_twine_to_android(value)
|
||||
end
|
||||
|
||||
def test_replaces_string_placeholder
|
||||
def test_replaces_simple_string_placeholder
|
||||
assert_equal "some '%s' value", to_android("some '%@' value")
|
||||
end
|
||||
|
||||
def test_replaces_complicated_string_placeholder
|
||||
placeholder = placeholder('@')
|
||||
expected = placeholder
|
||||
expected = placeholder.dup
|
||||
expected[-1] = 's'
|
||||
assert_equal "some #{expected} value", to_android("some #{placeholder} value")
|
||||
end
|
||||
|
@ -41,6 +43,11 @@ class PlaceholderTest < TwineTestCase
|
|||
assert_equal "some % value", to_android("some % value")
|
||||
end
|
||||
|
||||
def test_does_not_modify_single_percent_signs_when_followed_by_space_and_format_letter
|
||||
# Said differently: formartter parser should not recognize %a in "70% and"
|
||||
assert_equal 'If 70% and 30% dog 80% end', to_android('If 70% and 30% dog 80% end')
|
||||
end
|
||||
|
||||
def test_escapes_single_percent_signs_if_placeholder_present
|
||||
assert_starts_with "some %% v", to_android("some % value #{placeholder}")
|
||||
end
|
||||
|
@ -72,9 +79,13 @@ class PlaceholderTest < TwineTestCase
|
|||
to_android("some %d second %2$f")
|
||||
end
|
||||
end
|
||||
|
||||
def test_complicated_float_placeholders
|
||||
assert_equal "%1$.0f%2$s (apparent: %3$.0f)", to_android("%.0f%@ (apparent: %.0f)")
|
||||
end
|
||||
end
|
||||
|
||||
class FromAndroid < PlaceholderTestCase
|
||||
class FromAndroid < PlaceholderTest
|
||||
def from_android(value)
|
||||
Twine::Placeholders.convert_placeholders_from_android_to_twine(value)
|
||||
end
|
||||
|
@ -83,4 +94,49 @@ class PlaceholderTest < TwineTestCase
|
|||
assert_equal "some %@ value", from_android("some %s value")
|
||||
end
|
||||
end
|
||||
|
||||
class ToFlash < PlaceholderTest
|
||||
def to_flash(value)
|
||||
Twine::Placeholders.convert_placeholders_from_twine_to_flash(value)
|
||||
end
|
||||
|
||||
def test_replaces_placeholder
|
||||
assert_equal "some {0} text", to_flash("some #{placeholder} text")
|
||||
end
|
||||
|
||||
def test_replaces_string_placeholder
|
||||
assert_equal "some {0} text", to_flash("some #{placeholder('@')} text")
|
||||
end
|
||||
|
||||
def test_numbers_placeholders
|
||||
assert_equal "some {0} more {1} text {2}", to_flash("some #{placeholder('@')} more #{placeholder('@')} text #{placeholder('@')}")
|
||||
end
|
||||
end
|
||||
|
||||
class FromFlash < PlaceholderTest
|
||||
def from_flash(value)
|
||||
Twine::Placeholders.convert_placeholders_from_flash_to_twine(value)
|
||||
end
|
||||
|
||||
def test_maps_all_placeholders_to_string
|
||||
assert_equal "some %@ more %@ text %@", from_flash("some {0} more {1} text {2}")
|
||||
end
|
||||
end
|
||||
|
||||
class PythonPlaceholder < PlaceholderTest
|
||||
def test_negative_for_regular_placeholders
|
||||
assert_equal false, Twine::Placeholders.contains_python_specific_placeholder(placeholder)
|
||||
end
|
||||
|
||||
def test_positive_for_named_placeholders
|
||||
inputs = [
|
||||
"%(language)s has",
|
||||
"For %(number)03d quotes",
|
||||
"bought on %(app_name)s"
|
||||
]
|
||||
inputs.each do |input|
|
||||
assert_equal true, Twine::Placeholders.contains_python_specific_placeholder(input)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,58 +0,0 @@
|
|||
require 'twine_test_case'
|
||||
|
||||
class TestStringsFile < TwineTestCase
|
||||
class Reading < TwineTestCase
|
||||
def setup
|
||||
super
|
||||
|
||||
@strings = Twine::StringsFile.new
|
||||
@strings.read fixture_path('twine_accent_values.txt')
|
||||
end
|
||||
|
||||
def test_reading_keeps_leading_accent
|
||||
assert_equal '`value', @strings.strings_map['value_with_leading_accent'].translations['en']
|
||||
end
|
||||
|
||||
def test_reading_keeps_trailing_accent
|
||||
assert_equal 'value`', @strings.strings_map['value_with_trailing_accent'].translations['en']
|
||||
end
|
||||
|
||||
def test_reading_keeps_leading_space
|
||||
assert_equal ' value', @strings.strings_map['value_with_leading_space'].translations['en']
|
||||
end
|
||||
|
||||
def test_reading_keeps_trailing_space
|
||||
assert_equal 'value ', @strings.strings_map['value_with_trailing_space'].translations['en']
|
||||
end
|
||||
|
||||
def test_reading_keeps_wrapping_spaces
|
||||
assert_equal ' value ', @strings.strings_map['value_wrapped_by_spaces'].translations['en']
|
||||
end
|
||||
|
||||
def test_reading_keeps_wrapping_accents
|
||||
assert_equal '`value`', @strings.strings_map['value_wrapped_by_accents'].translations['en']
|
||||
end
|
||||
end
|
||||
|
||||
class Writing < TwineTestCase
|
||||
|
||||
def test_accent_wrapping
|
||||
@strings = build_twine_file 'en' do
|
||||
add_section 'Section' do
|
||||
add_row value_with_leading_accent: '`value'
|
||||
add_row value_with_trailing_accent: 'value`'
|
||||
add_row value_with_leading_space: ' value'
|
||||
add_row value_with_trailing_space: 'value '
|
||||
add_row value_wrapped_by_spaces: ' value '
|
||||
add_row value_wrapped_by_accents: '`value`'
|
||||
end
|
||||
end
|
||||
|
||||
@strings.write @output_path
|
||||
|
||||
assert_equal content('twine_accent_values.txt'), File.read(@output_path)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
|
@ -1,47 +0,0 @@
|
|||
require 'twine_test_case'
|
||||
|
||||
class TestStringsRow < TwineTestCase
|
||||
def setup
|
||||
super
|
||||
|
||||
@reference = Twine::StringsRow.new 'reference-key'
|
||||
@reference.comment = 'reference comment'
|
||||
@reference.tags = ['ref1']
|
||||
@reference.translations['en'] = 'ref-value'
|
||||
|
||||
@row = Twine::StringsRow.new 'key'
|
||||
@row.reference_key = @reference.key
|
||||
@row.reference = @reference
|
||||
end
|
||||
|
||||
def test_reference_comment_used
|
||||
assert_equal 'reference comment', @row.comment
|
||||
end
|
||||
|
||||
def test_reference_comment_override
|
||||
@row.comment = 'row comment'
|
||||
|
||||
assert_equal 'row comment', @row.comment
|
||||
end
|
||||
|
||||
def test_reference_tags_used
|
||||
assert @row.matches_tags?(['ref1'], false)
|
||||
end
|
||||
|
||||
def test_reference_tags_override
|
||||
@row.tags = ['tag1']
|
||||
|
||||
refute @row.matches_tags?(['ref1'], false)
|
||||
assert @row.matches_tags?(['tag1'], false)
|
||||
end
|
||||
|
||||
def test_reference_translation_used
|
||||
assert_equal 'ref-value', @row.translated_string_for_lang('en')
|
||||
end
|
||||
|
||||
def test_reference_translation_override
|
||||
@row.translations['en'] = 'value'
|
||||
|
||||
assert_equal 'value', @row.translated_string_for_lang('en')
|
||||
end
|
||||
end
|
111
test/test_twine_definition.rb
Normal file
111
test/test_twine_definition.rb
Normal file
|
@ -0,0 +1,111 @@
|
|||
require 'twine_test'
|
||||
|
||||
class TestTwineDefinition < TwineTest
|
||||
class TestTags < TwineTest
|
||||
def setup
|
||||
super
|
||||
@definition = Twine::TwineDefinition.new 'key'
|
||||
end
|
||||
|
||||
def test_include_untagged
|
||||
assert @definition.matches_tags?([[rand(100000).to_s]], true)
|
||||
end
|
||||
|
||||
def test_matches_no_given_tags
|
||||
assert @definition.matches_tags?([], false)
|
||||
end
|
||||
|
||||
def test_matches_tag
|
||||
@definition.tags = ['tag1']
|
||||
|
||||
assert @definition.matches_tags?([['tag1']], false)
|
||||
end
|
||||
|
||||
def test_matches_any_tag
|
||||
@definition.tags = ['tag1']
|
||||
|
||||
assert @definition.matches_tags?([['tag0', 'tag1', 'tag2']], false)
|
||||
end
|
||||
|
||||
def test_matches_all_tags
|
||||
@definition.tags = ['tag1', 'tag2']
|
||||
|
||||
assert @definition.matches_tags?([['tag1'], ['tag2']], false)
|
||||
end
|
||||
|
||||
def test_does_not_match_all_tags
|
||||
@definition.tags = ['tag1']
|
||||
|
||||
refute @definition.matches_tags?([['tag1'], ['tag2']], false)
|
||||
end
|
||||
|
||||
def test_does_not_match_excluded_tag
|
||||
@definition.tags = ['tag1']
|
||||
|
||||
refute @definition.matches_tags?([['~tag1']], false)
|
||||
end
|
||||
|
||||
def test_matches_excluded_tag
|
||||
@definition.tags = ['tag2']
|
||||
|
||||
assert @definition.matches_tags?([['~tag1']], false)
|
||||
end
|
||||
|
||||
def test_complex_rules
|
||||
@definition.tags = ['tag1', 'tag2', 'tag3']
|
||||
|
||||
assert @definition.matches_tags?([['tag1']], false)
|
||||
assert @definition.matches_tags?([['tag1', 'tag4']], false)
|
||||
assert @definition.matches_tags?([['tag1'], ['tag2'], ['tag3']], false)
|
||||
refute @definition.matches_tags?([['tag1'], ['tag4']], false)
|
||||
|
||||
assert @definition.matches_tags?([['tag4', '~tag5']], false)
|
||||
end
|
||||
end
|
||||
|
||||
class TestReferences < TwineTest
|
||||
def setup
|
||||
super
|
||||
|
||||
@reference = Twine::TwineDefinition.new 'reference-key'
|
||||
@reference.comment = 'reference comment'
|
||||
@reference.tags = ['ref1']
|
||||
@reference.translations['en'] = 'ref-value'
|
||||
|
||||
@definition = Twine::TwineDefinition.new 'key'
|
||||
@definition.reference_key = @reference.key
|
||||
@definition.reference = @reference
|
||||
end
|
||||
|
||||
def test_reference_comment_used
|
||||
assert_equal 'reference comment', @definition.comment
|
||||
end
|
||||
|
||||
def test_reference_comment_override
|
||||
@definition.comment = 'definition comment'
|
||||
|
||||
assert_equal 'definition comment', @definition.comment
|
||||
end
|
||||
|
||||
def test_reference_tags_used
|
||||
assert @definition.matches_tags?([['ref1']], false)
|
||||
end
|
||||
|
||||
def test_reference_tags_override
|
||||
@definition.tags = ['tag1']
|
||||
|
||||
refute @definition.matches_tags?([['ref1']], false)
|
||||
assert @definition.matches_tags?([['tag1']], false)
|
||||
end
|
||||
|
||||
def test_reference_translation_used
|
||||
assert_equal 'ref-value', @definition.translation_for_lang('en')
|
||||
end
|
||||
|
||||
def test_reference_translation_override
|
||||
@definition.translations['en'] = 'value'
|
||||
|
||||
assert_equal 'value', @definition.translation_for_lang('en')
|
||||
end
|
||||
end
|
||||
end
|
58
test/test_twine_file.rb
Normal file
58
test/test_twine_file.rb
Normal file
|
@ -0,0 +1,58 @@
|
|||
require 'twine_test'
|
||||
|
||||
class TestTwineFile < TwineTest
|
||||
class Reading < TwineTest
|
||||
def setup
|
||||
super
|
||||
|
||||
@twine_file = Twine::TwineFile.new
|
||||
@twine_file.read fixture_path('twine_accent_values.txt')
|
||||
end
|
||||
|
||||
def test_reading_keeps_leading_accent
|
||||
assert_equal '`value', @twine_file.definitions_by_key['value_with_leading_accent'].translations['en']
|
||||
end
|
||||
|
||||
def test_reading_keeps_trailing_accent
|
||||
assert_equal 'value`', @twine_file.definitions_by_key['value_with_trailing_accent'].translations['en']
|
||||
end
|
||||
|
||||
def test_reading_keeps_leading_space
|
||||
assert_equal ' value', @twine_file.definitions_by_key['value_with_leading_space'].translations['en']
|
||||
end
|
||||
|
||||
def test_reading_keeps_trailing_space
|
||||
assert_equal 'value ', @twine_file.definitions_by_key['value_with_trailing_space'].translations['en']
|
||||
end
|
||||
|
||||
def test_reading_keeps_wrapping_spaces
|
||||
assert_equal ' value ', @twine_file.definitions_by_key['value_wrapped_by_spaces'].translations['en']
|
||||
end
|
||||
|
||||
def test_reading_keeps_wrapping_accents
|
||||
assert_equal '`value`', @twine_file.definitions_by_key['value_wrapped_by_accents'].translations['en']
|
||||
end
|
||||
end
|
||||
|
||||
class Writing < TwineTest
|
||||
|
||||
def test_accent_wrapping
|
||||
@twine_file = build_twine_file 'en' do
|
||||
add_section 'Section' do
|
||||
add_definition value_with_leading_accent: '`value'
|
||||
add_definition value_with_trailing_accent: 'value`'
|
||||
add_definition value_with_leading_space: ' value'
|
||||
add_definition value_with_trailing_space: 'value '
|
||||
add_definition value_wrapped_by_spaces: ' value '
|
||||
add_definition value_wrapped_by_accents: '`value`'
|
||||
end
|
||||
end
|
||||
|
||||
@twine_file.write @output_path
|
||||
|
||||
assert_equal content('twine_accent_values.txt'), File.read(@output_path)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
|
@ -1,61 +0,0 @@
|
|||
# encoding: utf-8
|
||||
|
||||
require 'command_test_case'
|
||||
|
||||
class TestValidateStringsFile < CommandTestCase
|
||||
def setup
|
||||
super
|
||||
@options = { strings_file: 'input.txt' }
|
||||
|
||||
@twine_file = build_twine_file 'en' do
|
||||
add_section 'Section 1' do
|
||||
add_row key1: 'value1', tags: ['tag1']
|
||||
add_row key2: 'value2', tags: ['tag1']
|
||||
end
|
||||
|
||||
add_section 'Section 2' do
|
||||
add_row key3: 'value3', tags: ['tag1', 'tag2']
|
||||
add_row key4: 'value4', tags: ['tag2']
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def random_row
|
||||
@twine_file.strings_map[@twine_file.strings_map.keys.sample]
|
||||
end
|
||||
|
||||
def test_recognizes_valid_file
|
||||
Twine::Runner.new(@options, @twine_file).validate_strings_file
|
||||
assert_equal "input.txt is valid.\n", Twine::stdout.string
|
||||
end
|
||||
|
||||
def test_reports_duplicate_keys
|
||||
@twine_file.sections[0].rows << random_row
|
||||
|
||||
assert_raises Twine::Error do
|
||||
Twine::Runner.new(@options, @twine_file).validate_strings_file
|
||||
end
|
||||
end
|
||||
|
||||
def test_reports_invalid_characters_in_keys
|
||||
random_row.key[0] = "!?;:,^`´'\"\\|/(){}[]~-+*=#$%".chars.to_a.sample
|
||||
|
||||
assert_raises Twine::Error do
|
||||
Twine::Runner.new(@options, @twine_file).validate_strings_file
|
||||
end
|
||||
end
|
||||
|
||||
def test_does_not_reports_missing_tags_by_default
|
||||
random_row.tags.clear
|
||||
|
||||
Twine::Runner.new(@options, @twine_file).validate_strings_file
|
||||
end
|
||||
|
||||
def test_reports_missing_tags
|
||||
random_row.tags.clear
|
||||
|
||||
assert_raises Twine::Error do
|
||||
Twine::Runner.new(@options.merge(pedantic: true), @twine_file).validate_strings_file
|
||||
end
|
||||
end
|
||||
end
|
69
test/test_validate_twine_file.rb
Normal file
69
test/test_validate_twine_file.rb
Normal file
|
@ -0,0 +1,69 @@
|
|||
# encoding: utf-8
|
||||
|
||||
require 'command_test'
|
||||
|
||||
class TestValidateTwineFile < CommandTest
|
||||
def setup
|
||||
super
|
||||
@options = { twine_file: 'input.txt' }
|
||||
|
||||
@twine_file = build_twine_file 'en' do
|
||||
add_section 'Section 1' do
|
||||
add_definition key1: 'value1', tags: ['tag1']
|
||||
add_definition key2: 'value2', tags: ['tag1']
|
||||
end
|
||||
|
||||
add_section 'Section 2' do
|
||||
add_definition key3: 'value3', tags: ['tag1', 'tag2']
|
||||
add_definition key4: 'value4', tags: ['tag2']
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def random_definition
|
||||
@twine_file.definitions_by_key[@twine_file.definitions_by_key.keys.sample]
|
||||
end
|
||||
|
||||
def test_recognizes_valid_file
|
||||
Twine::Runner.new(@options, @twine_file).validate_twine_file
|
||||
assert_equal "input.txt is valid.\n", Twine::stdout.string
|
||||
end
|
||||
|
||||
def test_reports_duplicate_keys
|
||||
@twine_file.sections[0].definitions << random_definition
|
||||
|
||||
assert_raises Twine::Error do
|
||||
Twine::Runner.new(@options, @twine_file).validate_twine_file
|
||||
end
|
||||
end
|
||||
|
||||
def test_reports_invalid_characters_in_keys
|
||||
random_definition.key[0] = "!?;:,^`´'\"\\|/(){}[]~-+*=#$%".chars.to_a.sample
|
||||
|
||||
assert_raises Twine::Error do
|
||||
Twine::Runner.new(@options, @twine_file).validate_twine_file
|
||||
end
|
||||
end
|
||||
|
||||
def test_does_not_reports_missing_tags_by_default
|
||||
random_definition.tags.clear
|
||||
|
||||
Twine::Runner.new(@options, @twine_file).validate_twine_file
|
||||
end
|
||||
|
||||
def test_reports_missing_tags
|
||||
random_definition.tags.clear
|
||||
|
||||
assert_raises Twine::Error do
|
||||
Twine::Runner.new(@options.merge(pedantic: true), @twine_file).validate_twine_file
|
||||
end
|
||||
end
|
||||
|
||||
def test_reports_python_specific_placeholders
|
||||
random_definition.translations["en"] = "%(python_only)s"
|
||||
|
||||
assert_raises Twine::Error do
|
||||
Twine::Runner.new(@options, @twine_file).validate_twine_file
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,6 +1,6 @@
|
|||
module TwineFileDSL
|
||||
def build_twine_file(*languages)
|
||||
@currently_built_twine_file = Twine::StringsFile.new
|
||||
@currently_built_twine_file = Twine::TwineFile.new
|
||||
@currently_built_twine_file.language_codes.concat languages
|
||||
yield
|
||||
result = @currently_built_twine_file
|
||||
|
@ -10,37 +10,37 @@ module TwineFileDSL
|
|||
|
||||
def add_section(name)
|
||||
return unless @currently_built_twine_file
|
||||
@currently_built_twine_file_section = Twine::StringsSection.new name
|
||||
@currently_built_twine_file_section = Twine::TwineSection.new name
|
||||
@currently_built_twine_file.sections << @currently_built_twine_file_section
|
||||
yield
|
||||
@currently_built_twine_file_section = nil
|
||||
end
|
||||
|
||||
def add_row(parameters)
|
||||
def add_definition(parameters)
|
||||
return unless @currently_built_twine_file
|
||||
return unless @currently_built_twine_file_section
|
||||
|
||||
# this relies on Ruby preserving the order of hash elements
|
||||
key, value = parameters.first
|
||||
row = Twine::StringsRow.new(key.to_s)
|
||||
definition = Twine::TwineDefinition.new(key.to_s)
|
||||
if value.is_a? Hash
|
||||
value.each do |language, translation|
|
||||
row.translations[language.to_s] = translation
|
||||
definition.translations[language.to_s] = translation
|
||||
end
|
||||
elsif !value.is_a? Symbol
|
||||
language = @currently_built_twine_file.language_codes.first
|
||||
row.translations[language] = value
|
||||
definition.translations[language] = value
|
||||
end
|
||||
|
||||
row.comment = parameters[:comment] if parameters[:comment]
|
||||
row.tags = parameters[:tags] if parameters[:tags]
|
||||
definition.comment = parameters[:comment] if parameters[:comment]
|
||||
definition.tags = parameters[:tags] if parameters[:tags]
|
||||
if parameters[:ref] || value.is_a?(Symbol)
|
||||
reference_key = (parameters[:ref] || value).to_s
|
||||
row.reference_key = reference_key
|
||||
row.reference = @currently_built_twine_file.strings_map[reference_key]
|
||||
definition.reference_key = reference_key
|
||||
definition.reference = @currently_built_twine_file.definitions_by_key[reference_key]
|
||||
end
|
||||
|
||||
@currently_built_twine_file_section.rows << row
|
||||
@currently_built_twine_file.strings_map[row.key] = row
|
||||
@currently_built_twine_file_section.definitions << definition
|
||||
@currently_built_twine_file.definitions_by_key[definition.key] = definition
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
require 'erb'
|
||||
require 'minitest/autorun'
|
||||
require "mocha/mini_test"
|
||||
require "mocha/minitest"
|
||||
require 'securerandom'
|
||||
require 'stringio'
|
||||
require 'twine'
|
||||
require 'twine_file_dsl'
|
||||
|
||||
class TwineTestCase < Minitest::Test
|
||||
class TwineTest < Minitest::Test
|
||||
include TwineFileDSL
|
||||
|
||||
KNOWN_LANGUAGES = %w(en fr de es)
|
||||
|
@ -23,14 +23,14 @@ class TwineTestCase < Minitest::Test
|
|||
end
|
||||
|
||||
def teardown
|
||||
FileUtils.remove_entry_secure @output_dir if File.exists? @output_dir
|
||||
FileUtils.remove_entry_secure @output_dir if File.exist? @output_dir
|
||||
Twine::Formatters.formatters.clear
|
||||
Twine::Formatters.formatters.concat @formatters
|
||||
super
|
||||
end
|
||||
|
||||
def execute(command)
|
||||
command += " -o #{@output_path}"
|
||||
command += " -o #{@output_path}"
|
||||
Twine::Runner.run(command.split(" "))
|
||||
end
|
||||
|
2
twine
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 "$@"
|
||||
|
|
|
@ -5,8 +5,8 @@ Gem::Specification.new do |s|
|
|||
s.name = "twine"
|
||||
s.version = Twine::VERSION
|
||||
s.date = Time.now.strftime('%Y-%m-%d')
|
||||
s.summary = "Manage strings and their translations for your iOS and Android projects."
|
||||
s.homepage = "https://github.com/mobiata/twine"
|
||||
s.summary = "Manage strings and their translations for your iOS, Android and other projects."
|
||||
s.homepage = "https://github.com/scelis/twine"
|
||||
s.email = "twine@mobiata.com"
|
||||
s.authors = [ "Sebastian Celis" ]
|
||||
s.has_rdoc = false
|
||||
|
@ -18,11 +18,13 @@ Gem::Specification.new do |s|
|
|||
s.files += Dir.glob("test/**/*")
|
||||
s.test_files = Dir.glob("test/test_*")
|
||||
|
||||
s.required_ruby_version = ">= 2.0"
|
||||
s.add_runtime_dependency('rubyzip', "~> 1.1")
|
||||
s.required_ruby_version = ">= 2.6"
|
||||
s.add_runtime_dependency('rexml', "~> 3.2")
|
||||
s.add_runtime_dependency('rubyzip', "~> 2.0")
|
||||
s.add_runtime_dependency('safe_yaml', "~> 1.0")
|
||||
s.add_development_dependency('rake', "~> 10.4")
|
||||
s.add_development_dependency('rake', "~> 13.0")
|
||||
s.add_development_dependency('minitest', "~> 5.5")
|
||||
s.add_development_dependency('minitest-ci', "~> 3.0")
|
||||
s.add_development_dependency('mocha', "~> 1.1")
|
||||
|
||||
s.executables = %w( twine )
|
||||
|
|
Reference in a new issue