[twine] Upgrade twine to 0.10.1

This commit is contained in:
Ilya Zverev 2017-06-16 17:05:19 +03:00 committed by Vladimir Byko-Ianko
parent 3c9fd26dcf
commit 6de61479bf
48 changed files with 1746 additions and 1660 deletions

31
tools/twine/0.10.1.patch Normal file
View 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]'

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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!(/&/, '&amp;')
value.gsub!('<', '&lt;')
# 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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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

View 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

View file

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

View 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

View file

@ -1,3 +1,3 @@
module Twine
VERSION = '0.6.0'
VERSION = '0.10.1'
end

View file

@ -1,5 +0,0 @@
{
"key1":"key1-english",
"key3":"key3-english",
"key5":"A new string"
}

View file

@ -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"

View file

@ -1,10 +0,0 @@
/**
* iOS Strings File
* Generated by Twine
* Language: en
*/
/* My Strings */
"key1" = "key1-english";
"key3" = "key3-english";
"key5" = "A new string";

View file

@ -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"

View file

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

View file

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

View file

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

View file

@ -1,5 +0,0 @@
[[My Strings]]
[key with space ]
en = `string with space `
tags = tag1
comment = String ends with space

View file

@ -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.

View file

@ -1,5 +0,0 @@
[[Line Break Strings]]
[line_breaking]
en = This\nstring\ncontains\nline\nbreaks
tags = tag1
fr = This\nstring\nalso\ncontains\nline\nbreaks

View file

@ -1,3 +0,0 @@
{
"line_breaking":"This\nstring\ncontains\nline\nbreaks"
}

View file

@ -1,3 +0,0 @@
{
"line_breaking":"This\nstring\nalso\ncontains\nline\nbreaks"
}

View file

@ -1,4 +0,0 @@
[[Line Break Strings]]
[line_breaking]
en = This\nstring\ncontains\nline\nbreaks
tags = tag1

View file

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

View file

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

View file

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

View file

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

View file

@ -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";

View file

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

View file

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

View file

@ -1,4 +0,0 @@
{
"key1":"key1-english",
"key3":"key3-english"
}

View file

@ -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 ";

View file

@ -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"

View file

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

View file

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

View file

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

View file

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

View file

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