forked from organicmaps/organicmaps
[twine] Upgrade twine to 0.10.1
This commit is contained in:
parent
3c9fd26dcf
commit
6de61479bf
48 changed files with 1746 additions and 1660 deletions
31
tools/twine/0.10.1.patch
Normal file
31
tools/twine/0.10.1.patch
Normal file
|
@ -0,0 +1,31 @@
|
|||
diff -r twine-0.10.1/lib/twine/formatters/android.rb twine/lib/twine/formatters/android.rb
|
||||
9a10,17
|
||||
> LANG_CODES = Hash[
|
||||
> 'zh' => 'zh-Hans',
|
||||
> 'zh-CN' => 'zh-Hans',
|
||||
> 'zh-HK' => 'zh-Hant',
|
||||
> 'en-GB' => 'en-GB',
|
||||
> 'in' => 'id'
|
||||
> ]
|
||||
>
|
||||
36c44,47
|
||||
< return match[1].sub('-r', '-') if match
|
||||
---
|
||||
> if match
|
||||
> lang = match[1].sub('-r', '-')
|
||||
> return LANG_CODES.fetch(lang, lang)
|
||||
> end
|
||||
diff -r twine-0.10.1/lib/twine/formatters/jquery.rb twine/lib/twine/formatters/jquery.rb
|
||||
19c19
|
||||
< match = /^((.+)-)?([^-]+)\.json$/.match(segment)
|
||||
---
|
||||
> match = /^(.+)\.json$/.match(segment)
|
||||
21c21
|
||||
< return match[3]
|
||||
---
|
||||
> return match[1]
|
||||
diff -r twine-0.10.1/lib/twine/placeholders.rb twine/lib/twine/placeholders.rb
|
||||
8c8
|
||||
< PLACEHOLDER_TYPES = '[diufFeEgGxXoscpaA]'
|
||||
---
|
||||
> PLACEHOLDER_TYPES = '[diufFeEgGxXoscpaAq]'
|
|
@ -1,6 +1,6 @@
|
|||
# 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 master text file and then Twine uses this file to import and export localization files in a variety of types, including iOS and Mac OS X `.strings` files, Android `.xml` files, gettext `.po` files, and [jquery-localize][jquerylocalize] `.json` files. This allows individuals and companies to easily share translations across multiple projects, as well as export localization files in any format the user wants.
|
||||
|
||||
## Install
|
||||
|
||||
|
@ -21,22 +21,33 @@ You can also run Twine directly from source. However, it requires [rubyzip][ruby
|
|||
|
||||
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 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.
|
||||
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.
|
||||
|
||||
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][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 `generate-report` 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 defintions containing all specified tags (`--tags tag1 --tags tag2` matches all definitions tagged with `tag1` _and_ `tag2`). You can match definitions _not_ containing a tag by prefixing the tag with a tilde (`--tags ~tag1` matches all definitions _not_ tagged with `tag1`). All three options are combinable.
|
||||
|
||||
### Whitespace
|
||||
|
||||
Whitepace in this file is mostly ignored. If you absolutely need to put spaces at the beginning or end of your translated string, you can wrap the entire string in a pair of `` ` `` characters. If your actual string needs to start *and* end with a grave accent, you can wrap it in another pair of `` ` `` characters. See the example, below.
|
||||
|
||||
### References
|
||||
|
||||
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
|
||||
|
||||
```ini
|
||||
[[General]]
|
||||
[yes]
|
||||
en = Yes
|
||||
|
@ -57,6 +68,9 @@ Whitepace in this file is mostly ignored. If you absolutely need to put spaces a
|
|||
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]
|
||||
|
@ -67,10 +81,11 @@ Whitepace in this file is mostly ignored. If you absolutely need to put spaces a
|
|||
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)
|
||||
|
@ -78,74 +93,69 @@ Twine currently supports the following formats for outputting strings:
|
|||
* [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.
|
||||
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. 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. It is often used for creating 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
|
||||
|
||||
#### `generate-report`
|
||||
#### `validate-twine-file`
|
||||
|
||||
This command gives you useful information about your strings. It will tell you how many strings you have and how many have been translated into each language.
|
||||
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 generate-report /path/to/strings.txt
|
||||
$ twine validate-twine-file /path/to/twine.txt
|
||||
|
||||
#### `validate-strings-file`
|
||||
## Creating Your First Twine Data 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.
|
||||
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.
|
||||
|
||||
$ twine validate-strings-file /path/to/strings.txt
|
||||
|
||||
## Creating Your First strings.txt 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.
|
||||
|
||||
$ 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
|
||||
|
||||
## Twine and Your Build Process
|
||||
|
||||
|
@ -154,12 +164,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.
|
||||
|
@ -171,8 +181,8 @@ Now, whenever you build your application, Xcode will automatically invoke Twine
|
|||
|
||||
Add the following task at the top level in 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'
|
||||
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
|
||||
|
@ -180,35 +190,17 @@ task generateStrings {
|
|||
}
|
||||
```
|
||||
|
||||
Now every time you build your app the strings are generated from the twine file.
|
||||
Now every time you build your app the localization files are generated from the Twine file.
|
||||
|
||||
|
||||
## 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 TextMate 2 Bundle](https://github.com/mobiata/twine.tmbundle) — This [TextMate 2](https://github.com/textmate/textmate) bundle will make it easier for you to work with Twine files. In particular, it lets you use code folding to easily collapse and expand both definitions and sections.
|
||||
* [twine_ui](https://github.com/Daij-Djan/twine_ui) — A user interface for Twine written by [Dominik Pich](https://github.com/Daij-Djan/). Consider using this if you would prefer to use Twine without dropping to a command line.
|
||||
|
||||
## 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
|
||||
|
||||
|
@ -222,6 +214,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/)
|
||||
* [Sebastian Ludwig](https://github.com/sebastianludwig)
|
||||
* [Sergey Pisarchik](https://github.com/SergeyPisarchik)
|
||||
* [Shai Shamir](https://github.com/pichirichi)
|
||||
|
||||
|
@ -235,3 +228,5 @@ Many thanks to all of the contributors to the Twine project, including:
|
|||
[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
|
||||
|
|
|
@ -2,7 +2,7 @@ require 'rake'
|
|||
require 'rake/testtask'
|
||||
|
||||
Rake::TestTask.new do |t|
|
||||
t.test_files = %w(test/twine_test.rb)
|
||||
t.libs = ['lib', 'test']
|
||||
end
|
||||
|
||||
task :default => :test
|
||||
|
|
|
@ -1,12 +1,41 @@
|
|||
module Twine
|
||||
@@stdout = STDOUT
|
||||
@@stderr = STDERR
|
||||
|
||||
def self.stdout
|
||||
@@stdout
|
||||
end
|
||||
|
||||
def self.stdout=(out)
|
||||
@@stdout = out
|
||||
end
|
||||
|
||||
def self.stderr
|
||||
@@stderr
|
||||
end
|
||||
|
||||
def self.stderr=(err)
|
||||
@@stderr = err
|
||||
end
|
||||
|
||||
class Error < StandardError
|
||||
end
|
||||
|
||||
require 'twine/plugin'
|
||||
require 'twine/cli'
|
||||
require 'twine/encoding'
|
||||
require 'twine/formatters'
|
||||
require 'twine/runner'
|
||||
require 'twine/stringsfile'
|
||||
require 'twine/version'
|
||||
require 'twine/plugin'
|
||||
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/django'
|
||||
require 'twine/formatters/flash'
|
||||
require 'twine/formatters/gettext'
|
||||
require 'twine/formatters/jquery'
|
||||
require 'twine/formatters/tizen'
|
||||
require 'twine/runner'
|
||||
require 'twine/cli'
|
||||
end
|
||||
|
|
|
@ -1,193 +1,409 @@
|
|||
require 'optparse'
|
||||
require 'io/console'
|
||||
|
||||
module Twine
|
||||
class CLI
|
||||
def initialize(args, options)
|
||||
@options = options
|
||||
@args = args
|
||||
end
|
||||
module CLI
|
||||
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
|
||||
},
|
||||
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).'
|
||||
},
|
||||
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.'
|
||||
}
|
||||
}
|
||||
|
||||
def self.parse_args(args, options)
|
||||
new(args, options).parse_args
|
||||
end
|
||||
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,
|
||||
:format,
|
||||
:include,
|
||||
:languages,
|
||||
: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,
|
||||
:file_name,
|
||||
:format,
|
||||
:include,
|
||||
: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,
|
||||
:include,
|
||||
: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,
|
||||
: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,
|
||||
: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,
|
||||
: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
|
||||
],
|
||||
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 parse_args
|
||||
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, generate reports on the strings, as well as create formatted string files to ship with your products. Twine currently supports iOS, OS X, Android, gettext, and jquery-localize string files.'
|
||||
opts.separator ''
|
||||
opts.separator 'Commands:'
|
||||
opts.separator ''
|
||||
opts.separator 'generate-string-file -- 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 -- 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 -- 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 -- 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 -- 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. This command assumes that --include-untranslated has been specified on the command line.'
|
||||
opts.separator ''
|
||||
opts.separator 'consume-loc-drop -- 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 'generate-report -- Generates a report containing data about your strings. It will tell you how many strings you have and how many of those strings have been translated into each language.'
|
||||
opts.separator ''
|
||||
opts.separator 'validate-strings-file -- 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 |langs|
|
||||
@options[:languages] = langs
|
||||
end
|
||||
opts.on('-t', '--tags TAGS', Array, 'The tag(s) to use for the specified action. Only strings with that tag will be processed. Do not specify any tags to match all strings in the strings data file.') do |tags|
|
||||
@options[:tags] = tags
|
||||
end
|
||||
opts.on('-u', '--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] = true
|
||||
end
|
||||
formats = []
|
||||
Formatters.formatters.each do |formatter|
|
||||
formats << formatter::FORMAT_NAME
|
||||
end
|
||||
opts.on('-f', '--format FORMAT', "The file format to read or write (#{formats.join(', ')}). Additional formatters can be placed in the formats/ directory.") do |format|
|
||||
lformat = format.downcase
|
||||
if !formats.include?(lformat)
|
||||
STDERR.puts "Invalid format: #{format}"
|
||||
end
|
||||
@options[:format] = lformat
|
||||
end
|
||||
opts.on('-a', '--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('-s', '--include-untranslated', 'This flag will cause any Android string files that are generated to include strings that have not yet been translated for the current language.') do |s|
|
||||
@options[:include_untranslated] = true
|
||||
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('-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.') do |d|
|
||||
@options[:developer_language] = d
|
||||
end
|
||||
opts.on('-c', '--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] = true
|
||||
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. This flag currently only works with Apple .strings files and is currently only supported in Ruby 1.9.3 or greater.') do |e|
|
||||
if !"".respond_to?(:encode)
|
||||
raise Twine::Error.new "The --encoding flag is only supported on Ruby 1.9.3 or greater."
|
||||
end
|
||||
@options[:output_encoding] = e
|
||||
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 |x|
|
||||
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 generate-report strings.txt'
|
||||
opts.separator '> twine validate-strings-file strings.txt'
|
||||
def self.parse(args)
|
||||
command = args.select { |a| a[0] != '-' }[0]
|
||||
args = args.reject { |a| a == command }
|
||||
|
||||
mapped_command = DEPRECATED_COMMAND_MAPPINGS[command]
|
||||
if mapped_command
|
||||
Twine::stderr.puts "WARNING: Twine commands names have changed. `#{command}` is now `#{mapped_command}`. The old command is deprecated will soon stop working. For more information please check the documentation at https://github.com/mobiata/twine"
|
||||
command = mapped_command
|
||||
end
|
||||
parser.parse! @args
|
||||
|
||||
if @args.length == 0
|
||||
unless COMMANDS.keys.include? command
|
||||
Twine::stderr.puts "Invalid command: #{command}" unless command.nil?
|
||||
print_help(args)
|
||||
abort
|
||||
end
|
||||
|
||||
options = parse_command_options(command, args)
|
||||
|
||||
return options
|
||||
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
|
||||
elsif option[:boolean]
|
||||
result[option_name] = true
|
||||
else
|
||||
result[option_name] = value
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
parser.define('-h', '--help', 'Show this message.') do
|
||||
puts parser.help
|
||||
exit
|
||||
end
|
||||
|
||||
@options[:command] = @args[0]
|
||||
parser.separator ''
|
||||
parser.separator 'Examples:'
|
||||
parser.separator ''
|
||||
parser.separator "> #{command[:example]}"
|
||||
|
||||
if !VALID_COMMANDS.include? @options[:command]
|
||||
raise Twine::Error.new "Invalid command: #{@options[:command]}"
|
||||
begin
|
||||
parser.parse! args
|
||||
rescue OptionParser::ParseError => e
|
||||
raise Twine::Error.new e.message
|
||||
end
|
||||
|
||||
if @args.length == 1
|
||||
raise Twine::Error.new 'You must specify your strings file.'
|
||||
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
|
||||
|
||||
@options[:strings_file] = @args[1]
|
||||
if args.length > command[:arguments].size
|
||||
raise Twine::Error.new "Unknown argument: #{args[command[:arguments].size]}"
|
||||
end
|
||||
|
||||
case @options[:command]
|
||||
when 'generate-string-file'
|
||||
if @args.length == 3
|
||||
@options[:output_path] = @args[2]
|
||||
elsif @args.length > 3
|
||||
raise Twine::Error.new "Unknown argument: #{@args[3]}"
|
||||
else
|
||||
raise Twine::Error.new 'Not enough arguments.'
|
||||
end
|
||||
if @options[:languages] and @options[:languages].length > 1
|
||||
raise Twine::Error.new 'Please only specify a single language for the generate-string-file command.'
|
||||
end
|
||||
when 'generate-all-string-files'
|
||||
if ARGV.length == 3
|
||||
@options[:output_path] = @args[2]
|
||||
elsif @args.length > 3
|
||||
raise Twine::Error.new "Unknown argument: #{@args[3]}"
|
||||
else
|
||||
raise Twine::Error.new 'Not enough arguments.'
|
||||
end
|
||||
when 'consume-string-file'
|
||||
if @args.length == 3
|
||||
@options[:input_path] = @args[2]
|
||||
elsif @args.length > 3
|
||||
raise Twine::Error.new "Unknown argument: #{@args[3]}"
|
||||
else
|
||||
raise Twine::Error.new 'Not enough arguments.'
|
||||
end
|
||||
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'
|
||||
if @args.length == 3
|
||||
@options[:input_path] = @args[2]
|
||||
elsif @args.length > 3
|
||||
raise Twine::Error.new "Unknown argument: #{@args[3]}"
|
||||
else
|
||||
raise Twine::Error.new 'Not enough arguments.'
|
||||
end
|
||||
when 'generate-loc-drop'
|
||||
@options[:include_untranslated] = true
|
||||
if @args.length == 3
|
||||
@options[:output_path] = @args[2]
|
||||
elsif @args.length > 3
|
||||
raise Twine::Error.new "Unknown argument: #{@args[3]}"
|
||||
else
|
||||
raise Twine::Error.new 'Not enough arguments.'
|
||||
end
|
||||
if !@options[:format]
|
||||
raise Twine::Error.new 'You must specify a format.'
|
||||
end
|
||||
when 'consume-loc-drop'
|
||||
if @args.length == 3
|
||||
@options[:input_path] = @args[2]
|
||||
elsif @args.length > 3
|
||||
raise Twine::Error.new "Unknown argument: #{@args[3]}"
|
||||
else
|
||||
raise Twine::Error.new 'Not enough arguments.'
|
||||
end
|
||||
when 'generate-report'
|
||||
if @args.length > 2
|
||||
raise Twine::Error.new "Unknown argument: #{@args[2]}"
|
||||
end
|
||||
when 'validate-strings-file'
|
||||
if @args.length > 2
|
||||
raise Twine::Error.new "Unknown argument: #{@args[2]}"
|
||||
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
|
||||
end
|
||||
|
||||
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
|
||||
|
|
|
@ -1,20 +1,22 @@
|
|||
module Twine
|
||||
module Encoding
|
||||
def self.encoding_for_path path
|
||||
File.open(path, 'rb') do |f|
|
||||
begin
|
||||
a = f.readbyte
|
||||
b = f.readbyte
|
||||
if (a == 0xfe && b == 0xff)
|
||||
return 'UTF-16BE'
|
||||
elsif (a == 0xff && b == 0xfe)
|
||||
return 'UTF-16LE'
|
||||
end
|
||||
rescue EOFError
|
||||
end
|
||||
end
|
||||
|
||||
'UTF-8'
|
||||
def self.bom(path)
|
||||
first_bytes = IO.binread(path, 2)
|
||||
return nil unless first_bytes
|
||||
first_bytes = first_bytes.codepoints.map.to_a
|
||||
return 'UTF-16BE' if first_bytes == [0xFE, 0xFF]
|
||||
return 'UTF-16LE' if first_bytes == [0xFF, 0xFE]
|
||||
rescue EOFError
|
||||
return nil
|
||||
end
|
||||
|
||||
def self.has_bom?(path)
|
||||
!bom(path).nil?
|
||||
end
|
||||
|
||||
def self.encoding_for_path(path)
|
||||
bom(path) || 'UTF-8'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,15 +1,6 @@
|
|||
require 'twine/formatters/abstract'
|
||||
require 'twine/formatters/android'
|
||||
require 'twine/formatters/apple'
|
||||
require 'twine/formatters/flash'
|
||||
require 'twine/formatters/gettext'
|
||||
require 'twine/formatters/jquery'
|
||||
require 'twine/formatters/django'
|
||||
require 'twine/formatters/tizen'
|
||||
|
||||
module Twine
|
||||
module Formatters
|
||||
@formatters = [Formatters::Apple, Formatters::Android, Formatters::Gettext, Formatters::JQuery, Formatters::Flash, Formatters::Django, Formatters::Tizen]
|
||||
@formatters = []
|
||||
|
||||
class << self
|
||||
attr_reader :formatters
|
||||
|
@ -22,9 +13,8 @@ module Twine
|
|||
# returns array of active formatters
|
||||
#
|
||||
def register_formatter formatter_class
|
||||
raise "#{formatter_class} already registered" if @formatters.include? formatter_class
|
||||
@formatters << formatter_class
|
||||
@formatters << formatter_class.new
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,145 +1,162 @@
|
|||
require 'fileutils'
|
||||
|
||||
module Twine
|
||||
module Formatters
|
||||
class Abstract
|
||||
attr_accessor :strings
|
||||
attr_accessor :twine_file
|
||||
attr_accessor :options
|
||||
|
||||
def self.can_handle_directory?(path)
|
||||
return false
|
||||
def initialize
|
||||
@twine_file = TwineFile.new
|
||||
@options = {}
|
||||
end
|
||||
|
||||
def initialize(strings, options)
|
||||
@strings = strings
|
||||
@options = options
|
||||
def format_name
|
||||
raise NotImplementedError.new("You must implement format_name in your formatter class.")
|
||||
end
|
||||
|
||||
def iosify_substitutions(str)
|
||||
# use "@" instead of "s" for substituting strings
|
||||
str.gsub!(/%([0-9\$]*)s/, '%\1@')
|
||||
return str
|
||||
def extension
|
||||
raise NotImplementedError.new("You must implement extension in your formatter class.")
|
||||
end
|
||||
|
||||
def androidify_substitutions(str)
|
||||
# 1) use "s" instead of "@" for substituting strings
|
||||
str.gsub!(/%([0-9\$]*)@/, '%\1s')
|
||||
|
||||
# 1a) escape strings that begin with a lone "@"
|
||||
str.sub!(/^@ /, '\\@ ')
|
||||
|
||||
# 2) if there is more than one substitution in a string, make sure they are numbered
|
||||
substituteCount = 0
|
||||
startFound = false
|
||||
str.each_char do |c|
|
||||
if startFound
|
||||
if c == "%"
|
||||
# ignore as this is a literal %
|
||||
elsif c.match(/\d/)
|
||||
# leave the string alone if it already has numbered substitutions
|
||||
return str
|
||||
else
|
||||
substituteCount += 1
|
||||
end
|
||||
startFound = false
|
||||
elsif c == "%"
|
||||
startFound = true
|
||||
end
|
||||
end
|
||||
|
||||
if substituteCount > 1
|
||||
currentSub = 1
|
||||
startFound = false
|
||||
newstr = ""
|
||||
str.each_char do |c|
|
||||
if startFound
|
||||
if !(c == "%")
|
||||
newstr = newstr + "#{currentSub}$"
|
||||
currentSub += 1
|
||||
end
|
||||
startFound = false
|
||||
elsif c == "%"
|
||||
startFound = true
|
||||
end
|
||||
newstr = newstr + c
|
||||
end
|
||||
return newstr
|
||||
else
|
||||
return str
|
||||
end
|
||||
end
|
||||
|
||||
def set_translation_for_key(key, lang, value)
|
||||
if @strings.strings_map.include?(key)
|
||||
@strings.strings_map[key].translations[lang] = value
|
||||
elsif @options[:consume_all]
|
||||
STDERR.puts "Adding new string '#{key}' to strings data file."
|
||||
arr = @strings.sections.select { |s| s.name == 'Uncategorized' }
|
||||
current_section = arr ? arr[0] : nil
|
||||
if !current_section
|
||||
current_section = StringsSection.new('Uncategorized')
|
||||
@strings.sections.insert(0, current_section)
|
||||
end
|
||||
current_row = StringsRow.new(key)
|
||||
current_section.rows << current_row
|
||||
|
||||
if @options[:tags] && @options[:tags].length > 0
|
||||
current_row.tags = @options[:tags]
|
||||
end
|
||||
|
||||
@strings.strings_map[key] = current_row
|
||||
@strings.strings_map[key].translations[lang] = value
|
||||
else
|
||||
STDERR.puts "Warning: '#{key}' not found in strings data file."
|
||||
end
|
||||
if !@strings.language_codes.include?(lang)
|
||||
@strings.add_language_code(lang)
|
||||
end
|
||||
end
|
||||
|
||||
def set_comment_for_key(key, comment)
|
||||
if @strings.strings_map.include?(key)
|
||||
@strings.strings_map[key].comment = comment
|
||||
end
|
||||
def can_handle_directory?(path)
|
||||
Dir.entries(path).any? { |item| /^.+#{Regexp.escape(extension)}$/.match(item) }
|
||||
end
|
||||
|
||||
def default_file_name
|
||||
raise NotImplementedError.new("You must implement default_file_name in your formatter class.")
|
||||
end
|
||||
|
||||
def set_translation_for_key(key, lang, value)
|
||||
value = value.gsub("\n", "\\n")
|
||||
|
||||
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]
|
||||
definition.translations[lang] = value
|
||||
end
|
||||
elsif @options[:consume_all]
|
||||
Twine::stderr.puts "Adding new definition '#{key}' to twine file."
|
||||
current_section = @twine_file.sections.find { |s| s.name == 'Uncategorized' }
|
||||
unless current_section
|
||||
current_section = TwineSection.new('Uncategorized')
|
||||
@twine_file.sections.insert(0, current_section)
|
||||
end
|
||||
current_definition = TwineDefinition.new(key)
|
||||
current_section.definitions << current_definition
|
||||
|
||||
if @options[:tags] && @options[:tags].length > 0
|
||||
current_definition.tags = @options[:tags]
|
||||
end
|
||||
|
||||
@twine_file.definitions_by_key[key] = current_definition
|
||||
@twine_file.definitions_by_key[key].translations[lang] = value
|
||||
else
|
||||
Twine::stderr.puts "Warning: '#{key}' not found in twine file."
|
||||
end
|
||||
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 @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 comment != reference.raw_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.")
|
||||
end
|
||||
|
||||
def read_file(path, lang)
|
||||
raise NotImplementedError.new("You must implement read_file in your formatter class.")
|
||||
def output_path_for_language(lang)
|
||||
lang
|
||||
end
|
||||
|
||||
def write_file(path, lang)
|
||||
raise NotImplementedError.new("You must implement write_file in your formatter class.")
|
||||
def read(io, lang)
|
||||
raise NotImplementedError.new("You must implement read in your formatter class.")
|
||||
end
|
||||
|
||||
def write_all_files(path)
|
||||
if !File.directory?(path)
|
||||
raise Twine::Error.new("Directory does not exist: #{path}")
|
||||
def format_file(lang)
|
||||
output_processor = Processors::OutputProcessor.new(@twine_file, @options)
|
||||
processed_twine_file = output_processor.process(lang)
|
||||
|
||||
return nil if processed_twine_file.definitions_by_key.empty?
|
||||
|
||||
header = format_header(lang)
|
||||
result = ""
|
||||
result += header + "\n" if header
|
||||
result += format_sections(processed_twine_file, lang)
|
||||
end
|
||||
|
||||
def format_header(lang)
|
||||
end
|
||||
|
||||
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_definition(definition, lang)
|
||||
return !definition.translation_for_lang(lang).nil?
|
||||
end
|
||||
|
||||
def format_section(section, lang)
|
||||
definitions = section.definitions.select { |definition| should_include_definition(definition, lang) }
|
||||
return if definitions.empty?
|
||||
|
||||
result = ""
|
||||
|
||||
if section.name && section.name.length > 0
|
||||
section_header = format_section_header(section)
|
||||
result += "\n#{section_header}" if section_header
|
||||
end
|
||||
|
||||
file_name = @options[:file_name] || default_file_name
|
||||
langs_written = []
|
||||
Dir.foreach(path) do |item|
|
||||
if item == "." or item == ".."
|
||||
next
|
||||
end
|
||||
item = File.join(path, item)
|
||||
if File.directory?(item)
|
||||
lang = determine_language_given_path(item)
|
||||
if lang
|
||||
write_file(File.join(item, file_name), lang)
|
||||
langs_written << lang
|
||||
end
|
||||
end
|
||||
end
|
||||
if langs_written.empty?
|
||||
raise Twine::Error.new("Failed to generate any files: No languages found at #{path}")
|
||||
end
|
||||
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_definition(definition, lang)
|
||||
[format_comment(definition, lang), format_key_value(definition, lang)].compact.join
|
||||
end
|
||||
|
||||
def format_comment(definition, lang)
|
||||
end
|
||||
|
||||
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 key_value_pattern
|
||||
raise NotImplementedError.new("You must implement key_value_pattern in your formatter class.")
|
||||
end
|
||||
|
||||
def format_key(key)
|
||||
key
|
||||
end
|
||||
|
||||
def format_value(value)
|
||||
value
|
||||
end
|
||||
|
||||
def escape_quotes(text)
|
||||
text.gsub('"', '\\\\"')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,41 +5,45 @@ require 'rexml/document'
|
|||
module Twine
|
||||
module Formatters
|
||||
class Android < Abstract
|
||||
FORMAT_NAME = 'android'
|
||||
EXTENSION = '.xml'
|
||||
DEFAULT_FILE_NAME = 'strings.xml'
|
||||
include Twine::Placeholders
|
||||
|
||||
LANG_CODES = Hash[
|
||||
'zh' => 'zh-Hans',
|
||||
'zh-rCN' => 'zh-Hans',
|
||||
'zh-rHK' => 'zh-Hant',
|
||||
'en-rGB' => 'en-GB',
|
||||
'zh-CN' => 'zh-Hans',
|
||||
'zh-HK' => 'zh-Hant',
|
||||
'en-GB' => 'en-GB',
|
||||
'in' => 'id'
|
||||
# TODO: spanish
|
||||
]
|
||||
DEFAULT_LANG_CODES = Hash[
|
||||
'zh-TW' => 'zh-Hant' # if we don't have a zh-TW translation, try zh-Hant before en
|
||||
]
|
||||
|
||||
def self.can_handle_directory?(path)
|
||||
def format_name
|
||||
'android'
|
||||
end
|
||||
|
||||
def extension
|
||||
'.xml'
|
||||
end
|
||||
|
||||
def can_handle_directory?(path)
|
||||
Dir.entries(path).any? { |item| /^values.*$/.match(item) }
|
||||
end
|
||||
|
||||
def default_file_name
|
||||
return DEFAULT_FILE_NAME
|
||||
'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
|
||||
match = /^values-(.*)$/.match(segment)
|
||||
# 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_CODES.fetch(lang, lang)
|
||||
lang.sub!('-r', '-')
|
||||
return lang
|
||||
lang = match[1].sub('-r', '-')
|
||||
return LANG_CODES.fetch(lang, lang)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -47,108 +51,108 @@ module Twine
|
|||
return
|
||||
end
|
||||
|
||||
def read_file(path, lang)
|
||||
resources_regex = /<resources(?:[^>]*)>(.*)<\/resources>/m
|
||||
key_regex = /<string name="(\w+)">/
|
||||
comment_regex = /<!-- (.*) -->/
|
||||
value_regex = /<string name="\w+">(.*)<\/string>/
|
||||
key = nil
|
||||
value = nil
|
||||
def output_path_for_language(lang)
|
||||
"values-#{lang}"
|
||||
end
|
||||
|
||||
def set_translation_for_key(key, lang, value)
|
||||
value = CGI.unescapeHTML(value)
|
||||
value.gsub!('\\\'', '\'')
|
||||
value.gsub!('\\"', '"')
|
||||
value = convert_placeholders_from_android_to_twine(value)
|
||||
value.gsub!('\@', '@')
|
||||
value.gsub!(/(\\u0020)*|(\\u0020)*\z/) { |spaces| ' ' * (spaces.length / 6) }
|
||||
super(key, lang, value)
|
||||
end
|
||||
|
||||
def read(io, lang)
|
||||
document = REXML::Document.new io, :compress_whitespace => %w{ string }
|
||||
|
||||
comment = nil
|
||||
document.root.children.each do |child|
|
||||
if child.is_a? REXML::Comment
|
||||
content = child.string.strip
|
||||
comment = content if content.length > 0 and not content.start_with?("SECTION:")
|
||||
elsif child.is_a? REXML::Element
|
||||
next unless child.name == 'string'
|
||||
|
||||
File.open(path, 'r:UTF-8') do |f|
|
||||
content_match = resources_regex.match(f.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)
|
||||
if value_match
|
||||
value = value_match[1]
|
||||
value = CGI.unescapeHTML(value)
|
||||
value.gsub!('\\\'', '\'')
|
||||
value.gsub!('\\"', '"')
|
||||
value = iosify_substitutions(value)
|
||||
value.gsub!(/(\\u0020)*|(\\u0020)*\z/) { |spaces| ' ' * (spaces.length / 6) }
|
||||
else
|
||||
value = ""
|
||||
end
|
||||
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
|
||||
key = child.attributes['name']
|
||||
|
||||
comment_match = comment_regex.match(line)
|
||||
if comment_match
|
||||
comment = comment_match[1]
|
||||
end
|
||||
end
|
||||
end
|
||||
set_translation_for_key(key, lang, child.text)
|
||||
set_comment_for_key(key, comment) if comment
|
||||
|
||||
comment = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def write_file(path, lang)
|
||||
default_lang = nil
|
||||
if DEFAULT_LANG_CODES.has_key?(lang)
|
||||
default_lang = DEFAULT_LANG_CODES[lang]
|
||||
end
|
||||
File.open(path, 'w:UTF-8') do |f|
|
||||
f.puts "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Android Strings File -->\n<!-- Generated by Twine #{Twine::VERSION} -->\n<!-- Language: #{lang} -->"
|
||||
f.write '<resources>'
|
||||
@strings.sections.each do |section|
|
||||
printed_section = false
|
||||
section.rows.each do |row|
|
||||
if row.matches_tags?(@options[:tags], @options[:untagged])
|
||||
if !printed_section
|
||||
f.puts ''
|
||||
if section.name && section.name.length > 0
|
||||
section_name = section.name.gsub('--', '—')
|
||||
f.puts "\t<!-- SECTION: #{section_name} -->"
|
||||
end
|
||||
printed_section = true
|
||||
end
|
||||
|
||||
key = row.key
|
||||
|
||||
value = row.translated_string_for_lang(lang, default_lang)
|
||||
if !value && @options[:include_untranslated]
|
||||
value = row.translated_string_for_lang(@strings.language_codes[0])
|
||||
end
|
||||
|
||||
if value # if values is nil, there was no appropriate translation, so let Android handle the defaulting
|
||||
value = String.new(value) # use a copy to prevent modifying the original
|
||||
|
||||
# Android enforces the following rules on the values
|
||||
# 1) apostrophes and quotes must be escaped with a backslash
|
||||
value.gsub!('\'', '\\\\\'')
|
||||
value.gsub!('"', '\\\\"')
|
||||
# 2) HTML escape the string
|
||||
value = CGI.escapeHTML(value)
|
||||
# 3) fix substitutions (e.g. %s/%@)
|
||||
value = androidify_substitutions(value)
|
||||
# 4) replace beginning and end spaces with \0020. Otherwise Android strips them.
|
||||
value.gsub!(/\A *| *\z/) { |spaces| '\u0020' * spaces.length }
|
||||
|
||||
comment = row.comment
|
||||
if comment
|
||||
comment = comment.gsub('--', '—')
|
||||
end
|
||||
|
||||
if comment && comment.length > 0
|
||||
f.puts "\t<!-- #{comment} -->\n"
|
||||
end
|
||||
f.puts "\t<string name=\"#{key}\">#{value}</string>"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
f.puts '</resources>'
|
||||
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} -->"
|
||||
end
|
||||
|
||||
def format_sections(twine_file, lang)
|
||||
result = '<resources>'
|
||||
|
||||
result += super + "\n"
|
||||
|
||||
result += "</resources>\n"
|
||||
end
|
||||
|
||||
def format_section_header(section)
|
||||
"\t<!-- SECTION: #{section.name} -->"
|
||||
end
|
||||
|
||||
def format_comment(definition, lang)
|
||||
"\t<!-- #{definition.comment.gsub('--', '—')} -->\n" if definition.comment
|
||||
end
|
||||
|
||||
def key_value_pattern
|
||||
"\t<string name=\"%{key}\">%{value}</string>"
|
||||
end
|
||||
|
||||
def escape_value(value)
|
||||
# escape double and single quotes, & signs and tags
|
||||
value = escape_quotes(value)
|
||||
value.gsub!("'", "\\\\'")
|
||||
value.gsub!(/&/, '&')
|
||||
value.gsub!('<', '<')
|
||||
|
||||
# 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, '\@')
|
||||
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
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Twine::Formatters.formatters << Twine::Formatters::Android.new
|
||||
|
|
|
@ -1,16 +1,20 @@
|
|||
module Twine
|
||||
module Formatters
|
||||
class Apple < Abstract
|
||||
FORMAT_NAME = 'apple'
|
||||
EXTENSION = '.strings'
|
||||
DEFAULT_FILE_NAME = 'Localizable.strings'
|
||||
def format_name
|
||||
'apple'
|
||||
end
|
||||
|
||||
def self.can_handle_directory?(path)
|
||||
def extension
|
||||
'.strings'
|
||||
end
|
||||
|
||||
def can_handle_directory?(path)
|
||||
Dir.entries(path).any? { |item| /^.+\.lproj$/.match(item) }
|
||||
end
|
||||
|
||||
def default_file_name
|
||||
return DEFAULT_FILE_NAME
|
||||
'Localizable.strings'
|
||||
end
|
||||
|
||||
def determine_language_given_path(path)
|
||||
|
@ -18,108 +22,70 @@ module Twine
|
|||
path_arr.each do |segment|
|
||||
match = /^(.+)\.lproj$/.match(segment)
|
||||
if match
|
||||
return match[1]
|
||||
if match[1] != "Base"
|
||||
return match[1]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
def read_file(path, lang)
|
||||
encoding = Twine::Encoding.encoding_for_path(path)
|
||||
sep = nil
|
||||
if !encoding.respond_to?(:encode)
|
||||
# This code is not necessary in 1.9.3 and does not work as it did in 1.8.7.
|
||||
if encoding.end_with? 'LE'
|
||||
sep = "\x0a\x00"
|
||||
elsif encoding.end_with? 'BE'
|
||||
sep = "\x00\x0a"
|
||||
else
|
||||
sep = "\n"
|
||||
def output_path_for_language(lang)
|
||||
"#{lang}.lproj"
|
||||
end
|
||||
|
||||
def read(io, lang)
|
||||
last_comment = nil
|
||||
while line = io.gets
|
||||
# matches a `key = "value"` line, where key may be quoted or unquoted. The former may also contain escaped characters
|
||||
match = /^\s*((?:"(?:[^"\\]|\\.)+")|(?:[^"\s=]+))\s*=\s*"((?:[^"\\]|\\.)*)"/.match(line)
|
||||
if match
|
||||
key = match[1]
|
||||
key = key[1..-2] if key[0] == '"' and key[-1] == '"'
|
||||
key.gsub!('\\"', '"')
|
||||
value = match[2]
|
||||
value.gsub!('\\"', '"')
|
||||
set_translation_for_key(key, lang, value)
|
||||
if last_comment
|
||||
set_comment_for_key(key, last_comment)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if encoding.index('UTF-16')
|
||||
mode = "rb:#{encoding}"
|
||||
else
|
||||
mode = "r:#{encoding}"
|
||||
end
|
||||
|
||||
File.open(path, mode) do |f|
|
||||
last_comment = nil
|
||||
while line = (sep) ? f.gets(sep) : f.gets
|
||||
if encoding.index('UTF-16')
|
||||
if line.respond_to? :encode!
|
||||
line.encode!('UTF-8')
|
||||
else
|
||||
require 'iconv'
|
||||
line = Iconv.iconv('UTF-8', encoding, line).join
|
||||
end
|
||||
end
|
||||
match = /"((?:[^"\\]|\\.)+)"\s*=\s*"((?:[^"\\]|\\.)*)"/.match(line)
|
||||
if match
|
||||
key = match[1]
|
||||
key.gsub!('\\"', '"')
|
||||
value = match[2]
|
||||
value.gsub!('\\"', '"')
|
||||
value = iosify_substitutions(value)
|
||||
set_translation_for_key(key, lang, value)
|
||||
if last_comment
|
||||
set_comment_for_key(key, last_comment)
|
||||
end
|
||||
end
|
||||
if @options[:consume_comments]
|
||||
match = /\/\* (.*) \*\//.match(line)
|
||||
if match
|
||||
last_comment = match[1]
|
||||
else
|
||||
last_comment = nil
|
||||
end
|
||||
end
|
||||
match = /\/\* (.*) \*\//.match(line)
|
||||
if match
|
||||
last_comment = match[1]
|
||||
else
|
||||
last_comment = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def write_file(path, lang)
|
||||
default_lang = @strings.language_codes[0]
|
||||
encoding = @options[:output_encoding] || 'UTF-8'
|
||||
File.open(path, "w:#{encoding}") do |f|
|
||||
f.puts "/**\n * Apple Strings File\n * Generated by Twine #{Twine::VERSION}\n * Language: #{lang}\n */"
|
||||
@strings.sections.each do |section|
|
||||
printed_section = false
|
||||
section.rows.each do |row|
|
||||
if row.matches_tags?(@options[:tags], @options[:untagged])
|
||||
f.puts ''
|
||||
if !printed_section
|
||||
if section.name && section.name.length > 0
|
||||
f.print "/********** #{section.name} **********/\n\n"
|
||||
end
|
||||
printed_section = true
|
||||
end
|
||||
def format_header(lang)
|
||||
"/**\n * Apple Strings File\n * Generated by Twine #{Twine::VERSION}\n * Language: #{lang}\n */"
|
||||
end
|
||||
|
||||
key = row.key
|
||||
key = key.gsub('"', '\\\\"')
|
||||
def format_section_header(section)
|
||||
"/********** #{section.name} **********/\n"
|
||||
end
|
||||
|
||||
value = row.translated_string_for_lang(lang, default_lang)
|
||||
if value
|
||||
value = value.gsub('"', '\\\\"')
|
||||
def key_value_pattern
|
||||
"\"%{key}\" = \"%{value}\";\n"
|
||||
end
|
||||
|
||||
comment = row.comment
|
||||
if comment
|
||||
comment = comment.gsub('*/', '* /')
|
||||
end
|
||||
def format_comment(definition, lang)
|
||||
"/* #{definition.comment.gsub('*/', '* /')} */\n" if definition.comment
|
||||
end
|
||||
|
||||
if comment && comment.length > 0
|
||||
f.print "/* #{comment} */\n"
|
||||
end
|
||||
def format_key(key)
|
||||
escape_quotes(key)
|
||||
end
|
||||
|
||||
f.print "\"#{key}\" = \"#{value}\";\n"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
def format_value(value)
|
||||
escape_quotes(value)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Twine::Formatters.formatters << Twine::Formatters::Apple.new
|
||||
|
|
|
@ -1,143 +1,103 @@
|
|||
module Twine
|
||||
module Formatters
|
||||
class Django < Abstract
|
||||
FORMAT_NAME = 'django'
|
||||
EXTENSION = '.po'
|
||||
DEFAULT_FILE_NAME = 'strings.po'
|
||||
def format_name
|
||||
'django'
|
||||
end
|
||||
|
||||
def self.can_handle_directory?(path)
|
||||
Dir.entries(path).any? { |item| /^.+\.po$/.match(item) }
|
||||
def extension
|
||||
'.po'
|
||||
end
|
||||
|
||||
def default_file_name
|
||||
return DEFAULT_FILE_NAME
|
||||
'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
|
||||
|
||||
path_arr = path.split(File::SEPARATOR)
|
||||
path_arr.each do |segment|
|
||||
match = /(..)\.po$/.match(segment)
|
||||
return match[1] if match
|
||||
end
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
def read_file(path, lang)
|
||||
comment_regex = /#.? *"(.*)"$/
|
||||
def read(io, lang)
|
||||
comment_regex = /#\. *"?(.*)"?$/
|
||||
key_regex = /msgid *"(.*)"$/
|
||||
value_regex = /msgstr *"(.*)"$/m
|
||||
|
||||
encoding = Twine::Encoding.encoding_for_path(path)
|
||||
sep = nil
|
||||
if !encoding.respond_to?(:encode)
|
||||
# This code is not necessary in 1.9.3 and does not work as it did in 1.8.7.
|
||||
if encoding.end_with? 'LE'
|
||||
sep = "\x0a\x00"
|
||||
elsif encoding.end_with? 'BE'
|
||||
sep = "\x00\x0a"
|
||||
else
|
||||
sep = "\n"
|
||||
last_comment = nil
|
||||
while line = io.gets
|
||||
comment_match = comment_regex.match(line)
|
||||
if comment_match
|
||||
comment = comment_match[1]
|
||||
end
|
||||
end
|
||||
|
||||
if encoding.index('UTF-16')
|
||||
mode = "rb:#{encoding}"
|
||||
else
|
||||
mode = "r:#{encoding}"
|
||||
end
|
||||
key_match = key_regex.match(line)
|
||||
if key_match
|
||||
key = key_match[1].gsub('\\"', '"')
|
||||
end
|
||||
value_match = value_regex.match(line)
|
||||
if value_match
|
||||
value = value_match[1].gsub(/"\n"/, '').gsub('\\"', '"')
|
||||
end
|
||||
|
||||
File.open(path, mode) do |f|
|
||||
last_comment = nil
|
||||
while line = (sep) ? f.gets(sep) : f.gets
|
||||
if encoding.index('UTF-16')
|
||||
if line.respond_to? :encode!
|
||||
line.encode!('UTF-8')
|
||||
else
|
||||
require 'iconv'
|
||||
line = Iconv.iconv('UTF-8', encoding, line).join
|
||||
end
|
||||
if key and key.length > 0 and value and value.length > 0
|
||||
set_translation_for_key(key, lang, value)
|
||||
if comment and comment.length > 0 and !comment.start_with?("--------- ")
|
||||
set_comment_for_key(key, comment)
|
||||
end
|
||||
if @options[:consume_comments]
|
||||
comment_match = comment_regex.match(line)
|
||||
if comment_match
|
||||
comment = comment_match[1]
|
||||
end
|
||||
else
|
||||
comment = nil
|
||||
end
|
||||
key_match = key_regex.match(line)
|
||||
if key_match
|
||||
key = key_match[1].gsub('\\"', '"')
|
||||
end
|
||||
value_match = value_regex.match(line)
|
||||
if value_match
|
||||
value = value_match[1].gsub(/"\n"/, '').gsub('\\"', '"')
|
||||
end
|
||||
|
||||
|
||||
if key and key.length > 0 and value and value.length > 0
|
||||
set_translation_for_key(key, lang, value)
|
||||
if comment and comment.length > 0 and !comment.start_with?("--------- ")
|
||||
set_comment_for_key(key, comment)
|
||||
end
|
||||
comment = nil
|
||||
end
|
||||
|
||||
key = nil
|
||||
value = nil
|
||||
comment = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def write_file(path, lang)
|
||||
default_lang = @strings.language_codes[0]
|
||||
encoding = @options[:output_encoding] || 'UTF-8'
|
||||
File.open(path, "w:#{encoding}") do |f|
|
||||
f.puts "##\n # Django Strings File\n # Generated by Twine #{Twine::VERSION}\n # Language: #{lang}\n "
|
||||
@strings.sections.each do |section|
|
||||
printed_section = false
|
||||
section.rows.each do |row|
|
||||
if row.matches_tags?(@options[:tags], @options[:untagged])
|
||||
f.puts ''
|
||||
if !printed_section
|
||||
if section.name && section.name.length > 0
|
||||
f.print "#--------- #{section.name} ---------#\n\n"
|
||||
end
|
||||
printed_section = true
|
||||
end
|
||||
|
||||
basetrans = row.translated_string_for_lang(default_lang)
|
||||
def format_file(lang)
|
||||
@default_lang = @twine_file.language_codes[0]
|
||||
result = super
|
||||
@default_lang = nil
|
||||
result
|
||||
end
|
||||
|
||||
key = row.key
|
||||
key = key.gsub('"', '\\\\"')
|
||||
def format_header(lang)
|
||||
"##\n # Django Strings File\n # Generated by Twine #{Twine::VERSION}\n # Language: #{lang}\nmsgid \"\"\nmsgstr \"\"\n\"Content-Type: text/plain; charset=UTF-8\\n\""
|
||||
end
|
||||
|
||||
value = row.translated_string_for_lang(lang, default_lang)
|
||||
if value
|
||||
value = value.gsub('"', '\\\\"')
|
||||
def format_section_header(section)
|
||||
"#--------- #{section.name} ---------#\n"
|
||||
end
|
||||
|
||||
comment = row.comment
|
||||
def format_definition(definition, lang)
|
||||
[format_comment(definition, lang), format_base_translation(definition), format_key_value(definition, lang)].compact.join
|
||||
end
|
||||
|
||||
if comment
|
||||
comment = comment.gsub('"', '\\\\"')
|
||||
end
|
||||
def format_base_translation(definition)
|
||||
base_translation = definition.translations[@default_lang]
|
||||
"# base translation: \"#{base_translation}\"\n" if base_translation
|
||||
end
|
||||
|
||||
if comment && comment.length > 0
|
||||
f.print "#. #{comment} \n"
|
||||
end
|
||||
def key_value_pattern
|
||||
"msgid \"%{key}\"\n" +
|
||||
"msgstr \"%{value}\"\n"
|
||||
end
|
||||
|
||||
if basetrans && basetrans.length > 0
|
||||
f.print "# base translation: \"#{basetrans}\"\n"
|
||||
end
|
||||
def format_comment(definition, lang)
|
||||
"#. #{escape_quotes(definition.comment)}\n" if definition.comment
|
||||
end
|
||||
|
||||
f.print "msgid \"#{key}\"\n"
|
||||
f.print "msgstr \"#{value}\"\n"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
def format_key(key)
|
||||
escape_quotes(key)
|
||||
end
|
||||
|
||||
def format_value(value)
|
||||
escape_quotes(value)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Twine::Formatters.formatters << Twine::Formatters::Django.new
|
||||
|
|
|
@ -1,110 +1,72 @@
|
|||
module Twine
|
||||
module Formatters
|
||||
class Flash < Abstract
|
||||
FORMAT_NAME = 'flash'
|
||||
EXTENSION = '.properties'
|
||||
DEFAULT_FILE_NAME = 'resources.properties'
|
||||
include Twine::Placeholders
|
||||
|
||||
def self.can_handle_directory?(path)
|
||||
return false
|
||||
def format_name
|
||||
'flash'
|
||||
end
|
||||
|
||||
def extension
|
||||
'.properties'
|
||||
end
|
||||
|
||||
def default_file_name
|
||||
return DEFAULT_FILE_NAME
|
||||
'resources.properties'
|
||||
end
|
||||
|
||||
def determine_language_given_path(path)
|
||||
return
|
||||
# match two-letter language code, optionally followed by a two letter region code
|
||||
path.split(File::SEPARATOR).reverse.find { |segment| segment =~ /^([a-z]{2}(-[a-z]{2})?)$/i }
|
||||
end
|
||||
|
||||
def read_file(path, lang)
|
||||
encoding = Twine::Encoding.encoding_for_path(path)
|
||||
sep = nil
|
||||
if !encoding.respond_to?(:encode)
|
||||
# This code is not necessary in 1.9.3 and does not work as it did in 1.8.7.
|
||||
if encoding.end_with? 'LE'
|
||||
sep = "\x0a\x00"
|
||||
elsif encoding.end_with? 'BE'
|
||||
sep = "\x00\x0a"
|
||||
else
|
||||
sep = "\n"
|
||||
end
|
||||
end
|
||||
def set_translation_for_key(key, lang, value)
|
||||
value = convert_placeholders_from_flash_to_twine(value)
|
||||
super(key, lang, value)
|
||||
end
|
||||
|
||||
if encoding.index('UTF-16')
|
||||
mode = "rb:#{encoding}"
|
||||
else
|
||||
mode = "r:#{encoding}"
|
||||
end
|
||||
def read(io, lang)
|
||||
last_comment = nil
|
||||
while line = io.gets
|
||||
match = /((?:[^"\\]|\\.)+)\s*=\s*((?:[^"\\]|\\.)*)/.match(line)
|
||||
if match
|
||||
key = match[1]
|
||||
value = match[2].strip
|
||||
|
||||
File.open(path, mode) do |f|
|
||||
last_comment = nil
|
||||
while line = (sep) ? f.gets(sep) : f.gets
|
||||
if encoding.index('UTF-16')
|
||||
if line.respond_to? :encode!
|
||||
line.encode!('UTF-8')
|
||||
else
|
||||
require 'iconv'
|
||||
line = Iconv.iconv('UTF-8', encoding, line).join
|
||||
end
|
||||
end
|
||||
match = /((?:[^"\\]|\\.)+)\s*=\s*((?:[^"\\]|\\.)*)/.match(line)
|
||||
if match
|
||||
key = match[1]
|
||||
value = match[2]
|
||||
value.gsub!(/\{[0-9]\}/, '%@')
|
||||
set_translation_for_key(key, lang, value)
|
||||
if last_comment
|
||||
set_comment_for_key(key, last_comment)
|
||||
end
|
||||
end
|
||||
if @options[:consume_comments]
|
||||
match = /#(.*)/.match(line)
|
||||
if match
|
||||
last_comment = match[1]
|
||||
else
|
||||
last_comment = nil
|
||||
end
|
||||
end
|
||||
set_translation_for_key(key, lang, value)
|
||||
set_comment_for_key(key, last_comment) if last_comment
|
||||
end
|
||||
|
||||
match = /# *(.*)/.match(line)
|
||||
last_comment = match ? match[1] : nil
|
||||
end
|
||||
end
|
||||
|
||||
def write_file(path, lang)
|
||||
default_lang = @strings.language_codes[0]
|
||||
encoding = @options[:output_encoding] || 'UTF-8'
|
||||
File.open(path, "w:#{encoding}") do |f|
|
||||
f.puts "## Flash Strings File\n## Generated by Twine #{Twine::VERSION}\n## Language: #{lang}\n"
|
||||
@strings.sections.each do |section|
|
||||
printed_section = false
|
||||
section.rows.each do |row|
|
||||
if row.matches_tags?(@options[:tags], @options[:untagged])
|
||||
f.puts ''
|
||||
if !printed_section
|
||||
if section.name && section.name.length > 0
|
||||
f.print "## #{section.name} ##\n\n"
|
||||
end
|
||||
printed_section = true
|
||||
end
|
||||
def format_sections(twine_file, lang)
|
||||
super + "\n"
|
||||
end
|
||||
|
||||
key = row.key
|
||||
value = row.translated_string_for_lang(lang, default_lang)
|
||||
if value
|
||||
placeHolderNumber = -1
|
||||
value = value.gsub(/%[d@]/) { placeHolderNumber += 1; '{%d}' % placeHolderNumber }
|
||||
|
||||
comment = row.comment
|
||||
if comment && comment.length > 0
|
||||
f.print "# #{comment}\n"
|
||||
end
|
||||
def format_header(lang)
|
||||
"## Flash Strings File\n## Generated by Twine #{Twine::VERSION}\n## Language: #{lang}"
|
||||
end
|
||||
|
||||
f.print "#{key}=#{value}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
def format_section_header(section)
|
||||
"## #{section.name} ##\n"
|
||||
end
|
||||
|
||||
def format_comment(definition, lang)
|
||||
"# #{definition.comment}\n" if definition.comment
|
||||
end
|
||||
|
||||
def key_value_pattern
|
||||
"%{key}=%{value}"
|
||||
end
|
||||
|
||||
def format_value(value)
|
||||
convert_placeholders_from_twine_to_flash(value)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Twine::Formatters.formatters << Twine::Formatters::Flash.new
|
||||
|
|
|
@ -3,16 +3,16 @@
|
|||
module Twine
|
||||
module Formatters
|
||||
class Gettext < Abstract
|
||||
FORMAT_NAME = 'gettext'
|
||||
EXTENSION = '.po'
|
||||
DEFAULT_FILE_NAME = 'strings.po'
|
||||
def format_name
|
||||
'gettext'
|
||||
end
|
||||
|
||||
def self.can_handle_directory?(path)
|
||||
Dir.entries(path).any? { |item| /^.+\.po$/.match(item) }
|
||||
def extension
|
||||
'.po'
|
||||
end
|
||||
|
||||
def default_file_name
|
||||
return DEFAULT_FILE_NAME
|
||||
'strings.po'
|
||||
end
|
||||
|
||||
def determine_language_given_path(path)
|
||||
|
@ -27,84 +27,79 @@ module Twine
|
|||
return
|
||||
end
|
||||
|
||||
def read_file(path, lang)
|
||||
def read(io, lang)
|
||||
comment_regex = /#.? *"(.*)"$/
|
||||
key_regex = /msgctxt *"(.*)"$/
|
||||
value_regex = /msgstr *"(.*)"$/m
|
||||
File.open(path, 'r:UTF-8') do |f|
|
||||
while item = f.gets("\n\n")
|
||||
key = nil
|
||||
value = nil
|
||||
comment = nil
|
||||
|
||||
while item = io.gets("\n\n")
|
||||
key = nil
|
||||
value = nil
|
||||
comment = nil
|
||||
|
||||
comment_match = comment_regex.match(item)
|
||||
if comment_match
|
||||
comment = comment_match[1]
|
||||
end
|
||||
key_match = key_regex.match(item)
|
||||
if key_match
|
||||
key = key_match[1].gsub('\\"', '"')
|
||||
end
|
||||
value_match = value_regex.match(item)
|
||||
if value_match
|
||||
value = value_match[1].gsub(/"\n"/, '').gsub('\\"', '"')
|
||||
end
|
||||
if key and key.length > 0 and value and value.length > 0
|
||||
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
|
||||
comment_match = comment_regex.match(item)
|
||||
if comment_match
|
||||
comment = comment_match[1]
|
||||
end
|
||||
key_match = key_regex.match(item)
|
||||
if key_match
|
||||
key = key_match[1].gsub('\\"', '"')
|
||||
end
|
||||
value_match = value_regex.match(item)
|
||||
if value_match
|
||||
value = value_match[1].gsub(/"\n"/, '').gsub('\\"', '"')
|
||||
end
|
||||
if key and key.length > 0 and value and value.length > 0
|
||||
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
|
||||
end
|
||||
end
|
||||
|
||||
def write_file(path, lang)
|
||||
default_lang = @strings.language_codes[0]
|
||||
encoding = @options[:output_encoding] || 'UTF-8'
|
||||
File.open(path, "w:#{encoding}") do |f|
|
||||
f.puts "msgid \"\"\nmsgstr \"\"\n\"Language: #{lang}\\n\"\n\"X-Generator: Twine #{Twine::VERSION}\\n\"\n\n"
|
||||
@strings.sections.each do |section|
|
||||
printed_section = false
|
||||
section.rows.each do |row|
|
||||
if row.matches_tags?(@options[:tags], @options[:untagged])
|
||||
if !printed_section
|
||||
f.puts ''
|
||||
if section.name && section.name.length > 0
|
||||
section_name = section.name.gsub('--', '—')
|
||||
f.puts "# SECTION: #{section_name}"
|
||||
end
|
||||
printed_section = true
|
||||
end
|
||||
def format_file(lang)
|
||||
@default_lang = twine_file.language_codes[0]
|
||||
result = super
|
||||
@default_lang = nil
|
||||
result
|
||||
end
|
||||
|
||||
basetrans = row.translated_string_for_lang(default_lang)
|
||||
def format_header(lang)
|
||||
"msgid \"\"\nmsgstr \"\"\n\"Language: #{lang}\\n\"\n\"X-Generator: Twine #{Twine::VERSION}\\n\"\n"
|
||||
end
|
||||
|
||||
if basetrans
|
||||
key = row.key
|
||||
key = key.gsub('"', '\\\\"')
|
||||
def format_section_header(section)
|
||||
"# SECTION: #{section.name}"
|
||||
end
|
||||
|
||||
comment = row.comment
|
||||
if comment
|
||||
comment = comment.gsub('"', '\\\\"')
|
||||
end
|
||||
def should_include_definition(definition, lang)
|
||||
super and !definition.translation_for_lang(@default_lang).nil?
|
||||
end
|
||||
|
||||
if comment && comment.length > 0
|
||||
f.print "#. \"#{comment}\"\n"
|
||||
end
|
||||
def format_comment(definition, lang)
|
||||
"#. \"#{escape_quotes(definition.comment)}\"\n" if definition.comment
|
||||
end
|
||||
|
||||
f.print "msgctxt \"#{key}\"\nmsgid \"#{basetrans}\"\n"
|
||||
value = row.translated_string_for_lang(lang)
|
||||
if value
|
||||
value = value.gsub('"', '\\\\"')
|
||||
end
|
||||
f.print "msgstr \"#{value}\"\n\n"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
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"
|
||||
end
|
||||
|
||||
def format_base_translation(definition)
|
||||
"msgid \"#{definition.translations[@default_lang]}\"\n"
|
||||
end
|
||||
|
||||
def format_value(value)
|
||||
"msgstr \"#{value}\"\n"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Twine::Formatters.formatters << Twine::Formatters::Gettext.new
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
module Twine
|
||||
module Formatters
|
||||
class JQuery < Abstract
|
||||
FORMAT_NAME = 'jquery'
|
||||
EXTENSION = '.json'
|
||||
DEFAULT_FILE_NAME = 'localize.json'
|
||||
def format_name
|
||||
'jquery'
|
||||
end
|
||||
|
||||
def self.can_handle_directory?(path)
|
||||
Dir.entries(path).any? { |item| /^.+\.json$/.match(item) }
|
||||
def extension
|
||||
'.json'
|
||||
end
|
||||
|
||||
def default_file_name
|
||||
return DEFAULT_FILE_NAME
|
||||
'localize.json'
|
||||
end
|
||||
|
||||
def determine_language_given_path(path)
|
||||
|
@ -25,63 +25,55 @@ module Twine
|
|||
return
|
||||
end
|
||||
|
||||
def read_file(path, lang)
|
||||
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."
|
||||
end
|
||||
|
||||
open(path) do |io|
|
||||
json = JSON.load(io)
|
||||
json.each do |key, value|
|
||||
value.gsub!("\n","\\n")
|
||||
set_translation_for_key(key, lang, value)
|
||||
end
|
||||
json = JSON.load(io)
|
||||
json.each do |key, value|
|
||||
set_translation_for_key(key, lang, value)
|
||||
end
|
||||
end
|
||||
|
||||
def write_file(path, 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."
|
||||
end
|
||||
def format_file(lang)
|
||||
result = super
|
||||
return result unless result
|
||||
"{\n#{super}\n}\n"
|
||||
end
|
||||
|
||||
printed_string = false
|
||||
default_lang = @strings.language_codes[0]
|
||||
encoding = @options[:output_encoding] || 'UTF-8'
|
||||
File.open(path, "w:#{encoding}") do |f|
|
||||
f.print "{"
|
||||
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
|
||||
|
||||
@strings.sections.each_with_index do |section, si|
|
||||
printed_section = false
|
||||
section.rows.each_with_index do |row, ri|
|
||||
if row.matches_tags?(@options[:tags], @options[:untagged])
|
||||
if printed_string
|
||||
f.print ",\n"
|
||||
end
|
||||
def format_section_header(section)
|
||||
end
|
||||
|
||||
if !printed_section
|
||||
f.print "\n"
|
||||
printed_section = true
|
||||
end
|
||||
def format_section(section, lang)
|
||||
definitions = section.definitions.dup
|
||||
|
||||
key = row.key
|
||||
key = key.gsub('"', '\\\\"')
|
||||
definitions.map! { |definition| format_definition(definition, lang) }
|
||||
definitions.compact! # remove nil definitions
|
||||
definitions.join(",\n")
|
||||
end
|
||||
|
||||
value = row.translated_string_for_lang(lang, default_lang)
|
||||
value = value.gsub('"', '\\\\"')
|
||||
def key_value_pattern
|
||||
"\"%{key}\":\"%{value}\""
|
||||
end
|
||||
|
||||
f.print "\"#{key}\":\"#{value}\""
|
||||
printed_string = true
|
||||
end
|
||||
end
|
||||
end
|
||||
f.puts "\n}"
|
||||
def format_key(key)
|
||||
escape_quotes(key)
|
||||
end
|
||||
|
||||
end
|
||||
def format_value(value)
|
||||
escape_quotes(value)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Twine::Formatters.formatters << Twine::Formatters::JQuery.new
|
||||
|
|
|
@ -5,9 +5,8 @@ require 'rexml/document'
|
|||
module Twine
|
||||
module Formatters
|
||||
class Tizen < Abstract
|
||||
FORMAT_NAME = 'tizen'
|
||||
EXTENSION = '.xml'
|
||||
DEFAULT_FILE_NAME = 'strings.xml'
|
||||
include Twine::Placeholders
|
||||
|
||||
LANG_CODES = Hash[
|
||||
'eng-GB' => 'en',
|
||||
'rus-RU' => 'ru',
|
||||
|
@ -20,39 +19,21 @@ module Twine
|
|||
'por-PT' => 'pt',
|
||||
'ukr-UA' => 'uk'
|
||||
]
|
||||
DEFAULT_LANG_CODES = Hash[
|
||||
]
|
||||
|
||||
def self.can_handle_directory?(path)
|
||||
def format_name
|
||||
'tizen'
|
||||
end
|
||||
|
||||
def extension
|
||||
'.xml'
|
||||
end
|
||||
|
||||
def can_handle_directory?(path)
|
||||
Dir.entries(path).any? { |item| /^values.*$/.match(item) }
|
||||
end
|
||||
|
||||
def default_file_name
|
||||
return DEFAULT_FILE_NAME
|
||||
end
|
||||
|
||||
def write_all_files(path)
|
||||
if !File.directory?(path)
|
||||
raise Twine::Error.new("Directory does not exist: #{path}")
|
||||
end
|
||||
|
||||
langs_written = []
|
||||
Dir.foreach(path) do |item|
|
||||
if item == "." or item == ".."
|
||||
next
|
||||
end
|
||||
item = File.join(path, item)
|
||||
if !File.directory?(item)
|
||||
lang = determine_language_given_path(item)
|
||||
if lang
|
||||
write_file(item, lang)
|
||||
langs_written << lang
|
||||
end
|
||||
end
|
||||
end
|
||||
if langs_written.empty?
|
||||
raise Twine::Error.new("Failed to genertate any files: No languages found at #{path}")
|
||||
end
|
||||
'strings.xml'
|
||||
end
|
||||
|
||||
def determine_language_given_path(path)
|
||||
|
@ -68,7 +49,7 @@ module Twine
|
|||
return
|
||||
end
|
||||
|
||||
def read_file(path, lang)
|
||||
def read(io, lang)
|
||||
resources_regex = /<resources(?:[^>]*)>(.*)<\/resources>/m
|
||||
key_regex = /<string name="(\w+)">/
|
||||
comment_regex = /<!-- (.*) -->/
|
||||
|
@ -77,99 +58,80 @@ module Twine
|
|||
value = nil
|
||||
comment = nil
|
||||
|
||||
File.open(path, 'r:UTF-8') do |f|
|
||||
content_match = resources_regex.match(f.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)
|
||||
if value_match
|
||||
value = value_match[1]
|
||||
value = CGI.unescapeHTML(value)
|
||||
value.gsub!('\\\'', '\'')
|
||||
value.gsub!('\\"', '"')
|
||||
value = iosify_substitutions(value)
|
||||
value.gsub!(/(\\u0020)*|(\\u0020)*\z/) { |spaces| ' ' * (spaces.length / 6) }
|
||||
else
|
||||
value = ""
|
||||
end
|
||||
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
|
||||
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)
|
||||
if value_match
|
||||
value = value_match[1]
|
||||
value = CGI.unescapeHTML(value)
|
||||
value.gsub!('\\\'', '\'')
|
||||
value.gsub!('\\"', '"')
|
||||
value = convert_placeholders_from_android_to_twine(value)
|
||||
value.gsub!(/(\\u0020)*|(\\u0020)*\z/) { |spaces| ' ' * (spaces.length / 6) }
|
||||
else
|
||||
value = ""
|
||||
end
|
||||
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
|
||||
comment_match = comment_regex.match(line)
|
||||
if comment_match
|
||||
comment = comment_match[1]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def write_file(path, lang)
|
||||
default_lang = nil
|
||||
if DEFAULT_LANG_CODES.has_key?(lang)
|
||||
default_lang = DEFAULT_LANG_CODES[lang]
|
||||
end
|
||||
File.open(path, 'w:UTF-8') do |f|
|
||||
f.puts "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Tizen Strings File -->\n<!-- Generated by Twine #{Twine::VERSION} -->\n<!-- Language: #{lang} -->"
|
||||
f.write '<string_table Bversion="2.0.0.201311071819" Dversion="20120315">'
|
||||
@strings.sections.each do |section|
|
||||
printed_section = false
|
||||
section.rows.each do |row|
|
||||
if row.matches_tags?(@options[:tags], @options[:untagged])
|
||||
if !printed_section
|
||||
f.puts ''
|
||||
if section.name && section.name.length > 0
|
||||
section_name = section.name.gsub('--', '—')
|
||||
f.puts "\t<!-- SECTION: #{section_name} -->"
|
||||
end
|
||||
printed_section = true
|
||||
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} -->"
|
||||
end
|
||||
|
||||
key = row.key
|
||||
def format_sections(twine_file, lang)
|
||||
result = '<string_table Bversion="2.0.0.201311071819" Dversion="20120315">'
|
||||
|
||||
result += super + "\n"
|
||||
|
||||
value = row.translated_string_for_lang(lang, default_lang)
|
||||
if !value && @options[:include_untranslated]
|
||||
value = row.translated_string_for_lang(@strings.language_codes[0])
|
||||
end
|
||||
result += "</string_table>\n"
|
||||
end
|
||||
|
||||
if value # if values is nil, there was no appropriate translation, so let Tizen handle the defaulting
|
||||
value = String.new(value) # use a copy to prevent modifying the original
|
||||
def format_section_header(section)
|
||||
"\t<!-- SECTION: #{section.name} -->"
|
||||
end
|
||||
|
||||
# Tizen enforces the following rules on the values
|
||||
# 1) apostrophes and quotes must be escaped with a backslash
|
||||
value.gsub!('\'', '\\\\\'')
|
||||
value.gsub!('"', '\\\\"')
|
||||
# 2) HTML escape the string
|
||||
value = CGI.escapeHTML(value)
|
||||
# 3) fix substitutions (e.g. %s/%@)
|
||||
value = androidify_substitutions(value)
|
||||
# 4) replace beginning and end spaces with \0020. Otherwise Tizen strips them.
|
||||
value.gsub!(/\A *| *\z/) { |spaces| '\u0020' * spaces.length }
|
||||
def format_comment(definition, lang)
|
||||
"\t<!-- #{definition.comment.gsub('--', '—')} -->\n" if definition.comment
|
||||
end
|
||||
|
||||
comment = row.comment
|
||||
if comment
|
||||
comment = comment.gsub('--', '—')
|
||||
end
|
||||
def key_value_pattern
|
||||
"\t<text id=\"IDS_%{key}\">%{value}</text>"
|
||||
end
|
||||
|
||||
if comment && comment.length > 0
|
||||
f.puts "\t<!-- #{comment} -->\n"
|
||||
end
|
||||
f.puts "\t<text id=\"IDS_#{key.upcase}\">#{value}</text>"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
def format_key(key)
|
||||
key.upcase
|
||||
end
|
||||
|
||||
f.puts '</string_table>'
|
||||
end
|
||||
def format_value(value)
|
||||
value = escape_quotes(value)
|
||||
# Tizen enforces the following rules on the values
|
||||
# 1) apostrophes and quotes must be escaped with a backslash
|
||||
value.gsub!("'", "\\\\'")
|
||||
# 2) HTML escape the string
|
||||
value = CGI.escapeHTML(value)
|
||||
# 3) fix substitutions (e.g. %s/%@)
|
||||
value = convert_placeholders_from_twine_to_android(value)
|
||||
# 4) replace beginning and end spaces with \0020. Otherwise Tizen strips them.
|
||||
value.gsub(/\A *| *\z/) { |spaces| '\u0020' * spaces.length }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Twine::Formatters.formatters << Twine::Formatters::Tizen.new
|
||||
|
|
57
tools/twine/lib/twine/output_processor.rb
Normal file
57
tools/twine/lib/twine/output_processor.rb
Normal file
|
@ -0,0 +1,57 @@
|
|||
module Twine
|
||||
module Processors
|
||||
|
||||
class OutputProcessor
|
||||
def initialize(twine_file, options)
|
||||
@twine_file = twine_file
|
||||
@options = options
|
||||
end
|
||||
|
||||
def default_language
|
||||
@options[:developer_language] || @twine_file.language_codes[0]
|
||||
end
|
||||
|
||||
def fallback_languages(language)
|
||||
fallback_mapping = {
|
||||
'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
|
||||
end
|
||||
|
||||
def process(language)
|
||||
result = TwineFile.new
|
||||
|
||||
result.language_codes.concat @twine_file.language_codes
|
||||
@twine_file.sections.each do |section|
|
||||
new_section = TwineSection.new section.name
|
||||
|
||||
section.definitions.each do |definition|
|
||||
next unless definition.matches_tags?(@options[:tags], @options[:untagged])
|
||||
|
||||
value = definition.translation_for_lang(language)
|
||||
|
||||
next if value && @options[:include] == :untranslated
|
||||
|
||||
if value.nil? && @options[:include] != :translated
|
||||
value = definition.translation_for_lang(fallback_languages(language))
|
||||
end
|
||||
|
||||
next unless value
|
||||
|
||||
new_definition = definition.dup
|
||||
new_definition.translations[language] = value
|
||||
|
||||
new_section.definitions << new_definition
|
||||
result.definitions_by_key[new_definition.key] = new_definition
|
||||
end
|
||||
|
||||
result.sections << new_section
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
74
tools/twine/lib/twine/placeholders.rb
Normal file
74
tools/twine/lib/twine/placeholders.rb
Normal file
|
@ -0,0 +1,74 @@
|
|||
module Twine
|
||||
module Placeholders
|
||||
extend self
|
||||
|
||||
# Note: the ` ` (single space) flag is NOT supported
|
||||
PLACEHOLDER_FLAGS_WIDTH_PRECISION_LENGTH = '([-+0#])?(\d+|\*)?(\.(\d+|\*))?(hh?|ll?|L|z|j|t)?'
|
||||
PLACEHOLDER_PARAMETER_FLAGS_WIDTH_PRECISION_LENGTH = '(\d+\$)?' + PLACEHOLDER_FLAGS_WIDTH_PRECISION_LENGTH
|
||||
PLACEHOLDER_TYPES = '[diufFeEgGxXoscpaAq]'
|
||||
|
||||
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)
|
||||
# %@ -> %s
|
||||
value = convert_twine_string_placeholder(input)
|
||||
|
||||
placeholder_syntax = PLACEHOLDER_PARAMETER_FLAGS_WIDTH_PRECISION_LENGTH + PLACEHOLDER_TYPES
|
||||
placeholder_regex = /%#{placeholder_syntax}/
|
||||
|
||||
number_of_placeholders = value.scan(placeholder_regex).size
|
||||
|
||||
return value if number_of_placeholders == 0
|
||||
|
||||
# got placeholders -> need to double single percent signs
|
||||
# % -> %% (but %% -> %%, %d -> %d)
|
||||
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})/
|
||||
|
||||
number_of_non_numbered_placeholders = value.scan(non_numbered_placeholder_regex).size
|
||||
|
||||
return value if number_of_non_numbered_placeholders == 0
|
||||
|
||||
raise Twine::Error.new("The value \"#{input}\" contains numbered and non-numbered placeholders") if number_of_placeholders != number_of_non_numbered_placeholders
|
||||
|
||||
# %d -> %$1d
|
||||
index = 0
|
||||
value.gsub!(non_numbered_placeholder_regex) { "%#{index += 1}$#{$1}" }
|
||||
|
||||
value
|
||||
end
|
||||
|
||||
def convert_placeholders_from_android_to_twine(input)
|
||||
placeholder_regex = /(%#{PLACEHOLDER_PARAMETER_FLAGS_WIDTH_PRECISION_LENGTH})s/
|
||||
|
||||
# %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)
|
||||
|
||||
placeholder_regex = /%#{PLACEHOLDER_PARAMETER_FLAGS_WIDTH_PRECISION_LENGTH}#{PLACEHOLDER_TYPES}/
|
||||
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
|
||||
end
|
||||
end
|
|
@ -1,318 +1,325 @@
|
|||
require 'tmpdir'
|
||||
require 'fileutils'
|
||||
|
||||
Twine::Plugin.new # Initialize plugins first in Runner.
|
||||
|
||||
module Twine
|
||||
VALID_COMMANDS = ['generate-string-file', 'generate-all-string-files', 'consume-string-file', 'consume-all-string-files', 'generate-loc-drop', 'consume-loc-drop', 'generate-report', 'validate-strings-file']
|
||||
|
||||
class Runner
|
||||
def initialize(args)
|
||||
@options = {}
|
||||
@args = args
|
||||
end
|
||||
|
||||
def self.run(args)
|
||||
new(args).run
|
||||
options = CLI.parse(args)
|
||||
|
||||
twine_file = TwineFile.new
|
||||
twine_file.read options[:twine_file]
|
||||
runner = new(options, twine_file)
|
||||
|
||||
case options[:command]
|
||||
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 run
|
||||
# Parse all CLI arguments.
|
||||
CLI::parse_args(@args, @options)
|
||||
read_strings_data
|
||||
execute_command
|
||||
def initialize(options = {}, twine_file = TwineFile.new)
|
||||
@options = options
|
||||
@twine_file = twine_file
|
||||
end
|
||||
|
||||
def read_strings_data
|
||||
@strings = StringsFile.new
|
||||
@strings.read @options[:strings_file]
|
||||
end
|
||||
|
||||
def write_strings_data(path)
|
||||
def write_twine_data(path)
|
||||
if @options[:developer_language]
|
||||
@strings.set_developer_language_code(@options[:developer_language])
|
||||
@twine_file.set_developer_language_code(@options[:developer_language])
|
||||
end
|
||||
@strings.write(path)
|
||||
@twine_file.write(path)
|
||||
end
|
||||
|
||||
def execute_command
|
||||
case @options[:command]
|
||||
when 'generate-string-file'
|
||||
generate_string_file
|
||||
when 'generate-all-string-files'
|
||||
generate_all_string_files
|
||||
when 'consume-string-file'
|
||||
consume_string_file
|
||||
when 'consume-all-string-files'
|
||||
consume_all_string_files
|
||||
when 'generate-loc-drop'
|
||||
generate_loc_drop
|
||||
when 'consume-loc-drop'
|
||||
consume_loc_drop
|
||||
when 'generate-report'
|
||||
generate_report
|
||||
when 'validate-strings-file'
|
||||
validate_strings_file
|
||||
end
|
||||
end
|
||||
def generate_localization_file
|
||||
validate_twine_file if @options[:validate]
|
||||
|
||||
def generate_string_file
|
||||
lang = nil
|
||||
if @options[:languages]
|
||||
lang = @options[:languages][0]
|
||||
end
|
||||
lang = @options[:languages][0] if @options[:languages]
|
||||
|
||||
read_write_string_file(@options[:output_path], false, lang)
|
||||
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 translations." unless output
|
||||
|
||||
IO.write(@options[:output_path], output, encoding: output_encoding)
|
||||
end
|
||||
|
||||
def generate_all_string_files
|
||||
def generate_all_localization_files
|
||||
validate_twine_file if @options[:validate]
|
||||
|
||||
if !File.directory?(@options[:output_path])
|
||||
raise Twine::Error.new("Directory does not exist: #{@options[:output_path]}")
|
||||
end
|
||||
|
||||
format = @options[:format]
|
||||
if !format
|
||||
format = determine_format_given_directory(@options[:output_path])
|
||||
end
|
||||
if !format
|
||||
raise Twine::Error.new "Could not determine format given the contents of #{@options[:output_path]}"
|
||||
end
|
||||
|
||||
formatter = formatter_for_format(format)
|
||||
|
||||
formatter.write_all_files(@options[:output_path])
|
||||
end
|
||||
|
||||
def consume_string_file
|
||||
lang = nil
|
||||
if @options[:languages]
|
||||
lang = @options[:languages][0]
|
||||
end
|
||||
|
||||
read_write_string_file(@options[:input_path], true, 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_write_string_file(item, true, nil)
|
||||
rescue Twine::Error => e
|
||||
STDERR.puts "#{e.message}"
|
||||
end
|
||||
if @options[:create_folders]
|
||||
FileUtils.mkdir_p(@options[:output_path])
|
||||
else
|
||||
raise Twine::Error.new("Directory does not exist: #{@options[:output_path]}")
|
||||
end
|
||||
end
|
||||
|
||||
output_path = @options[:output_path] || @options[:strings_file]
|
||||
write_strings_data(output_path)
|
||||
end
|
||||
|
||||
def read_write_string_file(path, is_read, lang)
|
||||
if is_read && !File.file?(path)
|
||||
raise Twine::Error.new("File does not exist: #{path}")
|
||||
formatter_for_directory = find_formatter { |f| f.can_handle_directory?(@options[:output_path]) }
|
||||
formatter = formatter_for_format(@options[:format]) || formatter_for_directory
|
||||
|
||||
unless formatter
|
||||
raise Twine::Error.new "Could not determine format given the contents of #{@options[:output_path]}"
|
||||
end
|
||||
|
||||
format = @options[:format]
|
||||
if !format
|
||||
format = determine_format_given_path(path)
|
||||
end
|
||||
if !format
|
||||
raise Twine::Error.new "Unable to determine format of #{path}"
|
||||
end
|
||||
file_name = @options[:file_name] || formatter.default_file_name
|
||||
if @options[:create_folders]
|
||||
@twine_file.language_codes.each do |lang|
|
||||
output_path = File.join(@options[:output_path], formatter.output_path_for_language(lang))
|
||||
|
||||
formatter = formatter_for_format(format)
|
||||
FileUtils.mkdir_p(output_path)
|
||||
|
||||
if !lang
|
||||
lang = determine_language_given_path(path)
|
||||
end
|
||||
if !lang
|
||||
lang = formatter.determine_language_given_path(path)
|
||||
end
|
||||
if !lang
|
||||
raise Twine::Error.new "Unable to determine language for #{path}"
|
||||
end
|
||||
file_path = File.join(output_path, file_name)
|
||||
|
||||
if !@strings.language_codes.include? lang
|
||||
@strings.language_codes << lang
|
||||
end
|
||||
output = formatter.format_file(lang)
|
||||
unless output
|
||||
Twine::stderr.puts "Skipping file at path #{file_path} since it would not contain any translations."
|
||||
next
|
||||
end
|
||||
|
||||
if is_read
|
||||
formatter.read_file(path, lang)
|
||||
IO.write(file_path, output, encoding: output_encoding)
|
||||
end
|
||||
else
|
||||
formatter.write_file(path, lang)
|
||||
language_found = false
|
||||
Dir.foreach(@options[:output_path]) do |item|
|
||||
next if item == "." or item == ".."
|
||||
|
||||
output_path = File.join(@options[:output_path], item)
|
||||
next unless File.directory?(output_path)
|
||||
|
||||
lang = formatter.determine_language_given_path(output_path)
|
||||
next unless lang
|
||||
|
||||
language_found = true
|
||||
|
||||
file_path = File.join(output_path, file_name)
|
||||
output = formatter.format_file(lang)
|
||||
unless output
|
||||
Twine::stderr.puts "Skipping file at path #{file_path} since it would not contain any translations."
|
||||
next
|
||||
end
|
||||
|
||||
IO.write(file_path, output, encoding: output_encoding)
|
||||
end
|
||||
|
||||
unless language_found
|
||||
raise Twine::Error.new("Failed to generate any files: No languages found at #{@options[:output_path]}")
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
def generate_loc_drop
|
||||
begin
|
||||
require 'zip/zip'
|
||||
rescue LoadError
|
||||
raise Twine::Error.new "You must run 'gem install rubyzip' in order to create or consume localization drops."
|
||||
end
|
||||
def generate_localization_archive
|
||||
validate_twine_file if @options[:validate]
|
||||
|
||||
require_rubyzip
|
||||
|
||||
if File.file?(@options[:output_path])
|
||||
File.delete(@options[:output_path])
|
||||
end
|
||||
|
||||
Dir.mktmpdir do |dir|
|
||||
Zip::ZipFile.open(@options[:output_path], Zip::ZipFile::CREATE) do |zipfile|
|
||||
Dir.mktmpdir do |temp_dir|
|
||||
Zip::File.open(@options[:output_path], Zip::File::CREATE) do |zipfile|
|
||||
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.class::EXTENSION
|
||||
real_path = File.join(dir, file_name)
|
||||
file_name = lang + formatter.extension
|
||||
temp_path = File.join(temp_dir, file_name)
|
||||
zip_path = File.join('Locales', file_name)
|
||||
formatter.write_file(real_path, lang)
|
||||
zipfile.add(zip_path, real_path)
|
||||
|
||||
output = formatter.format_file(lang)
|
||||
unless output
|
||||
Twine::stderr.puts "Skipping file #{file_name} since it would not contain any translations."
|
||||
next
|
||||
end
|
||||
|
||||
IO.write(temp_path, output, encoding: output_encoding)
|
||||
zipfile.add(zip_path, temp_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
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
|
||||
|
||||
begin
|
||||
require 'zip/zip'
|
||||
rescue LoadError
|
||||
raise Twine::Error.new "You must run 'gem install rubyzip' in order to create or consume localization drops."
|
||||
end
|
||||
|
||||
Dir.mktmpdir do |dir|
|
||||
Zip::ZipFile.open(@options[:input_path]) do |zipfile|
|
||||
Dir.mktmpdir do |temp_dir|
|
||||
Zip::File.open(@options[:input_path]) do |zipfile|
|
||||
zipfile.each do |entry|
|
||||
if !entry.name.end_with?'/' and !File.basename(entry.name).start_with?'.'
|
||||
real_path = File.join(dir, entry.name)
|
||||
FileUtils.mkdir_p(File.dirname(real_path))
|
||||
zipfile.extract(entry.name, real_path)
|
||||
begin
|
||||
read_write_string_file(real_path, true, nil)
|
||||
rescue Twine::Error => e
|
||||
STDERR.puts "#{e.message}"
|
||||
end
|
||||
next if entry.name.end_with? '/' or File.basename(entry.name).start_with? '.'
|
||||
|
||||
real_path = File.join(temp_dir, entry.name)
|
||||
FileUtils.mkdir_p(File.dirname(real_path))
|
||||
zipfile.extract(entry.name, real_path)
|
||||
begin
|
||||
read_localization_file(real_path)
|
||||
rescue Twine::Error => e
|
||||
Twine::stderr.puts "#{e.message}"
|
||||
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)
|
||||
end
|
||||
|
||||
def generate_report
|
||||
total_strings = 0
|
||||
strings_per_lang = {}
|
||||
@strings.language_codes.each do |code|
|
||||
strings_per_lang[code] = 0
|
||||
end
|
||||
|
||||
@strings.sections.each do |section|
|
||||
section.rows.each do |row|
|
||||
total_strings += 1
|
||||
|
||||
row.translations.each_key do |code|
|
||||
strings_per_lang[code] += 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Print the report.
|
||||
puts "Total number of strings = #{total_strings}"
|
||||
@strings.language_codes.each do |code|
|
||||
puts "#{code}: #{strings_per_lang[code]}"
|
||||
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
|
||||
errors = []
|
||||
invalid_keys = 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
|
||||
|
||||
if all_keys.include? row.key
|
||||
duplicate_keys.add(row.key)
|
||||
else
|
||||
all_keys.add(row.key)
|
||||
end
|
||||
duplicate_keys.add(definition.key) if all_keys.include? definition.key
|
||||
all_keys.add(definition.key)
|
||||
|
||||
if row.tags == nil || row.tags.length == 0
|
||||
keys_without_tags.add(row.key)
|
||||
end
|
||||
keys_without_tags.add(definition.key) if definition.tags == nil or definition.tags.length == 0
|
||||
|
||||
invalid_keys << definition.key unless definition.key =~ valid_key_regex
|
||||
end
|
||||
end
|
||||
|
||||
if duplicate_keys.length > 0
|
||||
error_body = duplicate_keys.to_a.join("\n ")
|
||||
errors << "Found duplicate string key(s):\n #{error_body}"
|
||||
errors = []
|
||||
join_keys = lambda { |set| set.map { |k| " " + k }.join("\n") }
|
||||
|
||||
unless duplicate_keys.empty?
|
||||
errors << "Found duplicate key(s):\n#{join_keys.call(duplicate_keys)}"
|
||||
end
|
||||
|
||||
if keys_without_tags.length == total_strings
|
||||
errors << "None of your strings have tags."
|
||||
elsif keys_without_tags.length > 0
|
||||
error_body = keys_without_tags.to_a.join("\n ")
|
||||
errors << "Found strings(s) without tags:\n #{error_body}"
|
||||
if @options[:pedantic]
|
||||
if keys_without_tags.length == total_definitions
|
||||
errors << "None of your definitions have tags."
|
||||
elsif keys_without_tags.length > 0
|
||||
errors << "Found definitions without tags:\n#{join_keys.call(keys_without_tags)}"
|
||||
end
|
||||
end
|
||||
|
||||
if errors.length > 0
|
||||
raise Twine::Error.new errors.join("\n\n")
|
||||
unless invalid_keys.empty?
|
||||
errors << "Found key(s) with invalid characters:\n#{join_keys.call(invalid_keys)}"
|
||||
end
|
||||
|
||||
puts "#{@options[:strings_file]} is valid."
|
||||
raise Twine::Error.new errors.join("\n\n") unless errors.empty?
|
||||
|
||||
Twine::stdout.puts "#{@options[:twine_file]} is valid."
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
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 archives."
|
||||
end
|
||||
end
|
||||
|
||||
def determine_language_given_path(path)
|
||||
code = File.basename(path, File.extname(path))
|
||||
if !@strings.language_codes.include? code
|
||||
code = nil
|
||||
end
|
||||
|
||||
code
|
||||
end
|
||||
|
||||
def determine_format_given_path(path)
|
||||
ext = File.extname(path)
|
||||
Formatters.formatters.each do |formatter|
|
||||
if formatter::EXTENSION == ext
|
||||
return formatter::FORMAT_NAME
|
||||
end
|
||||
end
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
def determine_format_given_directory(directory)
|
||||
Formatters.formatters.each do |formatter|
|
||||
if formatter.can_handle_directory?(directory)
|
||||
return formatter::FORMAT_NAME
|
||||
end
|
||||
end
|
||||
|
||||
return
|
||||
return code if @twine_file.language_codes.include? code
|
||||
end
|
||||
|
||||
def formatter_for_format(format)
|
||||
Formatters.formatters.each do |formatter|
|
||||
if formatter::FORMAT_NAME == format
|
||||
return formatter.new(@strings, @options)
|
||||
end
|
||||
find_formatter { |f| f.format_name == format }
|
||||
end
|
||||
|
||||
def find_formatter(&block)
|
||||
formatter = Formatters.formatters.find &block
|
||||
return nil unless formatter
|
||||
formatter.twine_file = @twine_file
|
||||
formatter.options = @options
|
||||
formatter
|
||||
end
|
||||
|
||||
def read_localization_file(path, lang = nil)
|
||||
unless File.file?(path)
|
||||
raise Twine::Error.new("File does not exist: #{path}")
|
||||
end
|
||||
|
||||
return
|
||||
formatter, lang = prepare_read_write(path, lang)
|
||||
|
||||
external_encoding = @options[:encoding] || Twine::Encoding.encoding_for_path(path)
|
||||
|
||||
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
|
||||
|
||||
unless formatter
|
||||
raise Twine::Error.new "Unable to determine format of #{path}"
|
||||
end
|
||||
|
||||
lang = lang || determine_language_given_path(path) || formatter.determine_language_given_path(path)
|
||||
unless lang
|
||||
raise Twine::Error.new "Unable to determine language for #{path}"
|
||||
end
|
||||
|
||||
@twine_file.language_codes << lang unless @twine_file.language_codes.include? lang
|
||||
|
||||
return formatter, lang
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
229
tools/twine/lib/twine/twine_file.rb
Normal file
229
tools/twine/lib/twine/twine_file.rb
Normal file
|
@ -0,0 +1,229 @@
|
|||
module Twine
|
||||
class TwineDefinition
|
||||
attr_reader :key
|
||||
attr_accessor :comment
|
||||
attr_accessor :tags
|
||||
attr_reader :translations
|
||||
attr_accessor :reference
|
||||
attr_accessor :reference_key
|
||||
|
||||
def initialize(key)
|
||||
@key = key
|
||||
@comment = nil
|
||||
@tags = nil
|
||||
@translations = {}
|
||||
end
|
||||
|
||||
def comment
|
||||
raw_comment || (reference.comment if reference)
|
||||
end
|
||||
|
||||
def raw_comment
|
||||
@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.
|
||||
return true
|
||||
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.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 translation_for_lang(lang)
|
||||
translation = [lang].flatten.map { |l| @translations[l] }.first
|
||||
|
||||
translation = reference.translation_for_lang(lang) if translation.nil? && reference
|
||||
|
||||
return translation
|
||||
end
|
||||
end
|
||||
|
||||
class TwineSection
|
||||
attr_reader :name
|
||||
attr_reader :definitions
|
||||
|
||||
def initialize(name)
|
||||
@name = name
|
||||
@definitions = []
|
||||
end
|
||||
end
|
||||
|
||||
class TwineFile
|
||||
attr_reader :sections
|
||||
attr_reader :definitions_by_key
|
||||
attr_reader :language_codes
|
||||
|
||||
private
|
||||
|
||||
def match_key(text)
|
||||
match = /^\[(.+)\]$/.match(text)
|
||||
return match[1] if match
|
||||
end
|
||||
|
||||
public
|
||||
|
||||
def initialize
|
||||
@sections = []
|
||||
@definitions_by_key = {}
|
||||
@language_codes = []
|
||||
end
|
||||
|
||||
def add_language_code(code)
|
||||
if @language_codes.length == 0
|
||||
@language_codes << code
|
||||
elsif !@language_codes.include?(code)
|
||||
dev_lang = @language_codes[0]
|
||||
@language_codes << code
|
||||
@language_codes.delete(dev_lang)
|
||||
@language_codes.sort!
|
||||
@language_codes.insert(0, dev_lang)
|
||||
end
|
||||
end
|
||||
|
||||
def set_developer_language_code(code)
|
||||
@language_codes.delete(code)
|
||||
@language_codes.insert(0, code)
|
||||
end
|
||||
|
||||
def read(path)
|
||||
if !File.file?(path)
|
||||
raise Twine::Error.new("File does not exist: #{path}")
|
||||
end
|
||||
|
||||
File.open(path, 'r:UTF-8') do |f|
|
||||
line_num = 0
|
||||
current_section = nil
|
||||
current_definition = nil
|
||||
while line = f.gets
|
||||
parsed = false
|
||||
line.strip!
|
||||
line_num += 1
|
||||
|
||||
if line.length == 0
|
||||
next
|
||||
end
|
||||
|
||||
if line.length > 4 && line[0, 2] == '[['
|
||||
match = /^\[\[(.+)\]\]$/.match(line)
|
||||
if match
|
||||
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_definition = TwineDefinition.new(key)
|
||||
@definitions_by_key[current_definition.key] = current_definition
|
||||
if !current_section
|
||||
current_section = TwineSection.new('')
|
||||
@sections << current_section
|
||||
end
|
||||
current_section.definitions << current_definition
|
||||
parsed = true
|
||||
end
|
||||
else
|
||||
match = /^([^=]+)=(.*)$/.match(line)
|
||||
if match
|
||||
key = match[1].strip
|
||||
value = match[2].strip
|
||||
|
||||
value = value[1..-2] if value[0] == '`' && value[-1] == '`'
|
||||
|
||||
case key
|
||||
when 'comment'
|
||||
current_definition.comment = value
|
||||
when 'tags'
|
||||
current_definition.tags = value.split(',')
|
||||
when 'ref'
|
||||
current_definition.reference_key = value if value
|
||||
else
|
||||
if !@language_codes.include? key
|
||||
add_language_code(key)
|
||||
end
|
||||
current_definition.translations[key] = value
|
||||
end
|
||||
parsed = true
|
||||
end
|
||||
end
|
||||
|
||||
if !parsed
|
||||
raise Twine::Error.new("Unable to parse line #{line_num} of #{path}: #{line}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# resolve_references
|
||||
@definitions_by_key.each do |key, definition|
|
||||
next unless definition.reference_key
|
||||
definition.reference = @definitions_by_key[definition.reference_key]
|
||||
end
|
||||
end
|
||||
|
||||
def write(path)
|
||||
dev_lang = @language_codes[0]
|
||||
|
||||
File.open(path, 'w:UTF-8') do |f|
|
||||
@sections.each do |section|
|
||||
if f.pos > 0
|
||||
f.puts ''
|
||||
end
|
||||
|
||||
f.puts "[[#{section.name}]]"
|
||||
|
||||
section.definitions.each do |definition|
|
||||
f.puts "\t[#{definition.key}]"
|
||||
|
||||
value = write_value(definition, dev_lang, f)
|
||||
if !value && !definition.reference_key
|
||||
puts "Warning: #{definition.key} does not exist in developer language '#{dev_lang}'"
|
||||
end
|
||||
|
||||
if definition.reference_key
|
||||
f.puts "\t\tref = #{definition.reference_key}"
|
||||
end
|
||||
if definition.tags && definition.tags.length > 0
|
||||
tag_str = definition.tags.join(',')
|
||||
f.puts "\t\ttags = #{tag_str}"
|
||||
end
|
||||
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(definition, lang, f)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def write_value(definition, language, file)
|
||||
value = definition.translations[language]
|
||||
return nil unless value
|
||||
|
||||
if value[0] == ' ' || value[-1] == ' ' || (value[0] == '`' && value[-1] == '`')
|
||||
value = '`' + value + '`'
|
||||
end
|
||||
|
||||
file.puts "\t\t#{language} = #{value}"
|
||||
return value
|
||||
end
|
||||
|
||||
end
|
||||
end
|
|
@ -1,3 +1,3 @@
|
|||
module Twine
|
||||
VERSION = '0.6.0'
|
||||
VERSION = '0.10.1'
|
||||
end
|
||||
|
|
5
tools/twine/test/fixtures/en-1.json
vendored
5
tools/twine/test/fixtures/en-1.json
vendored
|
@ -1,5 +0,0 @@
|
|||
{
|
||||
"key1":"key1-english",
|
||||
"key3":"key3-english",
|
||||
"key5":"A new string"
|
||||
}
|
16
tools/twine/test/fixtures/en-1.po
vendored
16
tools/twine/test/fixtures/en-1.po
vendored
|
@ -1,16 +0,0 @@
|
|||
msgid ""
|
||||
msgstr ""
|
||||
"Language: en\n"
|
||||
"X-Generator: Twine\n"
|
||||
|
||||
msgctxt "key1"
|
||||
msgid "key1-english"
|
||||
msgstr "key1-english"
|
||||
|
||||
msgctxt "key3"
|
||||
msgid "key3-english"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "key5"
|
||||
msgid "A new string"
|
||||
msgstr "A new string"
|
10
tools/twine/test/fixtures/en-1.strings
vendored
10
tools/twine/test/fixtures/en-1.strings
vendored
|
@ -1,10 +0,0 @@
|
|||
/**
|
||||
* iOS Strings File
|
||||
* Generated by Twine
|
||||
* Language: en
|
||||
*/
|
||||
|
||||
/* My Strings */
|
||||
"key1" = "key1-english";
|
||||
"key3" = "key3-english";
|
||||
"key5" = "A new string";
|
23
tools/twine/test/fixtures/en-2.po
vendored
23
tools/twine/test/fixtures/en-2.po
vendored
|
@ -1,23 +0,0 @@
|
|||
msgid ""
|
||||
msgstr ""
|
||||
"Language: en\n"
|
||||
"X-Generator: Twine\n"
|
||||
|
||||
msgctxt "key1"
|
||||
msgid "key1-english"
|
||||
msgstr "key1-english"
|
||||
|
||||
msgctxt "key3"
|
||||
msgid "key3-english"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "key4"
|
||||
msgid "key4"
|
||||
"multiline"
|
||||
msgstr "A multi"
|
||||
"line string\n"
|
||||
"can occur"
|
||||
|
||||
msgctxt "key5"
|
||||
msgid "A new string"
|
||||
msgstr "A new string"
|
8
tools/twine/test/fixtures/en-3.xml
vendored
8
tools/twine/test/fixtures/en-3.xml
vendored
|
@ -1,8 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Android Strings File -->
|
||||
<!-- Generated by Twine 0.5.0 -->
|
||||
<!-- Language: en -->
|
||||
<resources>
|
||||
<!-- SECTION: My Strings -->
|
||||
<string name="string_with_spaces">\u0020string with spaces\u0020\u0020</string>
|
||||
</resources>
|
10
tools/twine/test/fixtures/fr-1.xml
vendored
10
tools/twine/test/fixtures/fr-1.xml
vendored
|
@ -1,10 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Android Strings File -->
|
||||
<!-- Generated by Twine -->
|
||||
<!-- Language: fr -->
|
||||
<resources>
|
||||
<!-- This is a comment -->
|
||||
<string name="key1">key1-french</string>
|
||||
<string name="key2">key2-french</string>
|
||||
<string name="key3">key3-french</string>
|
||||
</resources>
|
17
tools/twine/test/fixtures/strings-1.txt
vendored
17
tools/twine/test/fixtures/strings-1.txt
vendored
|
@ -1,17 +0,0 @@
|
|||
[[My Strings]]
|
||||
[key1]
|
||||
en = key1-english
|
||||
tags = tag1
|
||||
comment = This is a comment
|
||||
es = key1-spanish
|
||||
fr = key1-french
|
||||
[key2]
|
||||
en = key2-english
|
||||
tags = tag2
|
||||
fr = key2-french
|
||||
[key3]
|
||||
en = key3-english
|
||||
tags = tag1,tag2
|
||||
es = key3-spanish
|
||||
[key4]
|
||||
en = key4-english
|
5
tools/twine/test/fixtures/strings-2.txt
vendored
5
tools/twine/test/fixtures/strings-2.txt
vendored
|
@ -1,5 +0,0 @@
|
|||
[[My Strings]]
|
||||
[key with space ]
|
||||
en = `string with space `
|
||||
tags = tag1
|
||||
comment = String ends with space
|
5
tools/twine/test/fixtures/strings-3.txt
vendored
5
tools/twine/test/fixtures/strings-3.txt
vendored
|
@ -1,5 +0,0 @@
|
|||
[[My Strings]]
|
||||
[parameterized_string]
|
||||
en = The %@ brown fox jumps over the %@ dog %d times.
|
||||
[percentage_string]
|
||||
en = This product is %d%% off.
|
|
@ -1,5 +0,0 @@
|
|||
[[Line Break Strings]]
|
||||
[line_breaking]
|
||||
en = This\nstring\ncontains\nline\nbreaks
|
||||
tags = tag1
|
||||
fr = This\nstring\nalso\ncontains\nline\nbreaks
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"line_breaking":"This\nstring\ncontains\nline\nbreaks"
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"line_breaking":"This\nstring\nalso\ncontains\nline\nbreaks"
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
[[Line Break Strings]]
|
||||
[line_breaking]
|
||||
en = This\nstring\ncontains\nline\nbreaks
|
||||
tags = tag1
|
12
tools/twine/test/fixtures/test-output-1.txt
vendored
12
tools/twine/test/fixtures/test-output-1.txt
vendored
|
@ -1,12 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Android Strings File -->
|
||||
<!-- Generated by Twine <%= Twine::VERSION %> -->
|
||||
<!-- Language: fr -->
|
||||
<resources>
|
||||
<!-- SECTION: My Strings -->
|
||||
<!-- This is a comment -->
|
||||
<string name="key1">key1-french</string>
|
||||
<string name="key2">key2-french</string>
|
||||
<string name="key3">key3-english</string>
|
||||
<string name="key4">key4-english</string>
|
||||
</resources>
|
9
tools/twine/test/fixtures/test-output-10.txt
vendored
9
tools/twine/test/fixtures/test-output-10.txt
vendored
|
@ -1,9 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Android Strings File -->
|
||||
<!-- Generated by Twine <%= Twine::VERSION %> -->
|
||||
<!-- Language: en -->
|
||||
<resources>
|
||||
<!-- SECTION: My Strings -->
|
||||
<!-- String ends with space -->
|
||||
<string name="key with space ">string with space\u0020</string>
|
||||
</resources>
|
9
tools/twine/test/fixtures/test-output-11.txt
vendored
9
tools/twine/test/fixtures/test-output-11.txt
vendored
|
@ -1,9 +0,0 @@
|
|||
[[Uncategorized]]
|
||||
[string_with_spaces]
|
||||
en = ` string with spaces `
|
||||
|
||||
[[My Strings]]
|
||||
[key with space ]
|
||||
en = `string with space `
|
||||
tags = tag1
|
||||
comment = String ends with space
|
12
tools/twine/test/fixtures/test-output-12.txt
vendored
12
tools/twine/test/fixtures/test-output-12.txt
vendored
|
@ -1,12 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Tizen Strings File -->
|
||||
<!-- Generated by Twine <%= Twine::VERSION %> -->
|
||||
<!-- Language: fr -->
|
||||
<string_table Bversion="2.0.0.201311071819" Dversion="20120315">
|
||||
<!-- SECTION: My Strings -->
|
||||
<!-- This is a comment -->
|
||||
<text id="IDS_KEY1">key1-french</text>
|
||||
<text id="IDS_KEY2">key2-french</text>
|
||||
<text id="IDS_KEY3">key3-english</text>
|
||||
<text id="IDS_KEY4">key4-english</text>
|
||||
</string_table>
|
12
tools/twine/test/fixtures/test-output-2.txt
vendored
12
tools/twine/test/fixtures/test-output-2.txt
vendored
|
@ -1,12 +0,0 @@
|
|||
/**
|
||||
* Apple Strings File
|
||||
* Generated by Twine <%= Twine::VERSION %>
|
||||
* Language: en
|
||||
*/
|
||||
|
||||
/********** My Strings **********/
|
||||
|
||||
/* This is a comment */
|
||||
"key1" = "key1-english";
|
||||
|
||||
"key3" = "key3-english";
|
18
tools/twine/test/fixtures/test-output-3.txt
vendored
18
tools/twine/test/fixtures/test-output-3.txt
vendored
|
@ -1,18 +0,0 @@
|
|||
[[My Strings]]
|
||||
[key1]
|
||||
en = key1-english
|
||||
tags = tag1
|
||||
comment = This is a comment
|
||||
es = key1-spanish
|
||||
fr = key1-french
|
||||
[key2]
|
||||
en = key2-english
|
||||
tags = tag2
|
||||
fr = key2-french
|
||||
[key3]
|
||||
en = key3-english
|
||||
tags = tag1,tag2
|
||||
es = key3-spanish
|
||||
fr = key3-french
|
||||
[key4]
|
||||
en = key4-english
|
21
tools/twine/test/fixtures/test-output-4.txt
vendored
21
tools/twine/test/fixtures/test-output-4.txt
vendored
|
@ -1,21 +0,0 @@
|
|||
[[Uncategorized]]
|
||||
[key5]
|
||||
en = A new string
|
||||
|
||||
[[My Strings]]
|
||||
[key1]
|
||||
en = key1-english
|
||||
tags = tag1
|
||||
comment = This is a comment
|
||||
es = key1-spanish
|
||||
fr = key1-french
|
||||
[key2]
|
||||
en = key2-english
|
||||
tags = tag2
|
||||
fr = key2-french
|
||||
[key3]
|
||||
en = key3-english
|
||||
tags = tag1,tag2
|
||||
es = key3-spanish
|
||||
[key4]
|
||||
en = key4-english
|
4
tools/twine/test/fixtures/test-output-5.txt
vendored
4
tools/twine/test/fixtures/test-output-5.txt
vendored
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"key1":"key1-english",
|
||||
"key3":"key3-english"
|
||||
}
|
10
tools/twine/test/fixtures/test-output-6.txt
vendored
10
tools/twine/test/fixtures/test-output-6.txt
vendored
|
@ -1,10 +0,0 @@
|
|||
/**
|
||||
* Apple Strings File
|
||||
* Generated by Twine <%= Twine::VERSION %>
|
||||
* Language: en
|
||||
*/
|
||||
|
||||
/********** My Strings **********/
|
||||
|
||||
/* String ends with space */
|
||||
"key with space " = "string with space ";
|
16
tools/twine/test/fixtures/test-output-7.txt
vendored
16
tools/twine/test/fixtures/test-output-7.txt
vendored
|
@ -1,16 +0,0 @@
|
|||
msgid ""
|
||||
msgstr ""
|
||||
"Language: en\n"
|
||||
"X-Generator: Twine <%= Twine::VERSION %>\n"
|
||||
|
||||
|
||||
# SECTION: My Strings
|
||||
#. "This is a comment"
|
||||
msgctxt "key1"
|
||||
msgid "key1-english"
|
||||
msgstr "key1-english"
|
||||
|
||||
msgctxt "key3"
|
||||
msgid "key3-english"
|
||||
msgstr "key3-english"
|
||||
|
9
tools/twine/test/fixtures/test-output-8.txt
vendored
9
tools/twine/test/fixtures/test-output-8.txt
vendored
|
@ -1,9 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Android Strings File -->
|
||||
<!-- Generated by Twine <%= Twine::VERSION %> -->
|
||||
<!-- Language: en -->
|
||||
<resources>
|
||||
<!-- SECTION: My Strings -->
|
||||
<string name="parameterized_string">The %1$s brown fox jumps over the %2$s dog %3$d times.</string>
|
||||
<string name="percentage_string">This product is %d%% off.</string>
|
||||
</resources>
|
21
tools/twine/test/fixtures/test-output-9.txt
vendored
21
tools/twine/test/fixtures/test-output-9.txt
vendored
|
@ -1,21 +0,0 @@
|
|||
[[Uncategorized]]
|
||||
[key5]
|
||||
en = A new string
|
||||
|
||||
[[My Strings]]
|
||||
[key1]
|
||||
en = key1-english
|
||||
tags = tag1
|
||||
comment = This is a comment
|
||||
es = key1-spanish
|
||||
fr = key1-french
|
||||
[key2]
|
||||
en = key2-english
|
||||
tags = tag2
|
||||
fr = key2-french
|
||||
[key3]
|
||||
en = key3-english
|
||||
tags = tag1,tag2
|
||||
es = key3-spanish
|
||||
[key4]
|
||||
en = A multiline string\ncan occur
|
|
@ -1,138 +0,0 @@
|
|||
require 'erb'
|
||||
require 'rubygems'
|
||||
require 'test/unit'
|
||||
require 'twine'
|
||||
|
||||
class TwineTest < Test::Unit::TestCase
|
||||
def test_generate_string_file_1
|
||||
Dir.mktmpdir do |dir|
|
||||
output_path = File.join(dir, 'fr.xml')
|
||||
Twine::Runner.run(%W(generate-string-file test/fixtures/strings-1.txt #{output_path} --include-untranslated))
|
||||
assert_equal(ERB.new(File.read('test/fixtures/test-output-1.txt')).result, File.read(output_path))
|
||||
end
|
||||
end
|
||||
|
||||
def test_generate_string_file_2
|
||||
Dir.mktmpdir do |dir|
|
||||
output_path = File.join(dir, 'en.strings')
|
||||
Twine::Runner.run(%W(generate-string-file test/fixtures/strings-1.txt #{output_path} -t tag1))
|
||||
assert_equal(ERB.new(File.read('test/fixtures/test-output-2.txt')).result, File.read(output_path))
|
||||
end
|
||||
end
|
||||
|
||||
def test_generate_string_file_3
|
||||
Dir.mktmpdir do |dir|
|
||||
output_path = File.join(dir, 'en.json')
|
||||
Twine::Runner.run(%W(generate-string-file test/fixtures/strings-1.txt #{output_path} -t tag1))
|
||||
assert_equal(ERB.new(File.read('test/fixtures/test-output-5.txt')).result, File.read(output_path))
|
||||
end
|
||||
end
|
||||
|
||||
def test_generate_string_file_4
|
||||
Dir.mktmpdir do |dir|
|
||||
output_path = File.join(dir, 'en.strings')
|
||||
Twine::Runner.run(%W(generate-string-file test/fixtures/strings-2.txt #{output_path} -t tag1))
|
||||
assert_equal(ERB.new(File.read('test/fixtures/test-output-6.txt')).result, File.read(output_path))
|
||||
end
|
||||
end
|
||||
|
||||
def test_generate_string_file_5
|
||||
Dir.mktmpdir do |dir|
|
||||
output_path = File.join(dir, 'en.po')
|
||||
Twine::Runner.run(%W(generate-string-file test/fixtures/strings-1.txt #{output_path} -t tag1))
|
||||
assert_equal(ERB.new(File.read('test/fixtures/test-output-7.txt')).result, File.read(output_path))
|
||||
end
|
||||
end
|
||||
|
||||
def test_generate_string_file_6
|
||||
Dir.mktmpdir do |dir|
|
||||
output_path = File.join(dir, 'en.xml')
|
||||
Twine::Runner.run(%W(generate-string-file test/fixtures/strings-3.txt #{output_path}))
|
||||
assert_equal(ERB.new(File.read('test/fixtures/test-output-8.txt')).result, File.read(output_path))
|
||||
end
|
||||
end
|
||||
|
||||
def test_generate_string_file_7
|
||||
Dir.mktmpdir do |dir|
|
||||
output_path = File.join(dir, 'en.xml')
|
||||
Twine::Runner.run(%W(generate-string-file test/fixtures/strings-2.txt #{output_path} -t tag1))
|
||||
assert_equal(ERB.new(File.read('test/fixtures/test-output-10.txt')).result, File.read(output_path))
|
||||
end
|
||||
end
|
||||
|
||||
def test_generate_string_file_8
|
||||
Dir.mktmpdir do |dir|
|
||||
output_path = File.join(dir, 'fr.xml')
|
||||
Twine::Runner.run(%W(generate-string-file --format tizen test/fixtures/strings-1.txt #{output_path} --include-untranslated))
|
||||
assert_equal(ERB.new(File.read('test/fixtures/test-output-12.txt')).result, File.read(output_path))
|
||||
end
|
||||
end
|
||||
|
||||
def test_consume_string_file_1
|
||||
Dir.mktmpdir do |dir|
|
||||
output_path = File.join(dir, 'strings.txt')
|
||||
Twine::Runner.run(%W(consume-string-file test/fixtures/strings-1.txt test/fixtures/fr-1.xml -o #{output_path} -l fr))
|
||||
assert_equal(File.read('test/fixtures/test-output-3.txt'), File.read(output_path))
|
||||
end
|
||||
end
|
||||
|
||||
def test_consume_string_file_2
|
||||
Dir.mktmpdir do |dir|
|
||||
output_path = File.join(dir, 'strings.txt')
|
||||
Twine::Runner.run(%W(consume-string-file test/fixtures/strings-1.txt test/fixtures/en-1.strings -o #{output_path} -l en -a))
|
||||
assert_equal(File.read('test/fixtures/test-output-4.txt'), File.read(output_path))
|
||||
end
|
||||
end
|
||||
|
||||
def test_consume_string_file_3
|
||||
Dir.mktmpdir do |dir|
|
||||
output_path = File.join(dir, 'strings.txt')
|
||||
Twine::Runner.run(%W(consume-string-file test/fixtures/strings-1.txt test/fixtures/en-1.json -o #{output_path} -l en -a))
|
||||
assert_equal(File.read('test/fixtures/test-output-4.txt'), File.read(output_path))
|
||||
end
|
||||
end
|
||||
|
||||
def test_consume_string_file_4
|
||||
Dir.mktmpdir do |dir|
|
||||
output_path = File.join(dir, 'strings.txt')
|
||||
Twine::Runner.run(%W(consume-string-file test/fixtures/strings-1.txt test/fixtures/en-1.po -o #{output_path} -l en -a))
|
||||
assert_equal(File.read('test/fixtures/test-output-4.txt'), File.read(output_path))
|
||||
end
|
||||
end
|
||||
|
||||
def test_consume_string_file_5
|
||||
Dir.mktmpdir do |dir|
|
||||
output_path = File.join(dir, 'strings.txt')
|
||||
Twine::Runner.run(%W(consume-string-file test/fixtures/strings-1.txt test/fixtures/en-2.po -o #{output_path} -l en -a))
|
||||
assert_equal(File.read('test/fixtures/test-output-9.txt'), File.read(output_path))
|
||||
end
|
||||
end
|
||||
|
||||
def test_consume_string_file_6
|
||||
Dir.mktmpdir do |dir|
|
||||
output_path = File.join(dir, 'strings.txt')
|
||||
Twine::Runner.run(%W(consume-string-file test/fixtures/strings-2.txt test/fixtures/en-3.xml -o #{output_path} -l en -a))
|
||||
assert_equal(File.read('test/fixtures/test-output-11.txt'), File.read(output_path))
|
||||
end
|
||||
end
|
||||
|
||||
def test_generate_report_1
|
||||
Twine::Runner.run(%w(generate-report test/fixtures/strings-1.txt))
|
||||
end
|
||||
|
||||
def test_json_line_breaks_consume
|
||||
Dir.mktmpdir do |dir|
|
||||
output_path = File.join(dir, 'strings.txt')
|
||||
Twine::Runner.run(%W(consume-string-file test/fixtures/test-json-line-breaks/line-breaks.txt test/fixtures/test-json-line-breaks/line-breaks.json -l fr -o #{output_path}))
|
||||
assert_equal(File.read('test/fixtures/test-json-line-breaks/consumed.txt'), File.read(output_path))
|
||||
end
|
||||
end
|
||||
|
||||
def test_json_line_breaks_generate
|
||||
Dir.mktmpdir do |dir|
|
||||
output_path = File.join(dir, 'en.json')
|
||||
Twine::Runner.run(%W(generate-string-file test/fixtures/test-json-line-breaks/line-breaks.txt #{output_path}))
|
||||
assert_equal(File.read('test/fixtures/test-json-line-breaks/generated.json'), File.read(output_path))
|
||||
end
|
||||
end
|
||||
end
|
|
@ -5,22 +5,25 @@ 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.summary = "Manage strings and their translations for your iOS, Android and other projects."
|
||||
s.homepage = "https://github.com/mobiata/twine"
|
||||
s.email = "twine@mobiata.com"
|
||||
s.authors = [ "Sebastian Celis" ]
|
||||
s.has_rdoc = false
|
||||
s.license = "BSD-3-Clause"
|
||||
|
||||
s.files = %w( Gemfile README.md LICENSE )
|
||||
s.files += Dir.glob("lib/**/*")
|
||||
s.files += Dir.glob("bin/**/*")
|
||||
s.files += Dir.glob("test/**/*")
|
||||
s.test_file = 'test/twine_test.rb'
|
||||
s.test_files = Dir.glob("test/test_*")
|
||||
|
||||
s.required_ruby_version = ">= 1.8.7"
|
||||
s.add_runtime_dependency('rubyzip', "~> 0.9.5")
|
||||
s.add_runtime_dependency('safe_yaml', "~> 1.0.3")
|
||||
s.add_development_dependency('rake', "~> 0.9.2")
|
||||
s.required_ruby_version = ">= 2.0"
|
||||
s.add_runtime_dependency('rubyzip', "~> 1.1")
|
||||
s.add_runtime_dependency('safe_yaml', "~> 1.0")
|
||||
s.add_development_dependency('rake', "~> 10.4")
|
||||
s.add_development_dependency('minitest', "~> 5.5")
|
||||
s.add_development_dependency('mocha', "~> 1.1")
|
||||
|
||||
s.executables = %w( twine )
|
||||
s.description = <<desc
|
||||
|
|
|
@ -6,9 +6,9 @@ TWINE="$OMIM_PATH/tools/twine/twine"
|
|||
|
||||
# TODO: Add "--untagged --tags android" when tags are properly set.
|
||||
# TODO: Add validate-strings-file call to check for duplicates (and avoid Android build errors) when tags are properly set.
|
||||
$TWINE --format android generate-all-string-files "$OMIM_PATH/strings.txt" "$OMIM_PATH/android/res/"
|
||||
$TWINE --format apple generate-all-string-files "$OMIM_PATH/strings.txt" "$OMIM_PATH/iphone/Maps/LocalizedStrings/"
|
||||
$TWINE --format apple --file-name InfoPlist.strings generate-all-string-files "$OMIM_PATH/iphone/plist.txt" "$OMIM_PATH/iphone/Maps/LocalizedStrings/"
|
||||
$TWINE --format jquery generate-all-string-files "$OMIM_PATH/data/cuisines.txt" "$OMIM_PATH/data/cuisine-strings/"
|
||||
$TWINE --format jquery generate-all-string-files "$OMIM_PATH/data/countries_names.txt" "$OMIM_PATH/data/countries-strings/"
|
||||
#$TWINE --format tizen generate-all-string-files "$OMIM_PATH/strings.txt" "$OMIM_PATH/tizen/MapsWithMe/res/" --tags tizen
|
||||
$TWINE generate-all-localization-files --include translated --format android "$OMIM_PATH/strings.txt" "$OMIM_PATH/android/res/"
|
||||
$TWINE generate-all-localization-files --include all --format apple "$OMIM_PATH/strings.txt" "$OMIM_PATH/iphone/Maps/LocalizedStrings/"
|
||||
$TWINE generate-all-localization-files --include all --format apple --file-name InfoPlist.strings "$OMIM_PATH/iphone/plist.txt" "$OMIM_PATH/iphone/Maps/LocalizedStrings/"
|
||||
$TWINE generate-all-localization-files --include all --format jquery "$OMIM_PATH/data/cuisines.txt" "$OMIM_PATH/data/cuisine-strings/"
|
||||
$TWINE generate-all-localization-files --include all --format jquery "$OMIM_PATH/data/countries_names.txt" "$OMIM_PATH/data/countries-strings/"
|
||||
#$TWINE generate-all-localization-files --include translated --format tizen "$OMIM_PATH/strings.txt" "$OMIM_PATH/tizen/MapsWithMe/res/" --tags tizen
|
||||
|
|
Loading…
Add table
Reference in a new issue