-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
12 changed files
with
494 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
# A sample Gemfile | ||
source "https://rubygems.org" | ||
gemspec |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
# i18n-asf | ||
|
||
This library extends the [Ruby i18n library](https://github.com/svenfuchs/i18n) that ships with Rails to add support for ASF (Adobe Strings Format), Adobe's XML-based alternative to ZStrings for localizing application UI text. While i18n is the Rails default, and this library has been written with a Rails application in mind, you can also use this gem to add ASF support to any Ruby project. | ||
|
||
## Setup | ||
|
||
Just add this library to your `Gemfile`, either in addition to or instead of `i18n`: | ||
|
||
```ruby | ||
gem 'i18n-asf', :git => "git@fit.corp.adobe.com:ddemaree/i18n-asf.git" | ||
``` | ||
|
||
Once that's done, you will be able to add `.asf` translation files to your app's `config/locales` directory alongside the `.yml` or `.rb` formats supported by the core library. | ||
|
||
## Usage | ||
|
||
The [Rails Internationalization Guide](http://guides.rubyonrails.org/i18n.html) provides a good overview of the i18n library and how to use it in a Rails project. Below you'll find a brief overview of ASF and how its structure maps to features and concepts in the core i18n framework. | ||
|
||
ASF is a fairly lightweight XML grammar. Documents consist of an `asf` tag, which in turn can contain `str` (string) or `set` tags, both of which are identified by a `name` attribute. Both kinds of elements can have a `desc` element, used to describe the element's purpose and provide context for the translators. ASF strings can have multiple values, and both strings and sets can support arbitrary key-value metadata, though neither of these features are used here. | ||
|
||
Here's an example of a valid ASF document: | ||
|
||
```xml | ||
<?xml version="1.0" encoding="utf-8" standalone="no" ?> | ||
<!DOCTYPE asf SYSTEM "http://ns.adobe.com/asf/asf_1_0.dtd"> | ||
<asf version="1.0" locale="en_US" xmlns="http://ns.adobe.com/asf"> | ||
<str name="simple"> | ||
<desc>Example of a simple string definition</desc> | ||
<val>Add to Kit</val> | ||
</str> | ||
<set name="browse"> | ||
<desc>Strings for the font browsing UI</desc> | ||
<str name="font_count_heading"> | ||
<desc>Text for heading that includes the number of fonts on the page</desc> | ||
<val>Showing <param name="font_count"/> fonts</val> | ||
</str> | ||
</set> | ||
</asf> | ||
``` | ||
|
||
ASF files are parsed into a Ruby hash structure, following the same general nesting and structural conventions the i18n library expects: | ||
|
||
```ruby | ||
:en => { | ||
:simple => "Add to Kit", | ||
:browse => { | ||
:font_count_heading => "Showing %{font_count} fonts" | ||
} | ||
} | ||
``` | ||
|
||
As of version 0.0.3, `i18n-asf` uses the `locale` attribute of the top-level `asf` tag (if present) to determine the locale for a given set of strings, so all the strings included in the example code above would be assigned to the `:en_US` locale. | ||
|
||
If the `locale` attribute is not present, translations are assigned to a locale based on their filename (per the I18n library's default behavior), i.e. a file named `de.asf` will be assigned to the `:de` locale. | ||
|
||
In your Rails app you can refer to these translations by a scoped identifier, and take advantage of I18n features such as interpolation, pluralization, date and number conversion, and so on. | ||
|
||
```ruby | ||
# This method is aliased as `t()` in Rails ERb views | ||
I18n.translate("browse.font_count_heading", :count => 22) | ||
#=> "Showing 22 fonts" | ||
``` | ||
|
||
Calls to `translate` can take a `:scope` parameter, which can help DRY up your code: | ||
|
||
```ruby | ||
# These are all roughly equivalent | ||
I18n.translate("index.font_count_heading", :count => 22, :scope => "browse") | ||
I18n.translate("font_count_heading", :count => 22, :scope => "browse.index") | ||
I18n.translate("font_count_heading", :count => 22, :scope => ["browse", "index"]) | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
require "bundler/gem_tasks" | ||
|
||
require 'rspec/core' | ||
require 'rspec/core/rake_task' | ||
RSpec::Core::RakeTask.new(:spec) do |spec| | ||
spec.pattern = FileList['spec/**/*_spec.rb'] | ||
end | ||
|
||
GIT_REMOTES = %w(origin github) | ||
|
||
task :push_all do | ||
GIT_REMOTES.each do |remote| | ||
system "git push #{remote} master" | ||
system "git push #{remote} master --tags" | ||
end | ||
end | ||
|
||
task :default => :spec |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
# -*- encoding: utf-8 -*- | ||
$:.push File.expand_path("../lib", __FILE__) | ||
|
||
Gem::Specification.new do |s| | ||
s.name = "i18n-asf" | ||
s.version = "0.0.4" | ||
s.authors = ["David Demaree"] | ||
s.email = ["ddemaree@adobe.com"] | ||
s.homepage = "https://typekit.com/" | ||
s.summary = %q{Ruby I18n backend capable of reading Adobe Strings Format (ASF) XML files} | ||
s.description = %q{See above.} | ||
|
||
s.rubyforge_project = "i18n-asf" | ||
|
||
s.files = `git ls-files`.split("\n") | ||
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") | ||
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } | ||
s.require_paths = ["lib"] | ||
|
||
s.add_runtime_dependency "i18n" | ||
s.add_runtime_dependency "nokogiri" | ||
|
||
s.add_development_dependency "rake" | ||
s.add_development_dependency "rspec" | ||
s.add_development_dependency "fivemat" | ||
s.add_development_dependency "putsinator" | ||
s.add_development_dependency "awesome_print" | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
require 'i18n' | ||
require 'i18n/backend/asf' | ||
|
||
I18n::Backend::Simple.send(:include, I18n::Backend::ASF) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
require 'nokogiri' | ||
|
||
module I18n | ||
module ASF | ||
class Processor | ||
SKIP_TAG_NAMES = %w(dnt desc) | ||
|
||
def self.shared | ||
@processor ||= self.new | ||
end | ||
|
||
def self.process_document(document) | ||
self.shared.process_document(document) | ||
end | ||
|
||
def process_document(document, output={}) | ||
if document.is_a?(String) || document.respond_to?(:read) | ||
document = Nokogiri::XML(document) or raise "Document could not be parsed" | ||
end | ||
|
||
document.children.each do |node| | ||
next if SKIP_TAG_NAMES.include?(node.name) | ||
|
||
if node.name == 'str' | ||
key, value = process_string_node(node) | ||
output[key] = value | ||
elsif node.name == 'set' | ||
key = node[:name] or raise "Set missing name attribute at line #{string_node.line}" | ||
output[key] = process_document(node, {}) | ||
elsif node.name == 'asf' | ||
if _locale = node["locale"] | ||
output["_locale"] = _locale | ||
end | ||
|
||
process_document(node, output) | ||
end | ||
end | ||
|
||
output | ||
end | ||
|
||
def process_string_node(string_node) | ||
key = string_node[:name] or raise "Str missing name attribute at line #{string_node.line}" | ||
value = "" | ||
|
||
string_node.at("val").children.each do |node| | ||
value << process_content(node) | ||
next | ||
end | ||
|
||
[key, value] | ||
end | ||
|
||
def named_parameter(node) | ||
raise "Param tag missing `name' attribute at line #{node.line}" unless node[:name] | ||
raise "Param name #{node[:name].inspect} is too short" unless node[:name].length >= 1 | ||
"%{#{node[:name]}}" | ||
end | ||
|
||
def process_content(node, output="") | ||
current_tag = nil | ||
|
||
if node.content.nil? | ||
# Special case - likely HTML entity | ||
output << node.to_s | ||
elsif node.name == "param" | ||
output << named_parameter(node) | ||
elsif node.children.count == 0 | ||
output << node.content | ||
else | ||
# If element name isn't on the blacklist, turn it back to a string, | ||
# append its opening tag, and remember its name so the tag can be | ||
# closed later. | ||
unless SKIP_TAG_NAMES.include?(node.name) | ||
output << node.to_s.split(">").first + ">" | ||
current_tag = node.name | ||
end | ||
|
||
node.children.each do |part| | ||
if part.name == "param" | ||
output << named_parameter(part) | ||
elsif part.name == "dnt" | ||
process_content(part, output) | ||
elsif part.text? | ||
output << part.content | ||
else | ||
raise "Unhandled node type #{node} on line #{node.line}" | ||
end | ||
end | ||
|
||
if current_tag | ||
output << "</#{current_tag}>" | ||
current_tag = nil | ||
end | ||
end | ||
|
||
output | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
require 'i18n/asf/processor' | ||
|
||
module I18n | ||
module Backend | ||
module ASF | ||
|
||
protected | ||
|
||
def load_asf(filename) | ||
data = parse(filename) | ||
locale = data.delete("_locale") || get_locale_from_filename(filename) | ||
{ locale => data } | ||
end | ||
|
||
def parse(filename) | ||
::I18n::ASF::Processor.process_document File.read(filename) | ||
end | ||
|
||
def get_locale_from_filename(filename) | ||
::File.basename(filename, '.asf').to_sym | ||
end | ||
|
||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
<?xml version="1.0" encoding="utf-8" standalone="no" ?> | ||
<!DOCTYPE asf SYSTEM "http://ns.adobe.com/asf/asf_1_0.dtd"> | ||
<asf version="1.0" locale="en-US" xmlns="http://ns.adobe.com/asf"> | ||
<str name="simple"> | ||
<desc>Example of a simple string definition</desc> | ||
<val>Add to Kit</val> | ||
</str> | ||
|
||
<str name="with-html-entity"> | ||
<desc>String with HTML entity</desc> | ||
<val>Adobe® Photoshop® Lightroom™</val> | ||
</str> | ||
|
||
<str name="with-unicode"> | ||
<desc>String with Unicode characters</desc> | ||
<val>Adobe® Photoshop® Lightroom™</val> | ||
</str> | ||
|
||
<str name="with-interpolation"> | ||
<desc>Translated string with param interpolation</desc> | ||
<val>Invoice total: <param name="invoice_total" /></val> | ||
</str> | ||
|
||
<str name="with-dnt"> | ||
<desc>Translated string with portions that should not be translated</desc> | ||
<val>Copyright <param name="year" /> <dnt>Typekit</dnt></val> | ||
</str> | ||
|
||
<str name="untranslated" translate="no"> | ||
<desc>Untranslated string; should still interpolate params and strip dnts</desc> | ||
<val>Copyright ©<param name="year" /> <dnt>Typekit</dnt></val> | ||
</str> | ||
|
||
<str name="dnt-with-nested-param"> | ||
<desc>String with params nested inside dnt tag</desc> | ||
<val>Hello! <dnt>Typekit is <param name="awesome" />!</dnt></val> | ||
</str> | ||
|
||
<str name="embedded-html"> | ||
<desc>String with embedded HTML; this should probably be avoided if at all possible but also supported if possible</desc> | ||
<val>Your subscription is <b>Past Due since <param name="date" /></b></val> | ||
</str> | ||
|
||
<str name="encoded-html"> | ||
<desc>String with embedded HTML; this should probably be avoided if at all possible but also supported if possible</desc> | ||
<val>Your subscription is <b>Past Due since <param name="date" /></b></val> | ||
</str> | ||
|
||
<!-- ASF language features we don't plan to use, but which the parser must support --> | ||
|
||
<set name="backend"> | ||
<desc>Sample nested translation set</desc> | ||
<str name="nested-string"> | ||
<desc>Example of a nested string</desc> | ||
<val>I'm in the <dnt>backend</dnt></val> | ||
</str> | ||
</set> | ||
|
||
<str name="propsets"> | ||
<desc>String with properties</desc> | ||
<propset> | ||
<prop> | ||
<propkey>hello</propkey> | ||
<propvalue>world</propvalue> | ||
</prop> | ||
</propset> | ||
<val>Hello world!</val> | ||
</str> | ||
|
||
<str name="multi-value"> | ||
<desc>String with multiple values</desc> | ||
<val>Hello world!</val> | ||
<val accel="Cmd-Z" plat="mac">Goodnight Moon</val> | ||
<val plat="mystery_meat">Salutations Brother</val> | ||
</str> | ||
</asf> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
en: | ||
hello_yaml: "Hello YAML!" | ||
propsets: "From YAML" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
require "spec_helper" | ||
|
||
describe "Ruby I18N" do | ||
|
||
let(:fixtures_path) { Pathname.new(File.expand_path("../fixtures", __FILE__)) } | ||
|
||
before(:all) do | ||
I18n.load_path << fixtures_path.join("en.asf") | ||
I18n.load_path << fixtures_path.join("sample.yml") | ||
|
||
# The ASF file is en-US, YAML is just 'en'; ASF translations will have | ||
# precedence but YAML translations will be available as fallbacks. | ||
# In practice, ASF translations should be :en by default, with regional | ||
# variations (en-GB, en-AU) used as overrides to cover edge cases, though | ||
# wherever possible the standard English versions should be written so | ||
# as to be appropriate for all English-speaking regions. | ||
# U.S. spelling or semantic conventions, e.g. 'color' not 'colour', are | ||
# acceptable. | ||
I18n.default_locale = :en | ||
I18n.locale = :"en-US" | ||
end | ||
|
||
describe "loading ASF translations" do | ||
let(:translations) { I18n.backend.send(:translations) } | ||
|
||
before do | ||
I18n.backend.send(:init_translations) | ||
end | ||
|
||
it "reads locale from the ASF markup if present" do | ||
translations[:"en-US"][:simple].should == "Add to Kit" | ||
end | ||
end | ||
|
||
specify "using translations from ASF" do | ||
I18n.t("simple").should == "Add to Kit" | ||
I18n.t("with-interpolation", :invoice_total => "$49.99").should == "Invoice total: $49.99" | ||
I18n.t("backend.nested-string").should == "I'm in the backend" | ||
end | ||
|
||
describe "using ASF and YAML strings together" do | ||
it "can use YAML for fallbacks" do | ||
I18n.t("hello_yaml").should == "Hello YAML!" | ||
end | ||
it "prefers the current locale if the same string is present in both" do | ||
I18n.t("propsets").should == "Hello world!" | ||
end | ||
end | ||
end |
Oops, something went wrong.