lingara-apps 0.1.0.pre.alpha.7 → 0.1.0.pre.alpha.8
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.
- checksums.yaml +4 -4
- data/README.md +41 -0
- data/lib/lingara/apps/i18n.rb +158 -0
- data/lib/lingara/apps/listing.rb +95 -0
- data/lib/lingara/apps/manifest.rb +56 -8
- data/lib/lingara/apps/version.rb +2 -2
- data/lib/lingara/apps.rb +2 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fae6d6a2fd0f2ae63a10dfff6f337314be85565d58879160eb6b82b253cf072f
|
|
4
|
+
data.tar.gz: 7d10086620a5c492ed5c0c1f4afe2053424e0025dd6a55d8feeb02eaa336fc27
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e45e022cd0ee72996eed2809a5326057c3134fed1daad61eb172866dd49717e5e7e4f53209c02fbc3e13c9680ac7cac03dd791c91956c52be939d9b4f9d8c4d7
|
|
7
|
+
data.tar.gz: 9b20d6bd685e927ce7e8ace71ec76a6b45ee9a06ffd9eaacc2bb44a5457f468245f6038907794aa3558e499148709ae71c3883e1f15e607025e646222554b3d5
|
data/README.md
CHANGED
|
@@ -101,6 +101,47 @@ you want one; the kit never calls it for you.
|
|
|
101
101
|
breaks, and `to_json` writes `manifest.json`. A bare `name("…")` is the name
|
|
102
102
|
in the default locale.
|
|
103
103
|
|
|
104
|
+
The listing can also carry `.long_description(…)`,
|
|
105
|
+
`.developer(name:, url: nil, support_url: nil)` and
|
|
106
|
+
`.image(sha256, "hero" | "screenshot", alt)`. An image is uploaded in the
|
|
107
|
+
console or the CLI first and named here by `Lingara::Apps.image_sha256(bytes)`.
|
|
108
|
+
These fields need a Lingara that knows them (ADR 16.10.26ah); a manifest that
|
|
109
|
+
sets none of them is unaffected.
|
|
110
|
+
|
|
111
|
+
## Localising your card
|
|
112
|
+
|
|
113
|
+
Every request carries the learner's `locale`. Build one catalogue at startup
|
|
114
|
+
and look strings up per request:
|
|
115
|
+
|
|
116
|
+
```ruby
|
|
117
|
+
MESSAGES = Lingara::Apps::Messages.new(default_locale: "en", catalogue: {
|
|
118
|
+
"en" => {"heading" => "Today's five", "done" => "Done ({count})"},
|
|
119
|
+
"zh-Hans" => {"heading" => "今日五词"}
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
t = MESSAGES.for(request.locale)
|
|
123
|
+
Lingara::Apps.card.heading(t.t("heading"), 1).button(t.t("done", "count" => "3"), "done").build
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
- `Messages.new` raises `Lingara::Apps::MessagesError` at startup for a broken
|
|
127
|
+
catalogue, its `code` one of `:locale_invalid`, `:locale_duplicate`,
|
|
128
|
+
`:default_locale_missing`, `:key_missing_in_default`, never mid-render.
|
|
129
|
+
- The locale is chosen by `Lingara::Apps.pick_locale`: an exact match, then
|
|
130
|
+
the request with its last subtag dropped, then the default. `zh-Hans` never
|
|
131
|
+
falls to `zh-Hant`; key your catalogue by Lingara's spellings (bare
|
|
132
|
+
subtags, plus `zh-Hans` and `zh-Hant`).
|
|
133
|
+
- A key missing in the chosen locale falls back to the default's text, and an
|
|
134
|
+
unknown key returns the key, so a half-translated catalogue still renders a
|
|
135
|
+
whole card. `{name}` placeholders take String parameters; format numbers
|
|
136
|
+
and dates yourself with `t.locale`.
|
|
137
|
+
- Never choose the UI language from the `languages` context slice: that is
|
|
138
|
+
the learner's study pair.
|
|
139
|
+
|
|
140
|
+
A listing in one language only builds, with a warning: `builder.warnings`
|
|
141
|
+
(or `Lingara::Apps.manifest_warnings(json)`) is `[:single_locale]` when
|
|
142
|
+
`name` and `description` share no locale beyond the default. The kit never
|
|
143
|
+
prints it.
|
|
144
|
+
|
|
104
145
|
## Things to know
|
|
105
146
|
|
|
106
147
|
- **The user agent.** Lingara's requests carry
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lingara
|
|
4
|
+
module Apps
|
|
5
|
+
# Localising a card (ADR 16.10.26aj, contract AK6): one locale pick, a
|
|
6
|
+
# message catalogue checked when built and quiet when used, and the
|
|
7
|
+
# listing's derived locales and warnings. Every answer here is pinned by
|
|
8
|
+
# conformance/vectors/locales.json and manifest-warnings.json, so the
|
|
9
|
+
# seven kits agree. Nothing here prints or logs: that is the caller's.
|
|
10
|
+
|
|
11
|
+
# A catalogue Messages.new refuses, at startup rather than mid-render.
|
|
12
|
+
# #code is one of CODES, in the order they are checked.
|
|
13
|
+
class MessagesError < StandardError
|
|
14
|
+
CODES = %i[locale_invalid locale_duplicate default_locale_missing key_missing_in_default].freeze
|
|
15
|
+
|
|
16
|
+
attr_reader :code
|
|
17
|
+
|
|
18
|
+
def initialize(code)
|
|
19
|
+
@code = code
|
|
20
|
+
super("the message catalogue is refused: #{code}")
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# A message catalogue: locale => key => text, checked when built.
|
|
25
|
+
#
|
|
26
|
+
# messages = Lingara::Apps::Messages.new(default_locale: "en", catalogue: {
|
|
27
|
+
# "en" => {"greeting" => "Hello, {name}", "bye" => "Goodbye"},
|
|
28
|
+
# "ja" => {"greeting" => "こんにちは、{name}"}
|
|
29
|
+
# })
|
|
30
|
+
# t = messages.for(request.locale)
|
|
31
|
+
# t.t("greeting", "name" => "Ana") # also t.call(…) or t.(…)
|
|
32
|
+
# t.locale # the chosen locale, safe for a text element's lang
|
|
33
|
+
#
|
|
34
|
+
# Locales and keys may be Strings or Symbols; both are read as Strings.
|
|
35
|
+
class Messages
|
|
36
|
+
# One locale's lookup. A key missing in the chosen locale falls to the
|
|
37
|
+
# default locale's text; an unknown key returns the key itself. Never
|
|
38
|
+
# raises.
|
|
39
|
+
class Lookup
|
|
40
|
+
PLACEHOLDER = /\{([A-Za-z0-9_]+)\}/
|
|
41
|
+
|
|
42
|
+
# The chosen locale, safe for a text element's `lang`.
|
|
43
|
+
attr_reader :locale
|
|
44
|
+
|
|
45
|
+
def initialize(locale, table, fallback)
|
|
46
|
+
@locale = locale
|
|
47
|
+
@table = table
|
|
48
|
+
@fallback = fallback
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# The text for +key+, its `{name}` placeholders replaced in one pass by
|
|
52
|
+
# the String parameter of that name. An unknown or unsupplied one, or
|
|
53
|
+
# one whose value is not a String, stays literal: format numbers and
|
|
54
|
+
# dates yourself, with #locale.
|
|
55
|
+
def t(key, params = {})
|
|
56
|
+
key = key.to_s
|
|
57
|
+
text = @table.fetch(key) { @fallback.fetch(key, key) }
|
|
58
|
+
values = params.to_h.transform_keys(&:to_s)
|
|
59
|
+
text.gsub(PLACEHOLDER) do |whole|
|
|
60
|
+
value = values[Regexp.last_match(1)]
|
|
61
|
+
value.is_a?(String) ? value : whole
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
alias_method :call, :t
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
attr_reader :default_locale
|
|
69
|
+
|
|
70
|
+
# Raises MessagesError for a broken catalogue.
|
|
71
|
+
def initialize(default_locale:, catalogue:)
|
|
72
|
+
@default_locale = default_locale.to_s
|
|
73
|
+
@catalogue = catalogue.to_h.to_h do |locale, table|
|
|
74
|
+
[locale.to_s, table.to_h.to_h { |key, text| [key.to_s, text.to_s] }]
|
|
75
|
+
end
|
|
76
|
+
code = refusal
|
|
77
|
+
raise MessagesError, code if code
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# The catalogue's locales, in its own order and spelling.
|
|
81
|
+
def locales = @catalogue.keys
|
|
82
|
+
|
|
83
|
+
# The lookup for a request's `locale`, picked over the catalogue's locales.
|
|
84
|
+
def for(requested)
|
|
85
|
+
locale = Apps.pick_locale(requested, locales, @default_locale)
|
|
86
|
+
Lookup.new(locale, @catalogue.fetch(locale), @catalogue.fetch(@default_locale))
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
def refusal
|
|
92
|
+
return :locale_invalid unless (locales + [@default_locale]).all? { |l| l.match?(Limits::LANG_PATTERN) }
|
|
93
|
+
return :locale_duplicate if locales.map { |l| I18n.normal(l) }.uniq.size != locales.size
|
|
94
|
+
fallback = @catalogue[@default_locale]
|
|
95
|
+
return :default_locale_missing if fallback.nil?
|
|
96
|
+
:key_missing_in_default unless @catalogue.each_value.all? { |table| table.keys.all? { |k| fallback.key?(k) } }
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# The helpers' shared parts.
|
|
101
|
+
module I18n
|
|
102
|
+
module_function
|
|
103
|
+
|
|
104
|
+
# A locale as the pick compares it: `_` read as `-`, case-folded.
|
|
105
|
+
def normal(locale)
|
|
106
|
+
locale.tr("_", "-").downcase
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def keys(value)
|
|
110
|
+
value.is_a?(Hash) ? value.keys : []
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# The closed list of manifest warning codes; one member in v1.
|
|
115
|
+
MANIFEST_WARNINGS = %i[single_locale].freeze
|
|
116
|
+
|
|
117
|
+
module_function
|
|
118
|
+
|
|
119
|
+
# The available locale for +requested+, in its available spelling: an
|
|
120
|
+
# exact match (case-insensitive, `_` read as `-`), then +requested+ with
|
|
121
|
+
# its last subtag dropped, repeated, then +default_locale+. The chain
|
|
122
|
+
# never widens: `pt` does not match `pt-BR`, and `zh-Hans` never reaches
|
|
123
|
+
# `zh-Hant`. Never raises; a nil, empty or non-String +requested+ is the
|
|
124
|
+
# default.
|
|
125
|
+
def pick_locale(requested, available, default_locale)
|
|
126
|
+
return default_locale unless requested.is_a?(String) && !requested.empty?
|
|
127
|
+
spelled = {}
|
|
128
|
+
available.each { |locale| spelled[I18n.normal(locale)] ||= locale }
|
|
129
|
+
candidate = I18n.normal(requested)
|
|
130
|
+
loop do
|
|
131
|
+
return spelled[candidate] if spelled.key?(candidate)
|
|
132
|
+
cut = candidate.rindex("-")
|
|
133
|
+
return default_locale if cut.nil?
|
|
134
|
+
candidate = candidate[0, cut]
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# The locales the listing is translated into: where `name` and
|
|
139
|
+
# `description` both have an entry, and `long_description` too when
|
|
140
|
+
# present and not nil, sorted by byte order. [] for a manifest
|
|
141
|
+
# validate_manifest refuses.
|
|
142
|
+
def manifest_locales(manifest)
|
|
143
|
+
return [] if ManifestRules.first_broken(manifest)
|
|
144
|
+
long = manifest["long_description"]
|
|
145
|
+
I18n.keys(manifest["name"]).select do |locale|
|
|
146
|
+
I18n.keys(manifest["description"]).include?(locale) && (long.nil? || I18n.keys(long).include?(locale))
|
|
147
|
+
end.sort_by(&:b)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# What the listing could do better, never a refusal: [:single_locale]
|
|
151
|
+
# when it is translated into its default locale alone. [] for a refused
|
|
152
|
+
# manifest.
|
|
153
|
+
def manifest_warnings(manifest)
|
|
154
|
+
locales = manifest_locales(manifest)
|
|
155
|
+
(locales.size == 1 && locales.first == manifest["default_locale"]) ? [:single_locale] : []
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
module Lingara
|
|
6
|
+
module Apps
|
|
7
|
+
# The listing's three optional manifest fields (ADR 16.10.26aj D4), each
|
|
8
|
+
# rule mirroring Backend ADR 16.10.26ah D2–D5: `long_description`,
|
|
9
|
+
# `developer` and `images`, and the `locales` key a manifest never
|
|
10
|
+
# declares. A nil long_description, developer, url or support_url reads
|
|
11
|
+
# as absent; a nil images is refused.
|
|
12
|
+
module ListingRules
|
|
13
|
+
LONG_DESCRIPTION_MAX_CHARS = 1500
|
|
14
|
+
DEVELOPER_NAME_MAX_CHARS = 60
|
|
15
|
+
IMAGE_ALT_MAX_CHARS = 140
|
|
16
|
+
MAX_HERO_IMAGES = 1
|
|
17
|
+
MAX_SCREENSHOTS = 5
|
|
18
|
+
IMAGE_ROLES = %w[hero screenshot].freeze
|
|
19
|
+
|
|
20
|
+
# Zero-width characters, beyond the card rules' controls and bidi marks.
|
|
21
|
+
ZERO_WIDTH = /[-]/
|
|
22
|
+
SHA256_HEX = /\A[0-9a-f]{64}\z/
|
|
23
|
+
|
|
24
|
+
module_function
|
|
25
|
+
|
|
26
|
+
# One text: 1..max scalar values after trimming, none of the name
|
|
27
|
+
# rule's characters (`\n` aside when +newlines+, which also refuses a
|
|
28
|
+
# run of three `\n` in the untrimmed value).
|
|
29
|
+
def plain_text?(value, max, newlines = false)
|
|
30
|
+
return false unless value.is_a?(String)
|
|
31
|
+
trimmed = Limits.trim(value)
|
|
32
|
+
return false if newlines && value.include?("\n\n\n")
|
|
33
|
+
trimmed.length.between?(1, max) && !Limits.control?(trimmed, newlines) && !trimmed.match?(ZERO_WIDTH)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# A non-empty locale map whose every value is plain_text?.
|
|
37
|
+
def localized?(value, max, newlines = false)
|
|
38
|
+
value.is_a?(Hash) && !value.empty? && value.each_value.all? { |v| plain_text?(v, max, newlines) }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# A locale map holding +default_locale+, as AE-1 requires of the listing's maps.
|
|
42
|
+
def localized_with_default?(value, default_locale, max, newlines = false)
|
|
43
|
+
localized?(value, max, newlines) && default_locale.is_a?(String) && value.key?(default_locale)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def long_description?(m)
|
|
47
|
+
value = m["long_description"]
|
|
48
|
+
value.nil? || localized_with_default?(value, m["default_locale"], LONG_DESCRIPTION_MAX_CHARS, true)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def developer?(m)
|
|
52
|
+
value = m["developer"]
|
|
53
|
+
return true if value.nil?
|
|
54
|
+
return false unless value.is_a?(Hash) && plain_text?(value["name"], DEVELOPER_NAME_MAX_CHARS)
|
|
55
|
+
[value["url"], value["support_url"]].all? { |u| u.nil? || (u.is_a?(String) && Limits.safe_link?(u)) }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def image?(entry, default_locale)
|
|
59
|
+
entry.is_a?(Hash) && entry["sha256"].is_a?(String) && entry["sha256"].match?(SHA256_HEX) &&
|
|
60
|
+
IMAGE_ROLES.include?(entry["role"]) && localized_with_default?(entry["alt"], default_locale, IMAGE_ALT_MAX_CHARS)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def images?(m)
|
|
64
|
+
return true unless m.key?("images")
|
|
65
|
+
value = m["images"]
|
|
66
|
+
return false unless value.is_a?(Array) && value.all? { |e| image?(e, m["default_locale"]) }
|
|
67
|
+
roles = value.map { |e| e["role"] }
|
|
68
|
+
roles.count("hero") <= MAX_HERO_IMAGES && roles.count("screenshot") <= MAX_SCREENSHOTS &&
|
|
69
|
+
value.map { |e| e["sha256"] }.uniq.size == value.size
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# `locales` is derived by Lingara, so a manifest that declares it is refused.
|
|
73
|
+
def locales_absent?(m)
|
|
74
|
+
!m.key?("locales")
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# The listing keys the stored manifest carries when set: nil and an
|
|
78
|
+
# empty images are absent.
|
|
79
|
+
def stored(m)
|
|
80
|
+
out = {}
|
|
81
|
+
%w[long_description developer].each { |key| out[key] = m[key] unless m[key].nil? }
|
|
82
|
+
out["images"] = m["images"] if m["images"].is_a?(Array) && !m["images"].empty?
|
|
83
|
+
out
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
module_function
|
|
88
|
+
|
|
89
|
+
# The lowercase hex SHA-256 of an image's bytes (a String, read as
|
|
90
|
+
# binary), the hash its upload is stored under.
|
|
91
|
+
def image_sha256(bytes)
|
|
92
|
+
Digest::SHA256.hexdigest(bytes)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -25,7 +25,8 @@ module Lingara
|
|
|
25
25
|
DESCRIPTION_MAX_CHARS = 280
|
|
26
26
|
STORED_MAX_BYTES = 65_536
|
|
27
27
|
|
|
28
|
-
RULES = %i[manifest_version default_locale name description render_url slots context
|
|
28
|
+
RULES = %i[manifest_version default_locale name description render_url slots context long_description developer
|
|
29
|
+
images locales duplicate too_large].freeze
|
|
29
30
|
|
|
30
31
|
# The slots a manifest may name: the generated AppSlotName's values,
|
|
31
32
|
# without the generator's unknown-value placeholder.
|
|
@@ -65,13 +66,14 @@ module Lingara
|
|
|
65
66
|
%w[slots context scopes].any? { |k| m[k].is_a?(Array) && m[k].uniq.size != m[k].size }
|
|
66
67
|
end
|
|
67
68
|
|
|
68
|
-
# The compact UTF-8 JSON of the seven stored keys, defaults filled
|
|
69
|
+
# The compact UTF-8 JSON of the seven stored keys, defaults filled, and
|
|
70
|
+
# the listing's keys when set.
|
|
69
71
|
def stored_bytes(m)
|
|
70
72
|
stored = {
|
|
71
73
|
"default_locale" => m["default_locale"], "name" => m["name"], "description" => m["description"],
|
|
72
74
|
"slots" => m["slots"], "context" => m["context"] || [], "scopes" => m["scopes"] || [],
|
|
73
75
|
"tutor_note" => m["tutor_note"].nil? ? false : m["tutor_note"]
|
|
74
|
-
}
|
|
76
|
+
}.merge(ListingRules.stored(m))
|
|
75
77
|
JSON.generate(stored).bytesize
|
|
76
78
|
end
|
|
77
79
|
|
|
@@ -83,6 +85,10 @@ module Lingara
|
|
|
83
85
|
render_url: ->(m) { m["render_url"].is_a?(String) && m["render_url"].match?(RENDER_URL) },
|
|
84
86
|
slots: ->(m) { within?(m, "slots", SLOT_NAMES, false) },
|
|
85
87
|
context: ->(m) { within?(m, "context", CONTEXT_SLICE_KINDS, true) },
|
|
88
|
+
long_description: ->(m) { ListingRules.long_description?(m) },
|
|
89
|
+
developer: ->(m) { ListingRules.developer?(m) },
|
|
90
|
+
images: ->(m) { ListingRules.images?(m) },
|
|
91
|
+
locales: ->(m) { ListingRules.locales_absent?(m) },
|
|
86
92
|
duplicate: ->(m) { !duplicate?(m) },
|
|
87
93
|
too_large: ->(m) { stored_bytes(m) <= STORED_MAX_BYTES }
|
|
88
94
|
}.freeze
|
|
@@ -110,6 +116,9 @@ module Lingara
|
|
|
110
116
|
@scopes = []
|
|
111
117
|
@tutor_note = false
|
|
112
118
|
@listed = false
|
|
119
|
+
@long_description = nil
|
|
120
|
+
@developer = nil
|
|
121
|
+
@images = []
|
|
113
122
|
end
|
|
114
123
|
|
|
115
124
|
def default_locale(locale) = set(:@default_locale, locale)
|
|
@@ -136,17 +145,38 @@ module Lingara
|
|
|
136
145
|
# Whether the app appears in the catalogue; false unless set.
|
|
137
146
|
def listed(listed = true) = set(:@listed, listed)
|
|
138
147
|
|
|
148
|
+
# The listing's longer text: plain, paragraphs split on blank lines. A
|
|
149
|
+
# locale map, or a bare String meaning the default locale's.
|
|
150
|
+
def long_description(text) = set(:@long_description, text)
|
|
151
|
+
|
|
152
|
+
# Who made the app, shown on its listing as the developer's own claim.
|
|
153
|
+
def developer(name:, url: nil, support_url: nil)
|
|
154
|
+
set(:@developer, {"name" => name, "url" => url, "support_url" => support_url}.compact)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# An image already uploaded in the console or the CLI, named by
|
|
158
|
+
# Lingara::Apps.image_sha256 of its bytes; +role+ is "hero" or
|
|
159
|
+
# "screenshot", +alt+ a locale map or a bare String.
|
|
160
|
+
def image(sha256, role, alt)
|
|
161
|
+
@images << [sha256, role.to_s, alt]
|
|
162
|
+
self
|
|
163
|
+
end
|
|
164
|
+
|
|
139
165
|
# The manifest, or ManifestError naming the first rule it breaks.
|
|
140
166
|
def build
|
|
141
|
-
manifest =
|
|
142
|
-
"manifest_version" => 1, "default_locale" => @default_locale, "name" => localized(@name),
|
|
143
|
-
"description" => localized(@description), "render_url" => @render_url, "slots" => @slots.dup,
|
|
144
|
-
"context" => @context.dup, "scopes" => @scopes.dup, "tutor_note" => @tutor_note, "listed" => @listed
|
|
145
|
-
}
|
|
167
|
+
manifest = wire
|
|
146
168
|
Apps.validate_manifest(manifest)
|
|
147
169
|
manifest
|
|
148
170
|
end
|
|
149
171
|
|
|
172
|
+
# What the listing could do better, never a refusal; [] while the
|
|
173
|
+
# manifest would not build. Never raises.
|
|
174
|
+
def warnings = Apps.manifest_warnings(wire)
|
|
175
|
+
|
|
176
|
+
# The locales the listing is translated into; [] while the manifest
|
|
177
|
+
# would not build.
|
|
178
|
+
def locales = Apps.manifest_locales(wire)
|
|
179
|
+
|
|
150
180
|
# The validated manifest as the JSON the console accepts as an upload.
|
|
151
181
|
def to_json(*)
|
|
152
182
|
"#{JSON.pretty_generate(build)}\n"
|
|
@@ -154,6 +184,24 @@ module Lingara
|
|
|
154
184
|
|
|
155
185
|
private
|
|
156
186
|
|
|
187
|
+
def wire
|
|
188
|
+
{
|
|
189
|
+
"manifest_version" => 1, "default_locale" => @default_locale, "name" => localized(@name),
|
|
190
|
+
"description" => localized(@description), "render_url" => @render_url, "slots" => @slots.dup,
|
|
191
|
+
"context" => @context.dup, "scopes" => @scopes.dup, "tutor_note" => @tutor_note, "listed" => @listed
|
|
192
|
+
}.merge(listing)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def listing
|
|
196
|
+
out = {}
|
|
197
|
+
out["long_description"] = localized(@long_description) unless @long_description.nil?
|
|
198
|
+
out["developer"] = @developer.dup unless @developer.nil?
|
|
199
|
+
unless @images.empty?
|
|
200
|
+
out["images"] = @images.map { |sha, role, alt| {"sha256" => sha, "role" => role, "alt" => localized(alt)} }
|
|
201
|
+
end
|
|
202
|
+
out
|
|
203
|
+
end
|
|
204
|
+
|
|
157
205
|
def set(ivar, value)
|
|
158
206
|
instance_variable_set(ivar, value)
|
|
159
207
|
self
|
data/lib/lingara/apps/version.rb
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
module Lingara
|
|
4
4
|
module Apps
|
|
5
5
|
# This kit's released version, SemVer.
|
|
6
|
-
VERSION = "0.1.0-alpha.
|
|
6
|
+
VERSION = "0.1.0-alpha.8"
|
|
7
7
|
|
|
8
8
|
# The same version in RubyGems' spelling, which the gemspec uses.
|
|
9
|
-
GEM_VERSION = "0.1.0.pre.alpha.
|
|
9
|
+
GEM_VERSION = "0.1.0.pre.alpha.8"
|
|
10
10
|
|
|
11
11
|
# The API version the app view was generated from.
|
|
12
12
|
GENERATED_FROM = "2026-10-virile-dragon"
|
data/lib/lingara/apps.rb
CHANGED
|
@@ -19,6 +19,8 @@ require_relative "apps/unions"
|
|
|
19
19
|
require_relative "apps/operations"
|
|
20
20
|
require_relative "apps/limits"
|
|
21
21
|
require_relative "apps/card"
|
|
22
|
+
require_relative "apps/listing"
|
|
23
|
+
require_relative "apps/i18n"
|
|
22
24
|
require_relative "apps/manifest"
|
|
23
25
|
require_relative "apps/app"
|
|
24
26
|
require_relative "apps/rack_app"
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lingara-apps
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.0.pre.alpha.
|
|
4
|
+
version: 0.1.0.pre.alpha.8
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Spinning Cat Studios
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-22 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: lingara
|
|
@@ -49,7 +49,9 @@ files:
|
|
|
49
49
|
- lib/lingara/apps/generated/app_slot_name.rb
|
|
50
50
|
- lib/lingara/apps/generated/button_style.rb
|
|
51
51
|
- lib/lingara/apps/generated/card.rb
|
|
52
|
+
- lib/lingara/apps/i18n.rb
|
|
52
53
|
- lib/lingara/apps/limits.rb
|
|
54
|
+
- lib/lingara/apps/listing.rb
|
|
53
55
|
- lib/lingara/apps/manifest.rb
|
|
54
56
|
- lib/lingara/apps/operations.rb
|
|
55
57
|
- lib/lingara/apps/rack_app.rb
|