harbor 0.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/Rakefile +76 -0
- data/bin/harbor +7 -0
- data/lib/harbor.rb +17 -0
- data/lib/harbor/accessor_injector.rb +30 -0
- data/lib/harbor/application.rb +172 -0
- data/lib/harbor/auth/basic.rb +51 -0
- data/lib/harbor/block_io.rb +63 -0
- data/lib/harbor/cache.rb +90 -0
- data/lib/harbor/cache/disk.rb +99 -0
- data/lib/harbor/cache/item.rb +48 -0
- data/lib/harbor/cache/memory.rb +35 -0
- data/lib/harbor/cascade.rb +75 -0
- data/lib/harbor/console.rb +34 -0
- data/lib/harbor/container.rb +134 -0
- data/lib/harbor/contrib/debug.rb +236 -0
- data/lib/harbor/contrib/session/data_mapper.rb +74 -0
- data/lib/harbor/daemon.rb +105 -0
- data/lib/harbor/errors.rb +49 -0
- data/lib/harbor/events.rb +45 -0
- data/lib/harbor/exception_notifier.rb +59 -0
- data/lib/harbor/file.rb +66 -0
- data/lib/harbor/file_store.rb +69 -0
- data/lib/harbor/file_store/file.rb +100 -0
- data/lib/harbor/file_store/local.rb +71 -0
- data/lib/harbor/file_store/mosso.rb +154 -0
- data/lib/harbor/file_store/mosso/private.rb +8 -0
- data/lib/harbor/generator.rb +56 -0
- data/lib/harbor/generator/help.rb +34 -0
- data/lib/harbor/generator/setup.rb +82 -0
- data/lib/harbor/generator/skeletons/basic/config.ru.skel +21 -0
- data/lib/harbor/generator/skeletons/basic/lib/appname.rb.skel +49 -0
- data/lib/harbor/generator/skeletons/basic/lib/appname/controllers/home.rb +9 -0
- data/lib/harbor/generator/skeletons/basic/lib/appname/views/home/index.html.erb.skel +23 -0
- data/lib/harbor/generator/skeletons/basic/lib/appname/views/layouts/application.html.erb.skel +48 -0
- data/lib/harbor/generator/skeletons/basic/lib/appname/views/layouts/exception.html.erb.skel +13 -0
- data/lib/harbor/generator/skeletons/basic/lib/appname/views/layouts/login.html.erb.skel +11 -0
- data/lib/harbor/generator/skeletons/basic/log/development.log +0 -0
- data/lib/harbor/hooks.rb +105 -0
- data/lib/harbor/json_cookies.rb +37 -0
- data/lib/harbor/layouts.rb +61 -0
- data/lib/harbor/locale.rb +50 -0
- data/lib/harbor/locales.txt +22 -0
- data/lib/harbor/logging.rb +39 -0
- data/lib/harbor/logging/appenders/email.rb +84 -0
- data/lib/harbor/logging/request_logger.rb +34 -0
- data/lib/harbor/mail_servers/abstract.rb +12 -0
- data/lib/harbor/mail_servers/sendmail.rb +19 -0
- data/lib/harbor/mail_servers/smtp.rb +25 -0
- data/lib/harbor/mail_servers/test.rb +17 -0
- data/lib/harbor/mailer.rb +96 -0
- data/lib/harbor/messages.rb +17 -0
- data/lib/harbor/mime.rb +206 -0
- data/lib/harbor/plugin.rb +52 -0
- data/lib/harbor/plugin_list.rb +38 -0
- data/lib/harbor/request.rb +138 -0
- data/lib/harbor/response.rb +281 -0
- data/lib/harbor/router.rb +112 -0
- data/lib/harbor/script.rb +155 -0
- data/lib/harbor/session.rb +75 -0
- data/lib/harbor/session/abstract.rb +27 -0
- data/lib/harbor/session/cookie.rb +17 -0
- data/lib/harbor/shellwords.rb +26 -0
- data/lib/harbor/test/mailer.rb +10 -0
- data/lib/harbor/test/request.rb +28 -0
- data/lib/harbor/test/response.rb +17 -0
- data/lib/harbor/test/session.rb +11 -0
- data/lib/harbor/test/test.rb +22 -0
- data/lib/harbor/version.rb +3 -0
- data/lib/harbor/view.rb +89 -0
- data/lib/harbor/view_context.rb +134 -0
- data/lib/harbor/view_context/helpers.rb +7 -0
- data/lib/harbor/view_context/helpers/cache.rb +77 -0
- data/lib/harbor/view_context/helpers/form.rb +34 -0
- data/lib/harbor/view_context/helpers/html.rb +26 -0
- data/lib/harbor/view_context/helpers/text.rb +120 -0
- data/lib/harbor/view_context/helpers/url.rb +11 -0
- data/lib/harbor/xml_view.rb +57 -0
- data/lib/harbor/zipped_io.rb +203 -0
- data/lib/vendor/cloudfiles-1.3.0/cloudfiles.rb +77 -0
- data/lib/vendor/cloudfiles-1.3.0/cloudfiles/authentication.rb +46 -0
- data/lib/vendor/cloudfiles-1.3.0/cloudfiles/connection.rb +280 -0
- data/lib/vendor/cloudfiles-1.3.0/cloudfiles/container.rb +260 -0
- data/lib/vendor/cloudfiles-1.3.0/cloudfiles/storage_object.rb +253 -0
- metadata +155 -0
@@ -0,0 +1,21 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
ENV["ENVIRONMENT"] ||= "development"
|
4
|
+
require "lib/##>=app_name<##"
|
5
|
+
|
6
|
+
services = Harbor::Container.new
|
7
|
+
# services.register("mailer", Harbor::Mailer)
|
8
|
+
# services.register("mail_server", Harbor::SendmailServer)
|
9
|
+
|
10
|
+
# DataMapper.setup :default, "postgres://localhost/##>=app_name<##"
|
11
|
+
|
12
|
+
if $0 == __FILE__
|
13
|
+
require "harbor/console"
|
14
|
+
Harbor::Console.start
|
15
|
+
else $0['thin']
|
16
|
+
run Harbor::Cascade.new(
|
17
|
+
ENV['ENVIRONMENT'],
|
18
|
+
services,
|
19
|
+
##>=app_class<##
|
20
|
+
)
|
21
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
|
3
|
+
gem "harbor"
|
4
|
+
require "harbor"
|
5
|
+
|
6
|
+
Harbor::View::path.unshift(Pathname(__FILE__).dirname + "##>=app_name<##" + "views")
|
7
|
+
Harbor::View::layouts.default("layouts/application")
|
8
|
+
|
9
|
+
class ##>=app_class<## < Harbor::Application
|
10
|
+
|
11
|
+
def self.routes(services)
|
12
|
+
Harbor::Router.new do
|
13
|
+
|
14
|
+
using services, Home do
|
15
|
+
get("/") { |home| home.index }
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
@@public_path = Pathname(__FILE__).dirname.parent.expand_path + "public"
|
22
|
+
def self.public_path=(value)
|
23
|
+
@@public_path = value
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.public_path
|
27
|
+
@@public_path
|
28
|
+
end
|
29
|
+
|
30
|
+
@@private_path = Pathname(__FILE__).dirname.parent.expand_path + "private"
|
31
|
+
def self.private_path=(value)
|
32
|
+
@@private_path = value
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.private_path
|
36
|
+
@@private_path
|
37
|
+
end
|
38
|
+
|
39
|
+
@@tmp_path = Pathname(__FILE__).dirname.parent.expand_path + "tmp"
|
40
|
+
def self.tmp_path=(value)
|
41
|
+
@@tmp_path = value
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.tmp_path
|
45
|
+
@@tmp_path
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
require Pathname(__FILE__).dirname + '##>=app_name<##/controllers/home'
|
@@ -0,0 +1,23 @@
|
|
1
|
+
<h1>Welcome to Harbor <span>You're all set to go!</span></h1>
|
2
|
+
|
3
|
+
<p class="feature">Congratulations on creating your first Harbor application! Now you should be ready to dive in, and here are some resources to get you started:</p>
|
4
|
+
|
5
|
+
<h2>What Am I?</h2>
|
6
|
+
<p>I'm just a default view to help you get started. I live in <kbd>lib/views/home/index.html.erb</kbd>, and am being rendered by <kbd>lib/controllers/home.rb</kbd>. And, of course, you're being routed here by <kbd>lib/##>=app_name<##.rb</kbd>.</p>
|
7
|
+
|
8
|
+
<p>That's pretty much the default setup for Harbor applications, but if you poke around you'll see you can set me up however you like.</p>
|
9
|
+
|
10
|
+
<h2>Documentation</h2>
|
11
|
+
<p>You can access the documentation for the latest stable version of harbor <a href="http://wiecklabs.com/harbor/doc">here</a>. We're also putting together a few tutorials which can be found on our <a href="http://wiecklabs.com/blog">blog</a>:</p>
|
12
|
+
<ul>
|
13
|
+
<li><a href="http://wiecklabs.com/blog/2009/10/21/starting-a-new-harbor-project">Starting a new Harbor project</a></li>
|
14
|
+
</ul>
|
15
|
+
|
16
|
+
<h2>Samples</h2>
|
17
|
+
<p>We've open sourced a few of our own harbor applications for you to use and explore. You can check them out on <a href="http://github.com/wiecklabs">github</a>.</p>
|
18
|
+
<ul>
|
19
|
+
<li><a href="http://github.com/wiecklabs/cleat">Cleat</a> - URL Shortener</li>
|
20
|
+
<li><a href="http://github.com/wiecklabs/northstar">Northstar</a> - Wiki</li>
|
21
|
+
<li><a href="http://github.com/wiecklabs/captains_blog">Captains Blog</a> - Blog</li>
|
22
|
+
<li><a href="http://github.com/wiecklabs/port_authority">Port Authority</a> - User Management and Authentication</li>
|
23
|
+
</ul>
|
@@ -0,0 +1,48 @@
|
|
1
|
+
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
|
2
|
+
"http://www.w3.org/TR/html4/strict.dtd">
|
3
|
+
<html>
|
4
|
+
<head>
|
5
|
+
<title>Harbor Framework</title>
|
6
|
+
<style type="text/css" media="screen">
|
7
|
+
body { background-color: #def; color: #002; font: 12px/2em "Lucida Grande"; margin: 20px 0 0 0; }
|
8
|
+
.body { background-color: #fff; width: 700px; margin: 0 auto 20px; border: 20px solid #fff; }
|
9
|
+
|
10
|
+
a { outline: none; }
|
11
|
+
|
12
|
+
h1 {
|
13
|
+
background-color: #57b;
|
14
|
+
color: #fff;
|
15
|
+
margin: 0 0 20px;
|
16
|
+
font-family: 'geneva';
|
17
|
+
font-size: 28px;
|
18
|
+
font-weight: normal;
|
19
|
+
padding: 20px 10px;
|
20
|
+
text-align: center;
|
21
|
+
text-transform: uppercase;
|
22
|
+
}
|
23
|
+
|
24
|
+
h1 span { display: block; color: #002; font-size: 18px; margin-top: 10px; text-transform: none; }
|
25
|
+
|
26
|
+
p.feature { font-size: 16px; }
|
27
|
+
|
28
|
+
h2 { color: #833; margin-bottom: 0; font-size: 16px; }
|
29
|
+
h3 { color: #833; margin-bottom: 0; }
|
30
|
+
a { color: #383; }
|
31
|
+
p { margin: 5px 0; }
|
32
|
+
ul { margin-top: 0; }
|
33
|
+
kbd { font-family: Monaco; background-color: #eee; padding: 2px; }
|
34
|
+
code { font-family: Monaco; background-color: #eee; padding: 2px; }
|
35
|
+
pre { font-family: Monaco; background-color: #002; color: #eee; padding: 10px; display: block; }
|
36
|
+
|
37
|
+
</style>
|
38
|
+
</head>
|
39
|
+
<body>
|
40
|
+
<div class="body">
|
41
|
+
|
42
|
+
<div id="content">
|
43
|
+
<%= @content %>
|
44
|
+
</div>
|
45
|
+
|
46
|
+
</div>
|
47
|
+
</body>
|
48
|
+
</html>
|
@@ -0,0 +1,13 @@
|
|
1
|
+
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
|
2
|
+
"http://www.w3.org/TR/html4/strict.dtd">
|
3
|
+
<html>
|
4
|
+
<head>
|
5
|
+
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
6
|
+
</head>
|
7
|
+
<body>
|
8
|
+
<div id="content">
|
9
|
+
<h1>##>=app_name.capitalize<##</h1>
|
10
|
+
<%= @content %>
|
11
|
+
</div>
|
12
|
+
</body>
|
13
|
+
</html>
|
@@ -0,0 +1,11 @@
|
|
1
|
+
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
|
2
|
+
"http://www.w3.org/TR/html4/strict.dtd">
|
3
|
+
<html>
|
4
|
+
<head>
|
5
|
+
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
6
|
+
</head>
|
7
|
+
<body>
|
8
|
+
<h1>##>=app_name.capitalize<##</h1>
|
9
|
+
<%= @content %>
|
10
|
+
</body>
|
11
|
+
</html>
|
File without changes
|
data/lib/harbor/hooks.rb
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
module Harbor
|
2
|
+
module Hooks
|
3
|
+
|
4
|
+
def self.included(target)
|
5
|
+
target.extend(ClassMethods)
|
6
|
+
|
7
|
+
target.class_eval do
|
8
|
+
@__harbor_hooked_method_added = method(:method_added) if respond_to?(:method_added)
|
9
|
+
def self.method_added(method)
|
10
|
+
if !@__harbor_binding_method && hooks.has_key?(method)
|
11
|
+
chain = hooks[method]
|
12
|
+
chain.bind!
|
13
|
+
end
|
14
|
+
|
15
|
+
@__harbor_hooked_method_added.call(method) if @__harbor_hooked_method_added
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
class Map
|
22
|
+
def initialize(target)
|
23
|
+
@map = {}
|
24
|
+
@target = target
|
25
|
+
end
|
26
|
+
|
27
|
+
def has_key?(method_name)
|
28
|
+
@map.has_key?(method_name)
|
29
|
+
end
|
30
|
+
|
31
|
+
def [](method_name)
|
32
|
+
@map[method_name] ||= Chain.new(@target, method_name)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class Chain
|
37
|
+
def initialize(target, method_name)
|
38
|
+
@target = target
|
39
|
+
@method_name = method_name
|
40
|
+
@before = []
|
41
|
+
@after = []
|
42
|
+
|
43
|
+
bind! if target.instance_methods.include?(method_name.to_s)
|
44
|
+
end
|
45
|
+
|
46
|
+
def before(block)
|
47
|
+
@before << block
|
48
|
+
end
|
49
|
+
|
50
|
+
def after(block)
|
51
|
+
@after << block
|
52
|
+
end
|
53
|
+
|
54
|
+
def call(instance, args, blk = nil)
|
55
|
+
result = nil
|
56
|
+
|
57
|
+
catch(:halt) do
|
58
|
+
@before.each do |block|
|
59
|
+
block.call instance
|
60
|
+
end
|
61
|
+
|
62
|
+
result = instance.send("__hooked_#{@method_name}", *args, &blk)
|
63
|
+
|
64
|
+
@after.each do |block|
|
65
|
+
block.call instance
|
66
|
+
end
|
67
|
+
|
68
|
+
result
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.bind!(target, method_name)
|
73
|
+
target.send(:alias_method, "__hooked_#{method_name}", method_name)
|
74
|
+
|
75
|
+
target.send(:class_eval, <<-EOS)
|
76
|
+
instance_variable_set(:@__harbor_binding_method, true)
|
77
|
+
def #{method_name}(*args, &block)
|
78
|
+
self.class.hooks[#{method_name.inspect}].call(self, args, block)
|
79
|
+
end
|
80
|
+
remove_instance_variable(:@__harbor_binding_method)
|
81
|
+
EOS
|
82
|
+
end
|
83
|
+
|
84
|
+
def bind!
|
85
|
+
self.class.bind!(@target, @method_name)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
module ClassMethods
|
90
|
+
|
91
|
+
def hooks
|
92
|
+
@hooks ||= Map.new(self)
|
93
|
+
end
|
94
|
+
|
95
|
+
def before(method_name, &block)
|
96
|
+
hooks[method_name].before(block)
|
97
|
+
end
|
98
|
+
|
99
|
+
def after(method_name, &block)
|
100
|
+
hooks[method_name].after(block)
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Harbor
|
2
|
+
##
|
3
|
+
# Middleware for storing data in a cookie using JSON, for later access
|
4
|
+
# via javascript.
|
5
|
+
#
|
6
|
+
# use Harbor::JsonCookies, "key_to_serialize", "other_key"
|
7
|
+
##
|
8
|
+
class JsonCookies
|
9
|
+
def initialize(app, keys = [])
|
10
|
+
@app = app
|
11
|
+
@keys = keys
|
12
|
+
end
|
13
|
+
|
14
|
+
def call(env)
|
15
|
+
read_cookies(env)
|
16
|
+
status, headers, body = @app.call(env)
|
17
|
+
write_cookies(env, status, headers, body)
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def read_cookies(env)
|
23
|
+
request = Rack::Request.new(env)
|
24
|
+
(request.cookies.keys & @keys).each do |key|
|
25
|
+
env[key] = JSON.parse(request.cookies[key]) rescue nil
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def write_cookies(env, status, headers, body)
|
30
|
+
response = Rack::Response.new(body, status, headers)
|
31
|
+
@keys.each do |key|
|
32
|
+
response.set_cookie(key, :value => env[key].to_json, :path => "/")
|
33
|
+
end
|
34
|
+
response.to_a
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Harbor
|
2
|
+
class Layouts
|
3
|
+
|
4
|
+
include Enumerable
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@map = []
|
8
|
+
@default = nil
|
9
|
+
end
|
10
|
+
|
11
|
+
def map(fragment, layout)
|
12
|
+
fragment = fragment.squeeze("*").squeeze("/").sub(%r{/$}, "").gsub("*", ".*")
|
13
|
+
specificity = fragment_specificity(fragment)
|
14
|
+
|
15
|
+
regexp = Regexp.new("^#{fragment}")
|
16
|
+
|
17
|
+
if previous = @map.assoc(regexp)
|
18
|
+
@map[@map.index(previous)] = [regexp, layout, specificity]
|
19
|
+
else
|
20
|
+
@map << [regexp, layout, specificity]
|
21
|
+
end
|
22
|
+
|
23
|
+
sort!
|
24
|
+
@map
|
25
|
+
end
|
26
|
+
|
27
|
+
def default(layout)
|
28
|
+
@default = layout
|
29
|
+
end
|
30
|
+
|
31
|
+
def each
|
32
|
+
@map.each { |item| yield item }
|
33
|
+
end
|
34
|
+
|
35
|
+
def sort!
|
36
|
+
@map.sort! do |a, b|
|
37
|
+
b[2] <=> a[2]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def clear
|
42
|
+
@default = nil
|
43
|
+
@map.clear
|
44
|
+
end
|
45
|
+
|
46
|
+
def match(path)
|
47
|
+
@map.each do |fragment, layout|
|
48
|
+
return layout if fragment === path
|
49
|
+
end
|
50
|
+
|
51
|
+
return @default
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def fragment_specificity(fragment)
|
57
|
+
fragment.count("/") - fragment.count("*")
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Harbor
|
2
|
+
class Locale
|
3
|
+
|
4
|
+
def self.[](culture_code)
|
5
|
+
unless @locales
|
6
|
+
@locales = {}
|
7
|
+
|
8
|
+
::File.read(Pathname(__FILE__).dirname + "locales.txt").split("\n").each do |line|
|
9
|
+
next if line =~ /^\s*(\#.*)?$/
|
10
|
+
values = line.split(/\|/).map { |value| value.strip }
|
11
|
+
@locales[values[1]] = Locale.new(values[1], values[0], values[2])
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
@locales[culture_code]
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.active_locales
|
19
|
+
@active_locales ||= []
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.activate!(*culture_codes)
|
23
|
+
@active_locales = culture_codes.map { |culture_code| self[culture_code] }
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.default
|
27
|
+
@default ||= self[default_culture_code]
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.default_culture_code
|
31
|
+
@default_culture_code ||= "en-US"
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.default_culture_code=(value)
|
35
|
+
@default_culture_code = value
|
36
|
+
end
|
37
|
+
|
38
|
+
attr_reader :culture_code, :abbreviation, :description
|
39
|
+
|
40
|
+
def initialize(culture_code, abbreviation, description)
|
41
|
+
@culture_code = culture_code
|
42
|
+
@abbreviation = abbreviation
|
43
|
+
@description = description
|
44
|
+
end
|
45
|
+
|
46
|
+
def to_s
|
47
|
+
@description
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# This is a pipe-delimited file with values of
|
2
|
+
# LanguageAbbreviation|CultureCode|Description
|
3
|
+
|
4
|
+
US|en-US|English (US)
|
5
|
+
AU|en-AU|English (AU)
|
6
|
+
UK|en-GB|English (UK)
|
7
|
+
JP|ja|Japanese
|
8
|
+
IN|hi-IN|Hindi (IN)
|
9
|
+
KO|ko-KR|Korean (KO)
|
10
|
+
MY|ms-MY|Malay (MY)
|
11
|
+
TH|th-TH|Thai (TH)
|
12
|
+
PK|ur-PK|Urdu (PK)
|
13
|
+
VN|vi|Vietnamese
|
14
|
+
ID|id-ID|Indonesian (ID)
|
15
|
+
HK|zh-HK|Chinese (HK)
|
16
|
+
CN|zh-CN|Chinese (CN)
|
17
|
+
FR|fr-FR|French (FR)
|
18
|
+
DE|de-DE|German (DE)
|
19
|
+
IT|it-IT|Italian (IT)
|
20
|
+
SP|es-ES|Spanish (SP)
|
21
|
+
PT|pt|Portuguese
|
22
|
+
|